Netty(三) 什么是 TCP 拆、粘包?如何解决?

这个网关逻辑非常简单,就是接收客户端的请求然后解析报文最后发送短信。

但这个请求并不是常见的 HTTP ,而是利用 Netty 自定义的协议。

有个前提是:网关是需要读取一段完整的报文才能进行后面的逻辑。

问题是有天突然发现网关解析报文出错,查看了客户端的发送日志也没发现问题,最后通过日志发现收到了许多不完整的报文,有些还多了。

于是想会不会是 TCP 拆、粘包带来的问题,最后利用 Netty 自带的拆包工具解决了该问题。

这便有了此文。

TCP 协议

问题虽然解决了,但还是得想想原因,为啥会这样?打破砂锅问到底才是一个靠谱的程序员。

这就得从 TCP 这个协议说起了。

TCP 是一个面向字节流的协议,它是性质是流式的,所以它并没有分段。就像水流一样,你没法知道什么时候开始,什么时候结束。

所以他会根据当前的套接字缓冲区的情况进行拆包或是粘包。

下图展示了一个 TCP 协议传输的过程:

Netty(三) 什么是 TCP 拆、粘包?如何解决?

发送端的字节流都会先传入缓冲区,再通过网络传入到接收端的缓冲区中,最终由接收端获取。

当我们发送两个完整包到接收端的时候:

Netty(三) 什么是 TCP 拆、粘包?如何解决?

正常情况会接收到两个完整的报文。

但也有以下的情况:

Netty(三) 什么是 TCP 拆、粘包?如何解决?

接收到的是一个报文,它是由发送的两个报文组成的,这样对于应用程序来说就很难处理了(这样称为粘包)。

Netty(三) 什么是 TCP 拆、粘包?如何解决?

还有可能出现上面这样的虽然收到了两个包,但是里面的内容却是互相包含,对于应用来说依然无法解析(拆包)。

对于这样的问题只能通过上层的应用来解决,常见的方式有:

在报文末尾增加换行符表明一条完整的消息,这样在接收端可以根据这个换行符来判断消息是否完整。

将消息分为消息头、消息体。可以在消息头中声明消息的长度,根据这个长度来获取报文(比如 808 协议)。

规定好报文长度,不足的空位补齐,取的时候按照长度截取即可。

以上的这些方式我们在 Netty 的 pipline 中里加入对应的解码器都可以手动实现。

但其实 Netty 已经帮我们做好了,完全可以开箱即用。

比如:

LineBasedFrameDecoder 可以基于换行符解决。

DelimiterBasedFrameDecoder可基于分隔符解决。

FixedLengthFrameDecoder可指定长度解决。

字符串拆、粘包

下面来模拟一下最简单的字符串传输。

还是在之前的

https://github.com/crossoverJie/netty-action

进行演示。

在 Netty 客户端中加了一个入口可以循环发送 100 条字符串报文到接收端:

/** * 向服务端发消息 字符串 * @param stringReqVO * @return */ @ApiOperation("客户端发送消息,字符串") @RequestMapping(value = "sendStringMsg", method = RequestMethod.POST) @ResponseBody public BaseResponse<NULLBody> sendStringMsg(@RequestBody StringReqVO stringReqVO){ BaseResponse<NULLBody> res = new BaseResponse(); for (int i = 0; i < 100; i++) { heartbeatClient.sendStringMsg(stringReqVO.getMsg()) ; } // 利用 actuator 来自增 counterService.increment(Constants.COUNTER_CLIENT_PUSH_COUNT); SendMsgResVO sendMsgResVO = new SendMsgResVO() ; sendMsgResVO.setMsg("OK") ; res.setCode(StatusEnum.SUCCESS.getCode()) ; res.setMessage(StatusEnum.SUCCESS.getMessage()) ; return res ; } /** * 发送消息字符串 * * @param msg */ public void sendStringMsg(String msg) { ByteBuf message = Unpooled.buffer(msg.getBytes().length) ; message.writeBytes(msg.getBytes()) ; ChannelFuture future = channel.writeAndFlush(message); future.addListener((ChannelFutureListener) channelFuture -> LOGGER.info("客户端手动发消息成功={}", msg)); }

服务端直接打印即可:

@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 次消息:

Netty(三) 什么是 TCP 拆、粘包?如何解决?

正常情况下接收端应该打印 100 次 hello 才对,但是查看日志会发现:

Netty(三) 什么是 TCP 拆、粘包?如何解决?

收到的内容有完整的、多的、少的、拼接的;这也就对应了上面提到的拆包、粘包。

该怎么解决呢?这便可采用之前提到的 LineBasedFrameDecoder 利用换行符解决。

利用 LineBasedFrameDecoder 解决问题

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsfsww.html