服务端直接打印即可:
@Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { LOGGER.info("收到msg={}", msg); }
顺便提一下,这里加的有一个字符串的解码器:.addLast(new StringDecoder())
其实就是把消息解析为字符串。
@Override protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception { out.add(msg.toString(charset)); }
在 Swagger 中调用了客户端的接口用于给服务端发送了 100 次消息:
正常情况下接收端应该打印 100 次 hello
才对,但是查看日志会发现:
收到的内容有完整的、多的、少的、拼接的;这也就对应了上面提到的拆包、粘包。
该怎么解决呢?这便可采用之前提到的 LineBasedFrameDecoder
利用换行符解决。
利用 LineBasedFrameDecoder 解决问题
LineBasedFrameDecoder
解码器使用非常简单,只需要在 pipline 链条上添加即可。
//字符串解析,换行防拆包 .addLast(new LineBasedFrameDecoder(1024)) .addLast(new StringDecoder())
构造函数中传入了 1024 是指报的长度最大不超过这个值,具体可以看下文的源码分析。
然后我们再进行一次测试看看结果:
注意,由于 LineBasedFrameDecoder 解码器是通过换行符来判断的,所以在发送时,一条完整的消息需要加上
\n
。
最终的结果:
仔细观察日志,发现确实没有一条被拆、粘包。
LineBasedFrameDecoder 的原理
目的达到了,来看看它的实现原理:
- 第一步主要就是
findEndOfLine
方法去找到当前报文中是否存在分隔符,存在就会返回分隔符所在的位置。
- 判断是否需要丢弃,默认为 false ,第一次走这个逻辑(下文会判断是否需要改为 true)。
- 如果报文中存在换行符,就会将数据截取到那个位置。
- 如果不存在换行符(有可能是拆包、粘包),就看当前报文的长度是否大于预设的长度。大于则需要缓存这个报文长度,并将 discarding 设为 true。
- 如果是需要丢弃时,判断是否找到了换行符,存在则需要丢弃掉之前记录的长度然后截取数据。
- 如果没有找到换行符,则将之前缓存的报文长度进行累加,用于下次抛弃。
从这个逻辑中可以看出就是寻找报文中是否包含换行符,并进行相应的截取。
由于是通过缓冲区读取的,所以即使这次没有换行符的数据,只要下一次的报文存在换行符,上一轮的数据也不会丢。
高效的编码方式 Google Protocol
上面提到的其实就是在解码中进行操作,我们也可以自定义自己的拆、粘包工具。
编解码的主要目的就是为了可以编码成字节流用于在网络中传输、持久化存储。
Java 中也可以实现 Serializable 接口来实现序列化,但由于它性能等原因在一些 RPC 调用中用的很少。
而 Google Protocol
则是一个高效的序列化框架,下面来演示在 Netty 中如何使用。