netty5自定义私有协议实例
一般业务需求都会自行定义私有协议来满足自己的业务场景,私有协议也可以解决粘包和拆包问题,比如客户端发送数据时携带数据包长度,服务端接收数据后解析消息体,获取数据包长度值,据此继续获取数据包内容。我们来看具体例子,自定义的协议如下:
+--------------------------------------------------+----------+
| 消息头 | 消息体 |
| Delimiter | Length | Type | Reserved | data |
+-------------------------------------------------+----------+
1) Delimiter:4bytes,消息头,用于分割消息。
2) Length:数据长度。
3) Type:1bytes,消息类型。
4) Reserved:1bytes,保留。
5) Data包数据
接下来看如何实现编解码:
1、先定义好javabean:
总体的:
package com.wlf.netty.nettyapi.javabean; import lombok.Getter; import lombok.Setter; @Setter @Getter public class NettyMessage { private Header header; private byte[] data; @Override public String toString() { return "NettyMessage{" + "header=" + header + ", data=" + data + '}'; } }
头的:
package com.wlf.netty.nettyapi.javabean; import lombok.Getter; import lombok.Setter; @Getter @Setter public class Header { /** * 4bytes,消息头,用于分割消息。如0xABEF0101 */ private int delimiter; /** * 1byte,类型 */ private byte type; /** * 1byte,保留 */ private byte reserved; /** * 数据长度 */ private int length; @Override public String toString() { return "Header{" + "delimiter=" + delimiter + ", length=" + length + ", type=" + type + ", reserved=" + reserved + '}'; } }
2、编码:
package com.wlf.netty.nettyapi.msgpack; import com.wlf.netty.nettyapi.javabean.NettyMessage; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; public class NettyMessageEncoder extends MessageToByteEncoder<NettyMessage> { @Override protected void encode(ChannelHandlerContext channelHandlerContext, NettyMessage nettyMessage, ByteBuf byteBuf) throws Exception { if (nettyMessage == null || nettyMessage.getHeader() == null) { throw new Exception("The nettyMessage is null."); } // 1、写入分割标志 byteBuf.writeInt(nettyMessage.getHeader().getDelimiter()); // 2、写入数据包长度 byteBuf.writeInt(nettyMessage.getData() != null ? nettyMessage.getData().length : 0); // 3、写入请求类型 byteBuf.writeByte(nettyMessage.getHeader().getType()); // 4、写入预留字段 byteBuf.writeByte(nettyMessage.getHeader().getReserved()); // 5、写入数据 byteBuf.writeBytes(nettyMessage.getData() != null ? nettyMessage.getData() : null); } }
3、解码:
package com.wlf.netty.nettyapi.msgpack; import com.wlf.netty.nettyapi.constant.Delimiter; import com.wlf.netty.nettyapi.javabean.Header; import com.wlf.netty.nettyapi.javabean.NettyMessage; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import java.util.List; public class NettyMessageDecoder extends ByteToMessageDecoder { /** * 消息体字节大小:分割符字段4字节+长度字段4字节+请求类型字段1字节+预留字段1字节=10字节 */ private static final int HEAD_LENGTH = 10; @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception { // 字节流开始位置 int packStartIndex; while (true) { // 获取字节流开始位置 packStartIndex = byteBuf.readerIndex(); // 若读取到分割标识,说明读取当前字节流开始位置了 if (byteBuf.readInt() == Delimiter.DELIMITER) { break; } // 重置读索引为0 byteBuf.resetReaderIndex(); // 长度校验,字节流长度至少10字节,小于10字节则等待下一次字节流过来 if (byteBuf.readableBytes() < HEAD_LENGTH) { return; } } // 2、获取data的字节流长度 int dataLength = byteBuf.readInt(); // 校验数据包是否全部发送过来,总字节流长度(此处读取的是除去delimiter和length之后的总长度)减去type和reserved两个字节=data的字节流长度 int totalLength = byteBuf.readableBytes(); if ((totalLength - 2) < dataLength) { // 长度校验,字节流长度少于data数据包长度,说明数据包拆包了,等待下一次字节流过来 byteBuf.readerIndex(packStartIndex); return; } // 3、请求类型 byte type = byteBuf.readByte(); // 4、预留字段 byte reserved = byteBuf.readByte(); // 5、数据包内容 byte[] data = null; if (dataLength > 0) { data = new byte[dataLength]; byteBuf.readBytes(data); } NettyMessage nettyMessage = new NettyMessage(); Header header = new Header(); header.setDelimiter(0xABEF0101); header.setLength(dataLength); header.setType(type); header.setReserved(reserved); nettyMessage.setHeader(header); nettyMessage.setData(data); list.add(nettyMessage); // 回收已读字节 byteBuf.discardReadBytes(); } }
为了运行,我们需要写客户端和服务端的handler:
4、客户端handler:
package com.wlf.netty.nettyclient.handler; import com.wlf.netty.nettyapi.constant.Delimiter; import com.wlf.netty.nettyapi.constant.MessageType; import com.wlf.netty.nettyapi.javabean.Header; import com.wlf.netty.nettyapi.javabean.NettyMessage; import com.wlf.netty.nettyapi.util.CommonUtil; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import java.io.RandomAccessFile; import java.util.Arrays; /** * 客户端处理类 */ @Slf4j public class NettyClientHandler extends ChannelHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(buildClientRequest()); } /** * 创建请求消息体 * * @return */ private NettyMessage buildClientRequest() { NettyMessage nettyMessage = new NettyMessage(); Header header = new Header(); byte[] data = new byte[0]; try { data = buildPcmData(); } catch (Exception e) { e.printStackTrace(); } header.setDelimiter(0xABEF0101); header.setLength(data.length); header.setType((byte) 1); header.setReserved((byte) 0); nettyMessage.setHeader(header); // 设置数据包 nettyMessage.setData(data); return nettyMessage; } /** * 构造PCM请求消息体 * * @return */ private byte[] buildPcmData() throws Exception { byte[] resultByte = longToBytes(System.currentTimeMillis()); return resultByte; } /** * long转字节 * * @param values * @return */ private byte[] longToBytes(long values) { byte[] buffer = new byte[8]; for (int i = 0; i < 8; i++) { int offset = 64 - (i + 1) * 8; buffer[i] = (byte) ((values >> offset) & 0xff); } return buffer; } /** * 将两个数组合并起来 * * @param array1 * @param array2 * @return */ private byte[] addAll(byte[] array1, byte... array2) { byte[] joinedArray = new byte[array1.length + array2.length]; System.arraycopy(array1, 0, joinedArray, 0, array1.length); System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); return joinedArray; } /** * 在处理过程中引发异常时被调用 * * @param ctx * @param cause * @throws Exception */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { log.error("[Client] netty client request error: {}", cause.getMessage()); ctx.close(); } }
5、服务端handler:
package com.wlf.netty.nettyserver.handler; import com.alibaba.fastjson.JSON; import com.wlf.netty.nettyapi.constant.MessageType; import com.wlf.netty.nettyapi.javabean.NettyMessage; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import lombok.extern.slf4j.Slf4j; @Slf4j public class NettyServerHandler extends ChannelHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { NettyMessage nettyMessage = (NettyMessage) msg; if (nettyMessage.getHeader() != null && nettyMessage.getHeader().getType() == (byte) 1) { log.info("[server] server receive client message : {}", nettyMessage); if (nettyMessage == null || nettyMessage.getData() == null) { log.error("nettyMessage is null."); } // 获取时间戳(8字节) byte[] data = nettyMessage.getData(); ByteBuf buf = Unpooled.buffer(data.length); buf.writeBytes(data); long startTime = buf.readLong(); log.info("data length: {}", data.length); log.info("startTime: {}", startTime); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { log.error("server received failed, error : {}", cause.getMessage()); cause.printStackTrace(); ctx.close(); } }
最后,为了启动,我们还得再补两个启动类:
6、客户端:
package com.wlf.netty.nettyclient.client; import com.wlf.netty.nettyapi.msgpack.NettyMessageDecoder; import com.wlf.netty.nettyapi.msgpack.NettyMessageEncoder; import com.wlf.netty.nettyclient.handler.NettyClientHandler; 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 lombok.extern.slf4j.Slf4j; import java.net.InetSocketAddress; /** * 客户端 * 1.为初始化客户端,创建一个Bootstrap实例 * 2.为进行事件处理分配了一个NioEventLoopGroup实例,其中事件处理包括创建新的连接以及处理入站和出站数据; * 3.当连接被建立时,一个NettyClientHandler实例会被安装到(该Channel的一个ChannelPipeline中; * 4.在一切都设置完成后,调用Bootstrap.connect()方法连接到远程节点。 */ @Slf4j public class NettyClient { private ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); EventLoopGroup group = new NioEventLoopGroup(); public void connect(int port, String host) throws Exception { NioEventLoopGroup workGroup = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(workGroup).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel channel) throws Exception { channel.pipeline().addLast(new NettyMessageDecoder()); channel.pipeline().addLast(new NettyMessageEncoder()); channel.pipeline().addLast(new NettyClientHandler()); } }); ChannelFuture future = bootstrap.connect(host, port).sync(); future.channel().closeFuture().sync(); } finally { workGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 9911; new NettyClient().connect(port, "127.0.0.1"); } }
7、服务端启动类:
package com.wlf.netty.nettyserver.server; import com.wlf.netty.nettyapi.msgpack.NettyMessageDecoder; import com.wlf.netty.nettyapi.msgpack.NettyMessageEncoder; import com.wlf.netty.nettyserver.handler.NettyServerHandler; 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; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler;import lombok.extern.slf4j.Slf4j; @Slf4j public class NettyServer { private final EventLoopGroup bossGroup = new NioEventLoopGroup(); private final EventLoopGroup workGroup = new NioEventLoopGroup(); public void bind(int port) throws Exception{ try{ ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 100) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel channel) throws Exception { channel.pipeline().addLast(new NettyMessageDecoder()); channel.pipeline().addLast(new NettyMessageEncoder()); channel.pipeline().addLast(new NettyServerHandler()); } }); // 绑定端口 ChannelFuture channelFuture = serverBootstrap.bind(port).sync(); channelFuture.channel().closeFuture().sync(); }finally { bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 9911; new NettyServer().bind(port); } }
直接跑上面两个启动类,先跑服务端,再跑客户端:
客户端输出:
17:20:04.258 [nioEventLoopGroup-1-1] INFO com.wlf.netty.nettyclient.handler.NettyClientHandler - [client] client send data : NettyMessage{header=Header{delimiter=-1410399999, length=8, type=1, reserved=0}, data=[B@432f82d5}
服务端输出:
17:20:04.295 [nioEventLoopGroup-1-1] INFO com.wlf.netty.nettyserver.handler.NettyServerHandler - [server] server receive client message : NettyMessage{header=Header{delimiter=-1410399999, length=8, type=1, reserved=0}, data=[B@16eddbb3}
17:20:04.295 [nioEventLoopGroup-1-0] INFO com.wlf.netty.nettyserver.handler.NettyServerHandler - data length: 8
17:20:04.295 [nioEventLoopGroup-1-1] INFO com.wlf.netty.nettyserver.handler.NettyServerHandler - startTime: 1570785604258
以上解码在字节流总容量小于1024时都没问题,但超过就会出现服务端获取不到数据的问题,详情和解决办法请参见netty5拆包问题解决实例。