文章目录
1. Netty的粘包与半包现象
1.1 粘包现象
-
以下代码展示了黏包现象的产生
package com.zhyn.nettyadvance; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; 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 Server0 { public static void main(String[] args) { new Server0().start(); } public void start() { NioEventLoopGroup boss = new NioEventLoopGroup(1); NioEventLoopGroup worker = new NioEventLoopGroup(); try { final ChannelFuture channelFuture = new ServerBootstrap() .group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override // 连接建立时会执行该方法 public void channelActive(ChannelHandlerContext ctx) throws Exception { log.debug("connected {}", ctx.channel()); super.channelActive(ctx); } @Override // 连接断开时会执行该方法 public void channelInactive(ChannelHandlerContext ctx) throws Exception { log.debug("disconnect {}", ctx.channel()); super.channelInactive(ctx); } }); } }).bind(8080); log.debug("{} binding...", channelFuture.channel()); channelFuture.sync(); log.debug("{} bound", channelFuture.channel()); // 关闭channel channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { log.error("server error", e); } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); log.debug("stopped"); } } }
package com.zhyn.nettyadvance; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; 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; @Slf4j public class Client0 { public static void main(String[] args) { NioEventLoopGroup worker = new NioEventLoopGroup(); try { ChannelFuture channelFuture = new Bootstrap() .channel(NioSocketChannel.class) .group(worker) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { log.debug("connected..."); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { log.debug("sending..."); // 每次发送16个字节的数据,一共发送10次 for (int i = 0; i < 10; i++) { ByteBuf buffer = ctx.alloc().buffer(); buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}); ctx.writeAndFlush(buffer); } } }); } }) .connect(new InetSocketAddress(8080)) .sync(); channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { log.error("client error", e); } finally { worker.shutdownGracefully(); } } }
-
输出结果:从输出结果中可以看出服务器一次就接受到了160个字节的数据,而不是分10次接收16个字节的数据,这就是黏包问题;
10:38:07 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x117c169d, L:/192.168.31.158:8080 - R:/192.168.31.158:54158] READ: 160B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| |00000010| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| |00000020| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| |00000030| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| |00000040| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| |00000050| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| |00000060| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| |00000070| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| |00000080| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| |00000090| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| +--------+-------------------------------------------------+----------------+
1.2 半包现象
-
将代码做以下的的修改,使得客户端代码发送 1 个消息,这个消息是 160 个字节,并将客户端-服务器之间的channel容量进行调整:
// 对客户端的修改 ByteBuf buffer = ctx.alloc().buffer(); for (int i = 0; i < 10; i++) { buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}); } ctx.writeAndFlush(buffer); // 对服务端的修改 // serverBootstrap.option(ChannelOption.SO_RCVBUF, 10) 实际上影响的是底层接收缓冲区(即滑动窗口)的大小 // 它仅仅决定了 netty 读取数据的最小单位,但是 netty 实际上每次读取的数据大小一般是它的整数倍 serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
-
输出结果:可以看到接收消息被分为两节,第一节为20个字节,第二节为140个字节,这就是半包现象;
10:49:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xa1b502a3, L:/192.168.31.158:8080 - R:/192.168.31.158:54473] READ: 20B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| |00000010| 00 01 02 03 |.... | +--------+-------------------------------------------------+----------------+ 10:49:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xa1b502a3, L:/192.168.31.158:8080 - R:/192.168.31.158:54473] READ COMPLETE 10:49:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xa1b502a3, L:/192.168.31.158:8080 - R:/192.168.31.158:54473] READ: 140B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................| |00000010| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................| |00000020| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................| |00000030| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................| |00000040| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................| |00000050| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................| |00000060| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................| |00000070| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................| |00000080| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |............ | +--------+-------------------------------------------------+----------------+
1.3 对于粘包现象和半包现象的分析
-
粘包现象,发送 abc def,接收 abcdef;
-
具体原因:
- 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
- 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文时就会发生粘包
- Nagle 算法:TCP, IP的报头都是20个字节,哪怕我们想要传输的内容仅仅是1个字节,传输的时候也会有41个字节大小的数据,为了避免这一现象的发生,Nagle 算法会尽可能多地发送数据,所以会发生黏包现象;
-
半包现象,发送 abcdef,接收 abc def
-
具体原因:
- 应用层:接收方 ByteBuf 小于实际发送数据量;
- 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包;
- MSS 限制:当发送的数据大小超过 MSS 限制后,会将数据切分发送,就会造成半包(localhost回环地址,也就是我们平时做测试用的本机地址对MSS几乎是没有限制,非常的大,我们自己不容易复现这个现象;补充:MTU是最大传输单元,由硬件规定,以太网的MTU为1500字节;MSS是最大分节大小,为TCP数据包每次传输的最大数据分段大小,MSS的值为MTU减去IPv4Header和TCPHeader的大小得到的);
- 本质是因为 TCP 是流式协议,消息无边界;
-
关于滑动窗口的补充:
-
TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,这么做的缺点是包的往返时间越长性能就越差;
-
为了解决此问题,引入了窗口概念,窗口大小决定了无需等待应答而可以继续发送的数据的最大值;
-
窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用
- 该窗口是可以滑动的,每处理完一批数据,窗口就可以向下滑动继续处理下一批数据;
- 窗口内的数据才允许被发送,当应答未到达前,窗口必须停止滑动
- 假设发送方滑动窗口的大小是1000.如果 1001~2000 这个段的数据 ack 回来了,窗口就可以向前滑动;
- 接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收;
- 滑动窗口的大小一般是可调整自适应的,不需要我们可以的设置滑动窗口的大小;
- Netty对客户端默认的滑动窗口(缓冲区)大小是1024;
-
-
关于MSS的补充
- 链路层对一次能够发送的最大数据有限制,这个限制称之为 MTU(maximum transmission unit),不同的链路设备的 MTU 值也有所不同,例如以太网的 MTU 是 1500,FDDI(光纤分布式数据接口)的 MTU 是 4352,本地回环地址的 MTU 是 65535 - 本地测试不走网卡;
- MSS 是最大段长度(maximum segment size),它是 MTU 刨去 tcp 头和 ip 头后剩余能够作为数据传输的字节数;
- ipv4 tcp 头占用 20 bytes,ip 头占用 20 bytes,因此以太网 MSS 的值为 1500 - 40 = 1460;
- TCP 在传递大量数据时,会按照 MSS 大小将数据进行分割发送;
- MSS 的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为 MSS;
-
关于Nagle 算法的补充
- 即使发送一个字节,也需要加入 tcp 头和 ip 头,也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由;
- 该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送;
- 如果 SO_SNDBUF 的数据达到 MSS,则需要发送
- 如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭
- 如果 TCP_NODELAY = true,则需要发送
- 已发送的数据都收到 ack 时,则需要发送
- 上述条件不满足,但发生超时(一般为 200ms)则需要发送
- 除上述情况之外,会延迟发送
1.4 解决方案
- 短链接,即发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点就是效率太低;
- 每一条消息采用固定长度,缺点浪费空间;
- 每一条消息采用分隔符,例如 \n,缺点需要转义;
- 每一条消息分为 head 和 body,head 中包含 body 的长度;
方法1:短链接
-
客户端每次向服务器发送数据以后,就与服务器断开连接,此时的消息边界为连接建立到连接断开;这时便无需使用滑动窗口等技术来缓冲数据,自然也不会发生粘包现象了;但如果一次性发送过多的数据而接收方又无法一次性容纳发送过来的所有数据,还是会发生半包现象,所以短链接无法解决半包现象;
-
客户端代码更改:修改channelActive方法并将发送步骤封装为send()方法,在这里我们调用10次send()方法模拟10次数据的发送;
package com.zhyn.nettyadvance; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; 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 Server0 { public static void main(String[] args) { new Server0().start(); } public void start() { NioEventLoopGroup boss = new NioEventLoopGroup(1); NioEventLoopGroup worker = new NioEventLoopGroup(); try { final ChannelFuture channelFuture = new ServerBootstrap() .group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override // 连接建立时会执行该方法 public void channelActive(ChannelHandlerContext ctx) throws Exception { log.debug("connected {}", ctx.channel()); super.channelActive(ctx); } @Override // 连接断开时会执行该方法 public void channelInactive(ChannelHandlerContext ctx) throws Exception { log.debug("disconnect {}", ctx.channel()); super.channelInactive(ctx); } }); } }).bind(8080); log.debug("{} binding...", channelFuture.channel()); channelFuture.sync(); log.debug("{} bound", channelFuture.channel()); // 关闭channel channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { log.error("server error", e); } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); log.debug("stopped"); } } }
package com.zhyn.nettyadvance; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; 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; @Slf4j public class Client0 { public static void main(String[] args) { for (int i = 0; i < 10; i++) { send(); } } public static void send() { NioEventLoopGroup worker = new NioEventLoopGroup(); try { ChannelFuture channelFuture = new Bootstrap() .channel(NioSocketChannel.class) .group(worker) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { log.debug("connected..."); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { log.debug("sending..."); ByteBuf buffer = ctx.alloc().buffer(16); buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}); ctx.writeAndFlush(buffer); // 使用短链接,每次发送完毕后就断开连接 ctx.channel().close(); } }); } }) .connect(new InetSocketAddress(8080)) .sync(); channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { log.error("client error", e); } finally { worker.shutdownGracefully(); } } }
16:08:08 [DEBUG] [nioEventLoopGroup-3-3] i.n.h.l.LoggingHandler - [id: 0x1bcf3b5c, L:/192.168.31.158:8080 - R:/192.168.31.158:61580] ACTIVE 16:08:08 [DEBUG] [nioEventLoopGroup-3-3] c.z.n.Server0 - connected [id: 0x1bcf3b5c, L:/192.168.31.158:8080 - R:/192.168.31.158:61580] 16:08:08 [DEBUG] [nioEventLoopGroup-3-3] i.n.h.l.LoggingHandler - [id: 0x1bcf3b5c, L:/192.168.31.158:8080 - R:/192.168.31.158:61580] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| +--------+-------------------------------------------------+----------------+ 16:08:08 [DEBUG] [nioEventLoopGroup-3-3] i.n.h.l.LoggingHandler - [id: 0x1bcf3b5c, L:/192.168.31.158:8080 - R:/192.168.31.158:61580] READ COMPLETE 16:08:08 [DEBUG] [nioEventLoopGroup-3-3] i.n.h.l.LoggingHandler - [id: 0x1bcf3b5c, L:/192.168.31.158:8080 - R:/192.168.31.158:61580] READ COMPLETE 16:08:08 [DEBUG] [nioEventLoopGroup-3-3] i.n.h.l.LoggingHandler - [id: 0x1bcf3b5c, L:/192.168.31.158:8080 ! R:/192.168.31.158:61580] INACTIVE 16:08:08 [DEBUG] [nioEventLoopGroup-3-3] c.z.n.Server0 - disconnect [id: 0x1bcf3b5c, L:/192.168.31.158:8080 ! R:/192.168.31.158:61580] 16:08:08 [DEBUG] [nioEventLoopGroup-3-3] i.n.h.l.LoggingHandler - [id: 0x1bcf3b5c, L:/192.168.31.158:8080 ! R:/192.168.31.158:61580] UNREGISTERED 16:08:08 [DEBUG] [nioEventLoopGroup-3-4] i.n.h.l.LoggingHandler - [id: 0x7d60f706, L:/192.168.31.158:8080 - R:/192.168.31.158:61613] REGISTERED 16:08:08 [DEBUG] [nioEventLoopGroup-3-4] i.n.h.l.LoggingHandler - [id: 0x7d60f706, L:/192.168.31.158:8080 - R:/192.168.31.158:61613] ACTIVE 16:08:08 [DEBUG] [nioEventLoopGroup-3-4] c.z.n.Server0 - connected [id: 0x7d60f706, L:/192.168.31.158:8080 - R:/192.168.31.158:61613] 16:08:08 [DEBUG] [nioEventLoopGroup-3-4] i.n.h.l.LoggingHandler - [id: 0x7d60f706, L:/192.168.31.158:8080 - R:/192.168.31.158:61613] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................| +--------+-------------------------------------------------+----------------+ 16:08:08 [DEBUG] [nioEventLoopGroup-3-4] i.n.h.l.LoggingHandler - [id: 0x7d60f706, L:/192.168.31.158:8080 - R:/192.168.31.158:61613] READ COMPLETE 16:08:08 [DEBUG] [nioEventLoopGroup-3-4] i.n.h.l.LoggingHandler - [id: 0x7d60f706, L:/192.168.31.158:8080 - R:/192.168.31.158:61613] READ COMPLETE 16:08:08 [DEBUG] [nioEventLoopGroup-3-4] i.n.h.l.LoggingHandler - [id: 0x7d60f706, L:/192.168.31.158:8080 ! R:/192.168.31.158:61613] INACTIVE
-
客户端先于服务器建立连接,此时控制台打印ACTIVE,之后客户端向服务器发送了16B的数据,发送后便断开连接,此时控制台打印INACTIVE,可见未出现粘包现象;
方法2:基于定长解码器的固定长度
-
客户端与服务器约定一个最大长度,保证客户端每次发送的数据长度都不会大于该长度。若发送数据长度不足则需要补齐至该长度;
-
服务器接收数据时,将接收到的数据按照约定的最大长度进行拆分,即使发送过程中产生了粘包,也可以通过定长解码器将数据正确地进行拆分。服务端需要用到FixedLengthFrameDecoder对数据进行定长解码,具体使用方法如下;
ch.pipeline().addLast(new FixedLengthFrameDecoder(16));
-
客户端代码
package com.zhyn.nettyadvance; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; 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; @Slf4j public class Client0 { public static void main(String[] args) { for (int i = 0; i < 10; i++) { send(); } System.out.println("finish"); } public static void send() { NioEventLoopGroup worker = new NioEventLoopGroup(); try { ChannelFuture channelFuture = new Bootstrap() .channel(NioSocketChannel.class) .group(worker) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { log.debug("connected..."); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { log.debug("sending..."); // 约定最大长度为16 final int maxLength = 16; // 被发送的数据 char c = 'a'; // 向服务器发送10个报文 for (int i = 0; i < 10; i++) { ByteBuf buffer = ctx.alloc().buffer(maxLength); // 定长byte数组,未使用部分会以0进行填充 byte[] bytes = new byte[maxLength]; // 生成长度为0~15的数据 for (int j = 0; j < (int)(Math.random() * (maxLength - 1)); j++) { bytes[j] = (byte) c; } buffer.writeBytes(bytes); c++; // 将数据发送给服务器 ctx.writeAndFlush(buffer); } } }); } }) .connect(new InetSocketAddress(8080)) .sync(); channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { log.error("client error", e); } finally { worker.shutdownGracefully(); } } }
-
服务器端只需要使用FixedLengthFrameDecoder对粘包数据进行拆分即可,该handler需要添加在LoggingHandler之前,保证数据被打印时已被拆分;
// 通过定长解码器对粘包数据进行拆分 ch.pipeline().addLast(new FixedLengthFrameDecoder(16)); ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
-
输出结果
16:12:33 [DEBUG] [main] c.z.n.Server0 - [id: 0x1d4d0a2e] binding... 16:12:33 [DEBUG] [main] c.z.n.Server0 - [id: 0x1d4d0a2e, L:/0:0:0:0:0:0:0:0:8080] bound 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] REGISTERED 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] ACTIVE 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] c.z.n.Server0 - connected [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 61 61 61 00 00 00 00 00 00 00 00 00 00 00 00 00 |aaa.............| +--------+-------------------------------------------------+----------------+ 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 62 62 62 62 00 00 00 00 00 00 00 00 00 00 00 00 |bbbb............| +--------+-------------------------------------------------+----------------+ 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 63 63 63 63 63 00 00 00 00 00 00 00 00 00 00 00 |ccccc...........| +--------+-------------------------------------------------+----------------+ 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 64 64 64 64 64 64 64 64 00 00 00 00 00 00 00 00 |dddddddd........| +--------+-------------------------------------------------+----------------+ 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 65 65 65 00 00 00 00 00 00 00 00 00 00 00 00 00 |eee.............| +--------+-------------------------------------------------+----------------+ 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 66 66 66 00 00 00 00 00 00 00 00 00 00 00 00 00 |fff.............| +--------+-------------------------------------------------+----------------+ 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 67 67 67 67 67 67 67 00 00 00 00 00 00 00 00 00 |ggggggg.........| +--------+-------------------------------------------------+----------------+ 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 68 68 68 68 68 68 68 68 00 00 00 00 00 00 00 00 |hhhhhhhh........| +--------+-------------------------------------------------+----------------+ 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 69 69 69 69 69 69 00 00 00 00 00 00 00 00 00 00 |iiiiii..........| +--------+-------------------------------------------------+----------------+ 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 6a 6a 6a 6a 6a 00 00 00 00 00 00 00 00 00 00 00 |jjjjj...........| +--------+-------------------------------------------------+----------------+ 16:12:36 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xf2302659, L:/192.168.31.158:8080 - R:/192.168.31.158:61949] READ COMPLETE
-
但是这种方法有一个缺点就是数据包的大小不好掌握,数据包的长度太大的话会造成浪费,而且客户端会发送的数据的最大长度也不好得到;
方法3:基于行解码器使用固定的分隔符
-
行解码器的是通过分隔符对数据进行拆分来解决粘包半包问题的;
-
可以通过LineBasedFrameDecoder(int maxLength)来拆分以换行符(\n)为分隔符的数据,也可以通过DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf… delimiters)来指定通过什么分隔符来拆分数据(可以传入多个分隔符),两种解码器都需要传入数据的最大长度,如果超出指定长度仍未出现分隔符,则会抛出TooLongFrameException异常;
-
以默认\n 或 \r\n 作为分隔符的行解码器代码
-
客户端代码
package com.zhyn.nettyadvance; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; 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; import java.nio.charset.StandardCharsets; import java.util.Random; @Slf4j public class Client0 { public static void main(String[] args) { for (int i = 0; i < 10; i++) { send(); } System.out.println("finish"); } public static void send() { NioEventLoopGroup worker = new NioEventLoopGroup(); try { ChannelFuture channelFuture = new Bootstrap() .channel(NioSocketChannel.class) .group(worker) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { log.debug("connected..."); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { log.debug("sending..."); // 约定最大长度为 64 final int maxLength = 64; // 被发送的数据 char c = 'a'; for (int i = 0; i < 10; i++) { ByteBuf buffer = ctx.alloc().buffer(maxLength); // 生成长度为0~62的数据 Random random = new Random(); StringBuilder sb = new StringBuilder(); for (int j = 0; j < (int)(random.nextInt(maxLength-2)); j++) { sb.append(c); } // 数据以 \n 结尾 sb.append("\n"); buffer.writeBytes(sb.toString().getBytes(StandardCharsets.UTF_8)); c++; // 将数据发送给服务器 ctx.writeAndFlush(buffer); } } }); } }) .connect(new InetSocketAddress(8080)) .sync(); channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { log.error("client error", e); } finally { worker.shutdownGracefully(); } } }
-
服务器代码只需要加入相应的解码器
// 通过行解码器对粘包数据进行拆分,以 \n 为分隔符 // 需要指定最大长度 ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
-
运行结果
16:19:28 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xb25b31d0, L:/192.168.31.158:8080 - R:/192.168.31.158:62233] READ: 6B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 61 61 61 61 61 61 |aaaaaa | +--------+-------------------------------------------------+----------------+ 16:19:28 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xb25b31d0, L:/192.168.31.158:8080 - R:/192.168.31.158:62233] READ: 10B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 62 62 62 62 62 62 62 62 62 62 |bbbbbbbbbb | +--------+-------------------------------------------------+----------------+
-
另一种方式的使用方法与此类似
方法4:字段长度解码器
-
在传送数据时可以在数据中添加一个用于表示有用数据长度的字段,在解码时读取出这个用于表明长度的字段,同时读取其他相关参数,即可知道最终需要的数据是什么样子的;
-
LengthFieldBasedFrameDecoder
解码器可以提供更为丰富的拆分方法,其构造方法有五个参数;public LengthFieldBasedFrameDecoder( int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip)
-
参数解析:
- maxFrameLength 数据最大长度:表示数据的最大长度(包括附加信息、长度标识等内容)
- lengthFieldOffset 数据长度标识的起始偏移量,用于指明数据第几个字节开始是用于标识有用字节长度的,因为前面可能还有其他附加信息;
- lengthFieldLength 数据长度标识所占字节数(用于指明有用数据的长度),数据中用于表示有用数据长度的标识所占的字节数;
- lengthAdjustment 长度表示与有用数据的偏移量,用于指明数据长度标识和有用数据之间的距离,因为两者之间还可能有附加信息;
- initialBytesToStrip 数据读取起点,读取起点,不读取 0 ~ initialBytesToStrip 之间的数据
- 参数图解
lengthFieldOffset = 0 lengthFieldLength = 2 lengthAdjustment = 0 initialBytesToStrip = 0 (= do not strip header) BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes) +--------+----------------+ +--------+----------------+ | Length | Actual Content |----->| Length | Actual Content | | 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" | +--------+----------------+ +--------+----------------+
从0开始即为长度标识,长度标识长度为2个字节,0x000C 即为后面 HELLO, WORLD的长度;
lengthFieldOffset = 2 (= the length of Header 1) lengthFieldLength = 3 lengthAdjustment = 0 initialBytesToStrip = 0 BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes) +----------+----------+----------------+ +----------+----------+----------------+ | Header 1 | Length | Actual Content |----->| Header 1 | Length | Actual Content | | 0xCAFE | 0x00000C | "HELLO, WORLD" | | 0xCAFE | 0x00000C | "HELLO, WORLD" | +----------+----------+----------------+ +----------+----------+----------------+
从0开始即为长度标识,长度标识长度为2个字节,读取时从第二个字节开始读取(此处即跳过长度标识),因为跳过了用于表示长度的2个字节,所以此处直接读取HELLO, WORLD;
lengthFieldOffset = 2 (= the length of Header 1) lengthFieldLength = 3 lengthAdjustment = 0 initialBytesToStrip = 0 BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes) +----------+----------+----------------+ +----------+----------+----------------+ | Header 1 | Length | Actual Content |----->| Header 1 | Length | Actual Content | | 0xCAFE | 0x00000C | "HELLO, WORLD" | | 0xCAFE | 0x00000C | "HELLO, WORLD" | +----------+----------+----------------+ +----------+----------+----------------+
长度标识前面还有2个字节的其他内容(0xCAFE),第三个字节开始才是长度标识,长度表示长度为3个字节(0x00000C),Header1中有附加信息,读取长度标识时需要跳过这些附加信息来获取长度;
lengthFieldOffset = 0 lengthFieldLength = 3 lengthAdjustment = 2 (= the length of Header 1) initialBytesToStrip = 0 BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes) +----------+----------+----------------+ +----------+----------+----------------+ | Length | Header 1 | Actual Content |----->| Length | Header 1 | Actual Content | | 0x00000C | 0xCAFE | "HELLO, WORLD" | | 0x00000C | 0xCAFE | "HELLO, WORLD" | +----------+----------+----------------+ +----------+----------+----------------+
从0开始即为长度标识,长度标识长度为3个字节,长度标识之后还有2个字节的其他内容(0xCAFE),长度标识(0x00000C)表示的是从其后lengthAdjustment(2个字节)开始的数据的长度,即HELLO, WORLD,不包括0xCAFE;
lengthFieldOffset = 1 (= the length of HDR1) lengthFieldLength = 2 lengthAdjustment = 1 (= the length of HDR2) initialBytesToStrip = 3 (= the length of HDR1 + LEN) BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes) +------+--------+------+----------------+ +------+----------------+ | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content | | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" | +------+--------+------+----------------+ +------+----------------+
长度标识前面有1个字节的其他内容,后面也有1个字节的其他内容,读取时从第3个字节处开始读取,即读取 0xFE HELLO, WORLD;
-
使用:通过 EmbeddedChannel 对 handler 进行测试:
package com.zhyn.nettyadvance; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import java.nio.charset.StandardCharsets; public class FrameDecoderStudy { public static void main(String[] args) { // 模拟服务器 // 使用EmbeddedChannel测试handler EmbeddedChannel channel = new EmbeddedChannel( // 数据最大长度为1KB,长度标识前后各有1个字节的附加信息,长度标识长度为4个字节(int) new LengthFieldBasedFrameDecoder(1024, 1, 4, 1, 0), new LoggingHandler(LogLevel.DEBUG) ); // 模拟客户端,写入数据 ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); send(buffer, "Hello"); channel.writeInbound(buffer); send(buffer, "World"); channel.writeInbound(buffer); } private static void send(ByteBuf buf, String msg) { // 得到数据的长度 int length = msg.length(); byte[] bytes = msg.getBytes(StandardCharsets.UTF_8); // 将数据信息写入buf // 写入长度标识前的其他信息 buf.writeByte(0xCA); // 写入数据长度标识 buf.writeInt(length); // 写入长度标识后的其他信息 buf.writeByte(0xFE); // 写入具体的数据 buf.writeBytes(bytes); } }
-
运行结果
16:44:37 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] REGISTERED 16:44:37 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] ACTIVE 16:44:37 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 11B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| ca 00 00 00 05 fe 48 65 6c 6c 6f |......Hello | +--------+-------------------------------------------------+----------------+ 16:44:37 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE 16:44:37 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 11B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| ca 00 00 00 05 fe 57 6f 72 6c 64 |......World | +--------+-------------------------------------------------+----------------+ 16:44:37 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE
2. 协议设计与解析
2.1 为什么需要协议?
TCP/IP 中消息传输基于流的方式,没有边界。
协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则
例如:在网络上传输下雨天留客天留我不留
是中文一句著名的无标点符号句子,在没有标点符号情况下,这句话有数种拆解方式,而意思却是完全不同,所以常被用作讲述标点符号的重要性
一种解读是下雨天留客,天留,我不留
另一种解读是下雨天,留客天,留我不?留
如何设计协议呢?其实就是给网络传输的信息加上“标点符号”。但通过分隔符来断句不是很好,因为分隔符本身如果用于传输,那么必须加以区分。因此,下面一种协议较为常用定长字节表示内容长度 + 实际内容
;
Redis协议:如果我们要向Redis服务器发送一条set name zhangsan的指令,需要遵守如下协议
// 该指令一共有3部分,每条指令之后都要添加回车与换行符
*3\r\n
// 第一个指令的长度是3
$3\r\n
// 第一个指令是set指令
set\r\n
// 下面的指令以此类推
$4\r\n
name\r\n
$5\r\n
zhangsan\r\n
HTTP协议:HTTP协议在请求行请求头中都有很多的内容,自己实现较为困难,可以使用HttpServerCodec作为服务器端的解码器与编码器来处理HTTP请求;
// HttpServerCodec 中既有请求的解码器 HttpRequestDecoder 又有响应的编码器 HttpResponseEncoder
// Codec(CodeCombine) 一般代表该类既作为 编码器 又作为 解码器
public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>
implements HttpServerUpgradeHandler.SourceCodec
服务器代码:
服务器负责处理请求并响应浏览器,所以只需要处理HTTP请求即可
// 服务器只处理HTTPRequest
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>()
获得请求后,需要返回响应给浏览器,所以要创建响应对象 DefaultFullHttpResponse 并设置HTTP版本号及状态码,为避免浏览器获得响应后因获得 CONTENT_LENGTH 而一直空转,需要添加 CONTENT_LENGTH 字段,表明响应体中数据的具体长度;以下为核心代码:
// 获得完整响应,设置版本号与状态码
DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
// 设置响应内容
byte[] bytes = "<h1>Hello, World!</h1>".getBytes(StandardCharsets.UTF_8);
// 设置响应体长度,避免浏览器一直接收响应内容
response.headers().setInt(CONTENT_LENGTH, bytes.length);
// 设置响应体
response.content().writeBytes(bytes);
以下为完整代码:
package com.zhyn.nettyadvance;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
@Slf4j
public class HttpServer {
public static void main(String[] args) {
NioEventLoopGroup group = new NioEventLoopGroup();
new ServerBootstrap()
.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
// 作为服务器,使用 HttpServerCodec 作为编码器与解码器
ch.pipeline().addLast(new HttpServerCodec());
// 服务器只处理HTTPRequest
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) {
// 获得请求uri
log.debug(msg.uri());
// 获得完整响应,设置版本号与状态码
DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
// 设置响应内容
byte[] bytes = "<h1>Hello, World!</h1>".getBytes(StandardCharsets.UTF_8);
// 设置响应体长度,避免浏览器一直接收响应内容
response.headers().setInt(CONTENT_LENGTH, bytes.length);
// 设置响应体
response.content().writeBytes(bytes);
// 写回响应
ctx.writeAndFlush(response);
}
});
}
})
.bind(8080);
}
}
运行结果:
// 请求内容
17:09:35 [DEBUG] [nioEventLoopGroup-2-2] i.n.h.l.LoggingHandler - [id: 0x1f8f511a, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:63043] READ: 630B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a |GET / HTTP/1.1..|
|00000010| 48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 3a |Host: localhost:|
|00000020| 38 30 38 30 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e |8080..Connection|
|00000030| 3a 20 6b 65 65 70 2d 61 6c 69 76 65 0d 0a 73 65 |: keep-alive..se|
......
// 响应内容
17:09:35 [DEBUG] [nioEventLoopGroup-2-2] i.n.h.l.LoggingHandler - [id: 0x1f8f511a, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:63043] WRITE: 61B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d |HTTP/1.1 200 OK.|
|00000010| 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3a |.content-length:|
|00000020| 20 32 32 0d 0a 0d 0a 3c 68 31 3e 48 65 6c 6c 6f | 22....<h1>Hello|
|00000030| 2c 20 57 6f 72 6c 64 21 3c 2f 68 31 3e |, World!</h1> |
+--------+-------------------------------------------------+----------------+
......
2.2 自定义协议
自定义协议的组成要素
- 魔数:用来在第一时间判定接收的数据是否为无效数据包
- 版本号:可以支持协议的升级
- 序列化算法:消息正文到底采用哪种序列化反序列化方式,如:JSON、PROTOBUF、HESSIAN、JDK
- 指令类型:是登录、注册、单聊、群聊… 跟业务相关
- 请求序号:为了双工通信,提供异步能力
- 正文长度
- 消息正文
编码器与解码器
package cn.itcast.protocol;
import cn.itcast.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
@Slf4j
@ChannelHandler.Sharable
/**
* ByteToMessageCodec的作用是将ByteBuffer与我们的自定义的类型进行转换
* 通过泛型来指定我们的消息类型
*/
public class MessageCodec extends ByteToMessageCodec<Message> {
@Override
// 将msg写入到ByteBuf之中
public void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
// 1. 4 字节的魔数
out.writeBytes(new byte[]{1, 2, 3, 4});
// 2. 1 字节的版本
out.writeByte(1);
// 3. 1 字节的序列化方式 JDK 0 , JSON 1
out.writeByte(0);
// 4. 1 字节的指令类型(区分是聊天消息还是登录消息)
out.writeByte(msg.getMessageType());
// 5. 4 个字节的指令请求序号
out.writeInt(msg.getSequenceId());
// 无意义,对齐填充
out.writeByte(0xff);
// 6. 获取内容的字节数组,通过序列化的方式将对象转化为数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(msg);
byte[] bytes = bos.toByteArray();
// 7. 长度
out.writeInt(bytes.length);
// 以上的长度一共是16字节
// 8. 写入内容
out.writeBytes(bytes);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int magicNum = in.readInt();
byte version = in.readByte();
byte serializerType = in.readByte();
byte messageType = in.readByte();
int sequenceId = in.readInt();
// 读取对齐填充的无意义数据
in.readByte();
int length = in.readInt();
byte[] bytes = new byte[length];
// Transfers this buffer's data to the specified destination starting at the current readerIndex and increases
// the readerIndex by the number of the transferred bytes (= dst.length).
// 就是将当前缓冲区中的数据传输到bytes
in.readBytes(bytes, 0, length);
// 先用ByteArrayInputStream包装bytes,然后用ois读取
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
// 反序列化之后就是message
Message message = (Message) ois.readObject();
log.debug("{}, {}, {}, {}, {}, {}", magicNum, version, serializerType, messageType, sequenceId, length);
log.debug("{}", message);
// 将解码出来的结果存到一个参数之中以确保接下来的handler能够继续拿到解码出的结果
out.add(message);
}
}
-
编码器与解码器方法源于父类ByteToMessageCodec,通过该类可以自定义编码器与解码器,泛型类型为被编码与被解码的类。此处使用了自定义类Message,代表消息;
public class MessageCodec extends ByteToMessageCodec<Message>
-
编码器负责将附加信息与正文信息写入到ByteBuf中,其中附加信息总字节数最好为2n,不足需要补齐。正文内容如果为对象,需要通过序列化将其转换为数组然后放入到ByteBuf中;
-
解码器负责将ByteBuf中的信息取出,并放入List中,该List用于将信息传递给下一个handler;
编写测试类
package com.zhyn.netty;
import cn.itcast.message.LoginRequestMessage;
import cn.itcast.protocol.MessageCodec;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TestCodec {
public static void main(String[] args) throws Exception {
EmbeddedChannel channel = new EmbeddedChannel();
// 添加解码器,避免粘包半包问题
channel.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0));
channel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
channel.pipeline().addLast(new MessageCodec());
LoginRequestMessage user = new LoginRequestMessage("ZhangSan", "123");
// 测试编码与解码
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null, user, byteBuf);
channel.writeInbound(byteBuf);
}
}
-
测试类中用到了LengthFieldBasedFrameDecoder,避免粘包半包问题;
-
通过MessageCodec的encode方法将附加信息与正文写入到ByteBuf中,通过channel执行入站操作。入站时会调用decode方法进行解码;
-
运行结果:其中正文数据为十六进制C6,也就是十进制的198,加上16字节的附加信息,刚好就是214个字节;
21:19:58 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 214B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 02 03 04 01 00 00 00 00 00 00 ff 00 00 00 c6 |................| |00000010| ac ed 00 05 73 72 00 25 63 6e 2e 69 74 63 61 73 |....sr.%cn.itcas| |00000020| 74 2e 6d 65 73 73 61 67 65 2e 4c 6f 67 69 6e 52 |t.message.LoginR| |00000030| 65 71 75 65 73 74 4d 65 73 73 61 67 65 a0 3f 71 |equestMessage.?q| |00000040| cb 31 45 b5 88 02 00 02 4c 00 08 70 61 73 73 77 |.1E.....L..passw| |00000050| 6f 72 64 74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 |ordt..Ljava/lang| |00000060| 2f 53 74 72 69 6e 67 3b 4c 00 08 75 73 65 72 6e |/String;L..usern| |00000070| 61 6d 65 71 00 7e 00 01 78 72 00 19 63 6e 2e 69 |ameq.~..xr..cn.i| |00000080| 74 63 61 73 74 2e 6d 65 73 73 61 67 65 2e 4d 65 |tcast.message.Me| |00000090| 73 73 61 67 65 3d dd 19 a0 bc 07 47 cb 02 00 02 |ssage=.....G....| |000000a0| 49 00 0b 6d 65 73 73 61 67 65 54 79 70 65 49 00 |I..messageTypeI.| |000000b0| 0a 73 65 71 75 65 6e 63 65 49 64 78 70 00 00 00 |.sequenceIdxp...| |000000c0| 00 00 00 00 00 74 00 03 31 32 33 74 00 08 5a 68 |.....t..123t..Zh| |000000d0| 61 6e 67 53 61 6e |angSan | +--------+-------------------------------------------------+----------------+
@Sharable注解
为了提高handler的复用率,可以将handler创建为handler对象,然后在不同的channel中使用该handler对象进行处理操作
LoggingHandler loggingHandler = new LoggingHandler(LogLevel.DEBUG);
// 不同的channel中使用同一个handler对象,提高复用率
channel1.pipeline().addLast(loggingHandler);
channel2.pipeline().addLast(loggingHandler);
但是并不是所有的handler都能通过这种方法来提高复用率的,例如LengthFieldBasedFrameDecoder
,如果多个channel中使用同一个LengthFieldBasedFrameDecoder对象,则可能发生如下问题:
-
channel1中收到了一个半包,LengthFieldBasedFrameDecoder发现不是一条完整的数据,则没有继续向下传播;
-
此时channel2中也收到了一个半包,恰巧的是这个半包不是channel1中缺少的那一部分,因为两个channel使用了同一个LengthFieldBasedFrameDecoder,存入其中的数据刚好拼凑成了一个完整的数据包。LengthFieldBasedFrameDecoder让该数据包继续向下传播,最终引发错误;
-
为了提高handler的复用率,同时又避免出现一些并发问题,Netty中原生的handler中用@Sharable注解来标明该handler能否在多个channel中共享,只有带有该注解才能通过对象的方式被共享,否则无法被共享;
-
自定义编解码器能否使用@Sharable注解需要根据自定义的handler的处理逻辑进行分析,我们的MessageCodec本身接收的是LengthFieldBasedFrameDecoder处理之后的数据,那么数据肯定是完整的,按分析来说是可以添加@Sharable注解的,但是实际情况我们并不能添加该注解,会抛出异常信息ChannelHandler cn.nyimac.study.day8.protocol.MessageCodec is not allowed to be shared,因为MessageCodec继承自ByteToMessageCodec,ByteToMessageCodec类的注释如下:
-
这就意味着ByteToMessageCodec不能被多个channel所共享的,原因:因为该类的目标是:将ByteBuf转化为Message,意味着传进该handler的数据还未被处理过。所以传过来的ByteBuf可能并不是完整的数据,如果共享则会出现问题;
-
如果想要共享继承MessageToMessageDecoder即可。该类的目标是:将已经被处理的完整数据再次被处理。传过来的Message如果是被处理过的完整数据,那么被共享也就不会出现问题了,也就可以使用@Sharable注解了。实现方式与ByteToMessageCodec类似;
@ChannelHandler.Sharable public class MessageSharableCodec extends MessageToMessageCodec<ByteBuf, Message> { @Override protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> out) throws Exception { ... } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception { ... } }