TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题
图解说明TCP粘包/拆包:
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下 4 种情况
特别要注意的是,如果TCP的接收方的滑动窗口非常小,而数据包D1和D2比较大,很有可能会发生第五种情况,即服务端分多次才能将D1和D2包完全接受,期间发生多次拆包
具体的三个原因:
由于 TCP 协议本身的机制客户端与服务器会维持一个连接,数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用 Nagle 算法(可配置是否启用)对较小的数据包进行合并然后再发送。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次再取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象
Nagle算法的目的:TCP/IP协议中,无论发送多少数据,总是要在数据(DATA)前面加上协议头(TCP Header+IP Header),即使从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的包,其中包括1字节的消息内容和40字节的头部。这样的情况对于高负载的网络来说是无法接受的。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP希望每次都能够以MSS尺寸的数据块来发送数据)。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块
Nagle算法的规则:
MTU是以太网传输数据方面的限制,每个以太网帧都有最小的大小64bytes最大不能超过1518bytes。刨去以太网帧的帧头: (DMAC目的MAC地址:6Bytes+SMAC源MAC地址:6Bytes+Type域:2bytes) 14Bytes和帧尾 CRC校验部分4Bytes,那么剩下承载上层协议的数据域最大就只能有1500Bytes,我们就把它称之为MTU
如果IP层有一个数据包要传,而且数据的长度比链路层的MTU大,那么IP层就会进行分片,把数据包分成若干片,让每一片都不超过MTU。注意,IP分片可以发生在原始发送端主机上,也可以发生在中间路由器上
MSS是TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个的TCP报文段。所以MSS并不是TCP报文段的最大长度。并且MSS是根据MTU计算出来的,因此当发送的数据满足MSS时,必然也满足MTU。而上述我们知道MTU限制了一次最多可以发送1500个字节,而TCP协议在发送DATA时,还会加上额外的TCP Header和Ip Header,因此刨去这两个部分,就是TCP协议一次可以发送的实际应用数据的最大大小,也就是MSS即 MSS长度=MTU长度-IP Header-TCP Header
TCP Header的长度是20字节,IPv4中IP Header长度是20字节,IPV6中IP Header长度是40字节,因此:在IPV4中,TCP的MSS可以达到1460byte;在IPV6中,TCP的MSS可以达到1440byte
发送方发送数据时,当SO_SNDBUF(发送缓冲区)中的数据量大于MSS时,操作系统会将数据进行拆分,使得每一部分都小于MSS,这就是拆包
前面我们知道了TCP是一个没有界限数据流协议,并且协议本身无法避免粘包/拆包的发生,因此我们只能在应用层协议上,进行相应的控制。通常在制定数据传输时,可以使用如下方法:
对于使消息定长的粘包和拆包场景,可以使用FixedLengthFrameDecoder,该解码器会每次读取固定长度的消息,如果当前读取到的消息不足指定长度,那么就会等待下一个消息到达后进行补足。其使用也比较简单,只需要在构造函数中指定每个消息的长度即可。
这里需要注意的是,FixedLengthFrameDecoder只是一个解码一器,Netty也只提供了一个解码器,这是因为对于解码是需要等待下一个包的进行补全的,代码相对复杂,而对于编码器,用户可以自行编写,因为编码时只需要将不足指定长度的部分进行补全即可
对于通过分隔符进行粘包和拆包问题的处理,Netty提供了两个编解码的类,LineBasedFrameDecoder和DelimiterBasedFrameDecoder。这里LineBasedFrameDecoder的作用主要是通过换行符,即\n或者\r\n对数据进行处理;而DelimiterBasedFrameDecoder的作用则是通过用户指定的分隔符对数据进行粘包和拆包处理。同样的,这两个类都是解码器类,而对于数据的编码,也即在每个数据包最后添加换行符或者指定分割符的部分需要用户自行进行处理
Netty提供了LengthFieldBasedFrameDecoder自定义长度解码器,用来解析带长度字段数据包
数据包大小: 14B = 长度域2B + “HELLO! NETTY”(12B)
数据包大小: 14B = 长度域2B + “HELLO! NETTY”(12B),解码后会丢掉长度域
数据包大小: 16B = 长度域2B + “CAFE”(2B)+“HELLO! NETTY”(12B),在长度域后添加2个字节的Header
数据包大小: 16B = “CAFE”(2B)+长度域2B + “HELLO! NETTY”(12B),在长度域前添加2个字节的Header,解码后会丢掉长度域
数据包大小: 17B = “CA”(1B)+长度域3B +“FE”(1B)+ “HELLO! NETTY”(12B),在长度域前后各添加1个字节的Header,解码后会丢掉长度域
引入依赖:
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>msgpack</artifactId>
<version>0.6.12</version>
</dependency>
首先,定义发送的消息实体
//MessagePack提供的注解,表明这是一个需要序列化的实体类
@Message
public class LogEvent {
private String msg;
private long time;
private long msgId;
@Override
public String toString() {
return "LogEvent [msg=" + msg + ", time=" + time + ", msgId=" + msgId + "]";
}
....
}
定义服务端,服务端首先基于LengthFieldBasedFrameDecoder自定义长度解码器,对客户端发送的报文进行解码,然后在将解码后的字节交由MessagePack进行反序列化成目标实例,最后再由业务Handler来处理
public class MessagePackServer {
private static final int PORT=8761;
private static EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
private static ServerBootstrap bootstrap = new ServerBootstrap();
public static void startServer(){
try {
bootstrap.group(eventLoopGroup).
channel(NioServerSocketChannel.class).
localAddress(new InetSocketAddress(PORT)).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//解决粘包/拆包问题,添加自定义长度解码器
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2));
//添加自定义messagepack反序列化器
ch.pipeline().addLast(new MessagePackDecoder());
//添加业务handler
ch.pipeline().addLast(new MessagePackServerHandler());
}
});
ChannelFuture future=bootstrap.bind().sync();
System.out.println("服务器启动完成,等待客户端的连接和数据.....");
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}finally{
eventLoopGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
startServer();
}
}
定义MessagePack反序列化操作,将字节转化为LogEvent实例
public class MessagePackDecoder extends MessageToMessageDecoder<ByteBuf>{
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
int readLength=msg.readableBytes();
byte[] bytes=new byte[readLength];
//将内容写入到字节数组
msg.getBytes(msg.readerIndex(),bytes,0,readLength);
//创建MessagePack序列化器
MessagePack messagePack = new MessagePack();
//将字节数组反序列化成目标实例
out.add(messagePack.read(bytes, LogEvent.class));
}
}
服务端的业务handler,负责接收数据,并向客户端回复应答
public class MessagePackServerHandler extends ChannelInboundHandlerAdapter{
private AtomicInteger counter = new AtomicInteger(0);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//此时的msg已经被反序列化为目标实例
LogEvent logEvent = (LogEvent) msg;
System.out.println("Server Accept:"+logEvent.toString()+" and the counter is:"+counter.incrementAndGet());
//应答,应答结尾加上换行分割符
String response = "message "+logEvent.getMsgId()+" received successfully"+System.getProperty("line.separator");
ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
}
}
定义客户端,客户端首先基于LengthFieldPrepender设置长度域,然后将目标实例交由MessagePack进行序列化成字节数据,最后再由业务Handler来处理
public class MessagePackClient {
private static final int PORT=8761;
private static final String HOST = "127.0.0.1";
private static EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
private static Bootstrap bootstrap = new Bootstrap();
public static void startClient(){
try {
bootstrap.group(eventLoopGroup).
channel(NioSocketChannel.class).
remoteAddress(new InetSocketAddress(HOST, PORT)).handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//添加自定义长度的编码器
ch.pipeline().addLast(new LengthFieldPrepender(2));
//添加MessagePack序列化器
ch.pipeline().addLast(new MessagePackEncoder());
//添加应答信息, 按换行符进行分割编码器
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
//添加业务handler
ch.pipeline().addLast(new MessagePackClientHandler());
}
});
ChannelFuture future=bootstrap.connect().sync();
System.out.println("客户端启动,并连接到服务端.....");
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}finally{
eventLoopGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
startClient();
}
}
public class MessagePackEncoder extends MessageToByteEncoder<LogEvent>{
@Override
protected void encode(ChannelHandlerContext ctx, LogEvent msg, ByteBuf out) throws Exception {
MessagePack messagePack = new MessagePack();
out.writeBytes(messagePack.write(msg));
}
}
public class MessagePackClientHandler extends SimpleChannelInboundHandler<ByteBuf>{
private AtomicInteger counter = new AtomicInteger(0);
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println("client Accept["+msg.toString(CharsetUtil.UTF_8)
+"] and the counter is:"+counter.incrementAndGet());
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
LogEvent logEvent =new LogEvent();
for(int i=1;i<=10;i++){
logEvent.setTime(System.currentTimeMillis());
logEvent.setMsg("Send request :" + i);
logEvent.setMsgId(i);
System.out.println("Send logEvent:"+logEvent.toString());
ctx.writeAndFlush(logEvent);
}
}
}
测试
首先,启动服务端
接下来,启动客户端,可以看到客户端发出了请求报文,并接收到了服务端的应答报文
同时服务端也成功收到了客户端的请求报文
如果服务端不使用自定义长度的解码器,那么服务端就不会接收到完整的报文