# (一)什么是粘包、半包 {#一-什么是粘包、半包}
在实际的网络开发中或者在面试中,最开始使用TCP协议时经常会碰上粘包和半包的情况,因此我们有必要了解一下什么是粘包,什么是半包,以及如何去解决。
粘包:故名思意就是客户端和服务端之间发送的数据包粘在了一起,原本应该分多条发送的数据包粘在了一起发送。
半包:指的是一条数据包被分割成了多条发送。
粘包和半包发生的根本原因是在TCP协议中,只有流的概念,没有包的概念,消息发送到缓冲区之后是没有边界的说法的,因此就会发生粘包和半包。
# (二)粘包半包效果演示 {#二-粘包半包效果演示}
之前写了netty的入门案例,接下来就通过这个入门案例来展示粘包和半包的效果:
首先是粘包:服务端代码如下,没有其他特殊的地方:
public class FirstServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringDecoder());
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
}
});
}
})
.bind(8080);
}}
客户端代码在发送数据时通过for循环发送十次hello:
public class FirstClient {
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress("localhost", 8080))
.sync()
.channel();
for (int i = 0; i < 10; i++) {
channel.writeAndFlush("hello");
}
}}
结果发生了粘包,原本应该发送十次的数据一次性全部发过来了
接着是半包,在服务器端增加一行代码:
public class FirstServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
//修改缓冲区大小,设置的小一些
.option(ChannelOption.SO_RCVBUF,4)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringDecoder());
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
}
});
}
})
.bind(8080);
}}
然后再次重新运行:既发生了粘包,又发生了半包。
# (三)粘包半包原因分析 {#三-粘包半包原因分析}
看了效果之后,我们来分析一下为什么会发生粘包半包。
TCP协议中,每发送一次数据就需要进行一次ack确认,但是这样就意味着数据的发送将是串行的。于是TCP协议中引入了一个叫做滑动窗口的概念。
滑动窗口其实就是一个缓冲区,在滑动窗口内发送的数据,无需接收响应,可以继续发送。当第一个数据ack确认之后,滑动窗口就会向下移动一个单位。大致的流程图就像下面这样:
当接收方的滑动窗口设置足够大,并且接收方处理不及时的情况下,发送方发过来的数据就会在接收方的滑动窗口中缓冲多个报文,最终导致粘包。
当接收方的滑动窗口设置小于实际发送量,就只能先处理一部分数据,等ack确认后再处理后续的,就导致了半包的情况。
除了TCP层之外,Nagle算法也会造成粘包,网卡的MSS限制也会造成半包。
# (四)粘包、半包解决方案 {#四-粘包、半包解决方案}
提供两种解决粘包半包的思路:
1、指定消息的长度,在发送端和接收端都指定长度
2、在数据中插入分隔符
下面是netty中的粘包解决方案:
# 4.1 指定消息的长度(定长解码器) {#_4-1-指定消息的长度-定长解码器}
netty提供了定长解码器,可以指定消息的长度,适用于那些每次发送字符长度都一致的场景。修改服务器端,设置定长解码器的长度为5:
public class FirstServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new FixedLengthFrameDecoder(5));
nioSocketChannel.pipeline().addLast(new StringDecoder());
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
}
});
}
})
.bind(8080);
}}
客户端每次发送长度为5的数据:
public class FirstClient {
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
//将发送的内容encode编码
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress("localhost", 8080))
.sync()
.channel();
for (int i = 0; i < 10; i++) {
channel.writeAndFlush("hello");
}
}}
观察最后的结果:
这种方法的缺点在于,对于长度不确定的数据无法做到拆分。
# 4.2 指定分隔符(分隔符解码器) {#_4-2-指定分隔符-分隔符解码器}
netty中提供了分隔符解码器,可以获取到回车的分隔符"\n"或"\r\n",直接对上面的代码进行修改:
服务器端//nioSocketChannel.pipeline().addLast(new FixedLengthFrameDecoder(5));nioSocketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));客户端//channel.writeAndFlush("hello");channel.writeAndFlush("hello"+"\n");
# 4.3 LTC解码器 {#_4-3-ltc解码器}
最后介绍一个最实用的解码器:LengthFieldBasedFrameDecoder,LTC编码器弥补了定长编码器只能限定长度的缺点,实用更加灵活。
LTC解码器中有四个重要的参数:
1、lengthFieldOffset:长度字段偏移量
2、lengthFieldLength:长度字段长度
3、lengthAdjustment:长度字段后,多少个字节之后是内容
4、initialBytesToStrip:跳过多少个字节
我们可以通过源码中的这个例子来理解四个参数:
1、首先是长度偏移量,可以看到解码前的字节中长度之前还有一些字节HDR1,长度是1,因此lengthFieldOffset就等于1
2、长度字段的长度是2,因此lengthFieldLength等于2
3、长度字段后,经过1个字节之后是内容,因此lengthAdjustment等于1
4、initialBytesToStrip设置为3,因此解码之后输出的就是HDR2+ActualContent
下面给出在代码中的实现方式:首先是服务端的代码,增加了LengthFieldBasedFrameDecoder解码器
public class FirstServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
//nioSocketChannel.pipeline().addLast(new FixedLengthFrameDecoder(5));
nioSocketChannel.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024,0,4,0,4));
nioSocketChannel.pipeline().addLast(new StringDecoder());
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg.toString());
}
});
}
})
.bind(8080);
}}
客户端的代码,在客户端写数据时增加数据的长度:
public class FirstClient {
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new LengthFieldPrepender(4,false));
}
})
.connect(new InetSocketAddress("localhost", 8080))
.sync()
.channel();
String[] contentList=new String[]{"hello","helloWorld","hello, world"};
for (int i = 0; i <3;i++) {
String content=contentList[i];
byte[] data = content.getBytes();
ByteBuf buf= Unpooled.buffer();
buf.writeBytes(data,0,data.length);
channel.writeAndFlush(buf);
}
}}
# (五)总结 {#五-总结}
到这里关于粘包和半包以及netty解决粘包问题到这里就告一段落了,网络编程想要学好这条路才刚开始!