持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
背景🍀
滑动窗口
由于TCP滑动窗口的机制,客户端可以无需等待应答就可以持续发送数据,当某个段的ack回应到客户端时,客户端的滑动窗口才会向前移动。该滑动窗口可以看做成一个缓冲区,这个缓冲区中可以存储许多数据;
MSS限制
在链路层中,对一次能够发送的数据大小有所限制,这个限制大小称为MTU,以太网的MTU是1500,如果一次数据的数量大于(1500 - 40[ TCP头和IP头 ]),则会将多出来的数据存储到下一次发送;
服务端接收缓冲区的限制
如果服务端缓冲区过小,会造成不能完整的接收到客户端发送的一条信息,如果过大,可以一次就收到客户端发送的多条消息;
现象🍏
粘包
定义
粘包就是 服务端 一次 接收到了 客户端 多次发送的信息
客户端发送 abc def ghj (分三次发送)
而服务端一次就接收到了 abcdefghj
代码演示
(以下的演示都是基于netty进行)
客户端代码,连续发送10条信息
public class AdhesiveClient {
public static void main(String[] args) {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 客户端连续发送了10条信息
for (int i = 0; i < 10; i++) {
ByteBuf byteBuf = ctx.alloc().buffer();
byteBuf.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
ctx.writeAndFlush(byteBuf);
}
}
});
}
})
.connect(new InetSocketAddress("localhost", 8080));
}
}
服务端代码
public class AdhesiveServer {
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 LoggingHandler(LogLevel.DEBUG));
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
});
}
})
.bind(8080);
}
}
服务端结果,一次读完客户端端发送的十条信息,出现粘包
原因
- 服务端的 bytebuf 的值设置得过大,一次就接收了客户端发送的10条数据;
- 服务端滑动窗口的值可以缓存到客户端发送的10条数据,导致服务端可以一次性接收10条数据;
半包
定义
半包就是客户端发送的一条信息,而服务端却需要多次才能接收
客户端发送 abcdefghj (分一次发送)
服务端接收 abc def ghj (分三次接收)
代码演示
客户端发送3200B的数据
public class AdhesiveClient {
public static void main(String[] args) {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 客户端连续发送了3200B的数据
ByteBuf byteBuf = ctx.alloc().buffer();
for (int i = 0; i < 200; i++) {
byteBuf.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
}
ctx.writeAndFlush(byteBuf);
}
});
}
})
.connect(new InetSocketAddress("localhost", 8080));
}
}
服务端代码
public class AdhesiveServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
// 修改服务端byteBuf的大小
.option(ChannelOption.SO_RCVBUF, 10)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
});
}
})
.bind(8080);
}
}
服务端却分开两次接收,一次接收1024B,一次接收2176B
原因
- 服务端的 bytebuf 的值设置得过小,不足以一次接收客户端发送的所有数据;
- 假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包;
解决方案🔥
- 使用短连接💛
即每次客户端每次发送消息时都要建立一次连接,发送完后断开连接,这样连接建立到断开就是消息的边界;
- 缺点效率太低,同时也不太好处理半包,因为服务端的缓冲区是有限的;
- 固定长度💚
服务端和客户端约定好消息的长度,每一次发送都以固定的长度发送;
- 对于短消息可能会造成空间的浪费;
- 对于长消息就可能显得长度不够;
客户端代码
public static void main(String[] args) {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf byteBuf = ctx.alloc().buffer();
for (int i = 0; i < 5; i++) {
// 固定大小为8
byteBuf.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7});
}
ctx.writeAndFlush(byteBuf);
}
});
}
})
.connect(new InetSocketAddress("localhost", 8080));
}
服务端代码
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(8));
nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
});
}
})
.bind(8080);
}
FixedLengthFrameDecoder解码器的作用
将消息以固定字节数来接收
- 固定分隔符❤️
客户端发送消息的时候加入**\n** 或者 \r 作为消息的边界,服务端只需要根据这些标记进行区分是否是一条完整的消息即可;
客户端代码(每条信息后加入\n)
public static void main(String[] args) {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Random r = new Random();
char c = 'a';
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
for (int j = 1; j <= r.nextInt(16)+1; j++) {
buffer.writeByte((byte) c);
}
// 添加分隔符
buffer.writeByte(10);
c++;
}
ctx.writeAndFlush(buffer);
}
});
}
})
.connect(new InetSocketAddress("localhost", 8080));
}
服务端代码
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 {
// 加入LineBasedFrameDecoder解码器
nioSocketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
});
}
})
.bind(8080);
}
客户端结果
服务端结果(篇幅问题只截取部分)
LineBasedFrameDecoder解码器的作用
LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf中的可读字节,判断看是否有"\n” 或者 "\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以回车换行符为结束标记的解码器,支持配置单行的最大长度,如果连续读取到最大长度后仍然没有发现换行符,会抛出异常,同时忽略掉之前读取到的异常码流;
- 预设长度💙
在发送消息前,先约定用定长字节表示接下来数据的长度,即客户端发送的消息中,可以用消息的前N个字节表示该消息的长度,后 length - N 个字节表示消息内容即可;
客户端代码
public static void main(String[] args) {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Random r = new Random();
char c = 'a';
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
byte len = (byte) (r.nextInt(16) + 1);
// 先写入长度
buffer.writeByte(len);
for (int j = 1; j <= len; j++) {
// 再写入内容
buffer.writeByte((byte) c);
}
c++;
}
ctx.writeAndFlush(buffer);
}
});
}
})
.connect(new InetSocketAddress("localhost", 8080));
}
服务端代码
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 LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));
nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
});
}
})
.bind(8080);
}
客户端结果
服务端结果(篇幅问题只截取部分)
LengthFieldBasedFrameDecoder解码器的作用
它的其中一个构造器中有5个参数,分别表示消息的最大长度,表示长度位置的偏移量,表示长度所占的字节数,内容相对于长度的偏移量,剥离的字节数