一、TCP 粘包/拆包介绍
1、什么是粘包、拆包
首先只有TCP数据传输才会存在是粘包、拆包现象。
假设客户端分别发送两个数据包D1和D2给服务器,由于TCP是面向流的协议,TCP把客户端传过来的数据看成是一连串的无结构的字节流,且服务端一次读取到的数据是不确定的,所以可能会出现下面几种情况。
(1)服务端分两次接收到D1和D2数据包,没有发生粘包/拆包。
(2)服务端一次接收了两个数据包,D1和D2粘在一起,发生TCP粘包。
如:客户端第一次发送"Hello,Netty",第二次发送"Time out"数据包,发生粘包后,数据就会合并一起"Hello,NettyTime out"发送至服务端。
(3)服务端分两次读取,第一次读取完整D1包+不完整D2包;第二次读取D2包剩下内容。
如:服务端第一次接收到"Hello,NettyTi",第二次接收"me out",第一次即发生TCP拆包。
(4)同样,服务端也可能第一次读取不完整D1包;第二次读取到剩下的D1包和D2包。
2、粘包/拆包产生的原因
首先我们需要知道UDP协议中不存在粘包、拆包现象**,因为UDP是面向报文的**,应用层发送给UDP多长报文,UDP在加上UDP首部后就原封不动发送出去;接收方UDP对接收到的数据报文去除首部后再原装不动地交付给上层的应用进程。即UDP一次交付一个完整的报文,所以不存在粘包、拆包现象。
TCP数据传输是以流的形式进行传输。
一个完整的数据包会被拆分成多个小包进行发送;
也可能多个小包被封装成一个大的数据包进行发送。
这就涉及到了TCP的粘包/拆包问题。
TCP传输产生粘包/拆包问题的原因:
- 应用程序写入的字节大小大于套接口发送缓冲区大小(即一次发送的内容过多,导致产生拆包);
- 进行MSS大小的TCP分端;
- 以太网诊的payload大小大于MTU进行IP分片。
3、解决办法
- 消息定长:如可以设定每个报文的长度为200个字节,如果不够,空位补空格;
- 在包尾增加回车换行符进行分割,如FTP协议;
- 将消息分为消息头和消息体,消息头中至少标识消息总长度,这样接收端在接收到数据后就可以知道每一个数据包实际长度(Dubbo采用该方案进行编解码);
- 更复杂的应用协议;
二、代码演示TCP 粘包/拆包现象
下面用代码演示什么是示TCP 粘包/拆包。
1、服务端
服务端的作用就是读取客户端发送的指令,每读取一次计数器就+1;根据指令做出响应,并将响应发送给客户端。
代码:
package com.wgs.netty.demo2_packet;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.util.Date;
/**
* Created by wanggenshen
* Date: on 2019/7/12 23:41.
* Description: TCP粘包演示服务端
*/
public class TcpPacketServer {
public static void bind(int port) throws InterruptedException {
EventLoopGroup parentGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(parentGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new SimpleChannelInboundHandler() {
// 计数器
private int counter;
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
// 读取内容
ByteBuf byteBuf = (ByteBuf) msg;
byte[] req = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(req);
// 接收客户端消息后换行
String bodyFromClient = new String(req, "UTF-8")
.substring(0, req.length - System.getProperty("line.separator").length());
System.out.println("【Receive from client, msg is 】: " + System.getProperty("line.separator")
+ bodyFromClient
+ ", the counter is:" + ++counter);
// 收到客户端指令后, 将响应返回给客户端
String currentTime = "";
if ("QUERY TIME ORDER".equalsIgnoreCase(bodyFromClient)) {
currentTime = new Date(System.currentTimeMillis()).toString(