【Netty】自定义网络通信协议
介绍
本文主要介绍如何通过Netty自定义网络通信协议,协议比较简单和基础,相关的代码和原理也都不难理解,,重点是如何利用netty实现整个流程。我会提供完整的代码,有如何自定义编解码器的实现。
总共大概有如下几个步骤:
- 定义消息通信协议
- 定义解码器
- 定义编码器
- 定义服务器ChannelHandler
- 定义客户端ChannelHandler
- 定义服务器主程序
- 定义客户端主程序
总共就这几步,做完这些就可以测试了~因为都比较简单,所以不过多解释,下面开始实现。
消息通信协议
消息
package cn.md.netty.codec;
/**
* * @Author: Martin
* * @Date 2024/9/4 22:25
* * @Description
**/
public class Msg {
private MsgHeader msgHeader = new MsgHeader();
private String body;
public MsgHeader getMsgHeader() {
return msgHeader;
}
public void setMsgHeader(MsgHeader msgHeader) {
this.msgHeader = msgHeader;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public static Msg buildFromReqBody(String body) {
Msg msg = new Msg();
msg.setMsgHeader(new MsgHeader(MsgType.LOGIN_REQ.getValue(), body.getBytes().length));
msg.setBody(body);
return msg;
}
public static Msg buildFromResBody(String body) {
Msg msg = new Msg();
msg.setMsgHeader(new MsgHeader(MsgType.LOGIN_RES.getValue(), body.getBytes().length));
msg.setBody(body);
return msg;
}
}
消息头
package cn.md.netty.codec;
/**
* * @Author: Martin
* * @Date 2024/9/4 22:24
* * @Description
**/
public class MsgHeader {
// 消息类型
private byte msgType;
// 消息体长度
private int len;
public MsgHeader() {
}
public MsgHeader(byte msgType, int len) {
this.msgType = msgType;
this.len = len;
}
public byte getMsgType() {
return msgType;
}
public void setMsgType(byte msgType) {
this.msgType = msgType;
}
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
}
定义解码器
package cn.md.netty.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* * @Author: Martin
* * @Date 2024/9/4 22:38
* * @Description
**/
public class MyDecoder extends ByteToMessageDecoder {
/**
* Decode the from one {@link ByteBuf} to an other. This method will be called till either the input
* {@link ByteBuf} has nothing to read when return from this method or till nothing was read from the input
* {@link ByteBuf}.
*
* @param ctx the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to
* @param in the {@link ByteBuf} from which to read data
* @param out the {@link List} to which decoded messages should be added
* @throws Exception is thrown if an error occurs
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < 5) {
// 至少需要消息类型(1 字节)和消息体长度(4 字节)
return;
}
byte msgType = in.readByte();
int len = in.readInt();
if (in.readableBytes() < len) {
// 消息不完整,等待更多数据
in.resetReaderIndex();
return;
}
byte[] bodyBytes = new byte[len];
in.readBytes(bodyBytes);
Msg msg = new Msg();
msg.setMsgHeader(new MsgHeader(msgType, len));
msg.setBody(new String(bodyBytes, StandardCharsets.UTF_8));
out.add(msg);
}
}
定义编码器
package cn.md.netty.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import java.nio.charset.Charset;
/**
* 将Msg编码为ByteBuf
* * @Author: Martin
* * @Date 2024/9/4 22:26
* * @Description
**/
public class MyEncoder extends MessageToByteEncoder<Msg> {
/**
* Encode a message into a {@link ByteBuf}. This method will be called for each written message that can be handled
* by this encoder.
*
* @param ctx the {@link ChannelHandlerContext} which this {@link MessageToByteEncoder} belongs to
* @param msg the message to encode
* @param out the {@link ByteBuf} into which the encoded message will be written
* @throws Exception is thrown if an error occurs
*/
@Override
protected void encode(ChannelHandlerContext ctx, Msg msg, ByteBuf out) throws Exception {
if (msg == null || msg.getMsgHeader() == null) {
throw new Exception("The encode message is null");
}
MsgHeader header = msg.getMsgHeader();
String body = msg.getBody();
byte[] bodyBytes = body.getBytes(Charset.forName("utf-8"));
// 计算消息体的长度
int length = bodyBytes.length;
// 写入消息类型
out.writeByte(MsgType.LOGIN_REQ.getValue());
// 写入消息体长度
out.writeInt(length);
// 写入消息体
out.writeBytes(bodyBytes);
}
}
定义服务器ChannelHandler
package cn.md.netty.codec;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.net.SocketAddress;
/**
* * @Author: Martin
* * @Date 2024/9/4 22:48
* * @Description
**/
public class MyServerHandler extends SimpleChannelInboundHandler<Object> {
/**
* Is called for each message of type {@link I}.
*
* @param ctx the {@link ChannelHandlerContext} which this {@link SimpleChannelInboundHandler}
* belongs to
* @param obj the message to handle
* @throws Exception is thrown if an error occurred
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object obj) throws Exception {
if (obj instanceof Msg) {
Msg msg = (Msg) obj;
String body = msg.getBody();
System.out.println("服务端收到消息:" + body);
Msg resMsg = Msg.buildFromResBody("没听清,你再说一边");
ctx.writeAndFlush(resMsg);
}
}
}
定义客户端ChannelHandler
package cn.md.netty.codec.client;
import cn.md.netty.codec.Msg;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* * @Author: Martin
* * @Date 2024/9/4 22:52
* * @Description
**/
public class MyClientHandler extends SimpleChannelInboundHandler<Object> {
/**
* Is called for each message of type {@link I}.
*
* @param ctx the {@link ChannelHandlerContext} which this {@link SimpleChannelInboundHandler}
* belongs to
* @param obj the message to handle
* @throws Exception is thrown if an error occurred
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object obj) throws Exception {
if (obj instanceof Msg) {
Msg msg = (Msg) obj;
String body = msg.getBody();
System.out.println("客户端收到消息:" + body);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelInactive");
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved");
}
}
定义服务器主程序
package cn.md.netty.codec;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* * @Author: Martin
* * @Date 2024/9/4 22:53
* * @Description
**/
public class MyServer {
public static void main(String[] args) throws InterruptedException {
ServerBootstrap b = new ServerBootstrap();
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
b.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,64)
.childOption(ChannelOption.SO_KEEPALIVE,true)
.childHandler(new ChannelInitializer<SocketChannel>() {
/**
* This method will be called once the {@link Channel} was registered. After the method returns this instance
* will be removed from the {@link ChannelPipeline} of the {@link Channel}.
*
* @param ch the {@link Channel} which was registered.
* @throws Exception is thrown if an error occurs. In that case it will be handled by
* {@link #exceptionCaught(ChannelHandlerContext, Throwable)} which will by default close
* the {@link Channel}.
*/
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast("decoder", new MyDecoder())
.addLast("encoder", new MyEncoder())
//.addLast("codec", new MyCodec())
.addLast("handler", new MyServerHandler());
}
});
ChannelFuture channelFuture = b.bind(7788).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
throw e;
} finally{
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
定义客户端主程序
package cn.md.netty.codec.client;
import cn.md.netty.codec.Msg;
import cn.md.netty.codec.MyCodec;
import cn.md.netty.codec.MyDecoder;
import cn.md.netty.codec.MyEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.util.Scanner;
/**
* * @Author: Martin
* * @Date 2024/9/4 22:56
* * @Description
**/
public class MyClient {
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup loopGroup = new NioEventLoopGroup(1);
Bootstrap b = bootstrap.group(loopGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
/**
* This method will be called once the {@link Channel} was registered. After the method returns this instance
* will be removed from the {@link ChannelPipeline} of the {@link Channel}.
*
* @param ch the {@link Channel} which was registered.
* @throws Exception is thrown if an error occurs. In that case it will be handled by
* {@link #exceptionCaught(ChannelHandlerContext, Throwable)} which will by default close
* the {@link Channel}.
*/
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast("decoder", new MyDecoder())
.addLast("encoder", new MyEncoder())
// .addLast("codec", new MyCodec())
.addLast("handler", new MyClientHandler());
}
});
ChannelFuture future = b.connect("localhost", 7788);
while (true) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要发送的消息内容");
String msg = scanner.nextLine();
if ("exit".equals(msg)) {
future.channel().close();
break;
}
Msg sendMsg = Msg.buildFromReqBody(msg);
future.channel().writeAndFlush(sendMsg);
}
}
}
测试结果
客户端:
服务端:
其他
在客户端和服务端的主程序中,在添加ChannelHandler时,都设置了编码器和解码器
ch.pipeline()
.addLast("decoder", new MyDecoder())
.addLast("encoder", new MyEncoder())
// .addLast("codec", new MyCodec())
.addLast("handler", new MyClientHandler());
也可以使用一个聚合的编解码器实现,就是MyCodec 等同于 MyDecoder + MyEncoder。
把下面的注释打开,把上面的编解码器注释,如下:
ch.pipeline()
//.addLast("decoder", new MyDecoder())
//.addLast("encoder", new MyEncoder())
.addLast("codec", new MyCodec())
.addLast("handler", new MyClientHandler());
MyCodec.java
package cn.md.netty.codec;
import io.netty.channel.CombinedChannelDuplexHandler;
/**
* * @Author: Martin
* * @Date 2024/9/5 12:10
* * @Description
**/
public class MyCodec extends CombinedChannelDuplexHandler<MyDecoder,MyEncoder> {
public MyCodec()
{
super(new MyDecoder(),new MyEncoder());
}
}
文章到这结束了,本文介绍了如何自定义一个简单的通信协议,并通过Netty进行通络通信交互。
代码很简单,希望能举一反三,融会贯通。后续我还会持续分享一些关于Netty的文章,不妨点个关注~