1.什么是TCP拆包与粘包
TCP是基于流的协议,也就是说,是一段没有界限的数据。tcp底层并不了解上层应用系统的逻辑,他会根据tcp缓冲区进行包的划分。因此,在应用层面,可能会出现一个完成的数据包被TCP拆分为多个包发送,也有可能会出现多个包在一起发送。这就是所谓的TCP拆包粘包问题
2.TCP拆包与粘包发生的原因
- 应用程序写入的字节大于或小于套接口缓冲区的大小
- 进行MSS大小的TCP分段
-
3.TCP拆包与粘包的解决思路
因为拆包粘包问题发生在TCP的底层协议,底层协议是无法知道上层业务逻辑的包的具体重组和拆分,因此只能通过上层协议来解决这个问题。大致包括以下几种思路
消息定长,发送方固定每个报文长度,长度不足进行空格补位。接收方累计读取到定长的报文后,将其视作一个完整的包,然后重启计数器,继续读取下一个数据包
- 在包尾增加分隔符进行拆分。例如回车换行分隔符等等。发送方在包体尾部添加分隔符来划分数据包,接收方通过获取数据流中分隔符的位置来进行数据包的拆分读取。 FTP协议利用的就是换行分隔符。
- 将消息分为消息头和消息体,消息头中包含消息总长度(或者消息体的长度)。发送方在包体消息头中写入消息体总长度,接收方读取消息头第一个字符获取到总长度,再累计读取对应长度将其视作一个完整的数据包。通常设计为消息头第一个字段使用int32来表示消息总长度。
- 更复杂的应用层协议
4.编写不支持拆包粘包的服务端
```java package demo1;
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;
/**
- @author 冯铁城 [17615007230@163.com]
- @date 2022-05-09 19:11:57
@describe: 时间服务器服务端 */ public class TimeServer {
/**
- 开启socket链接 *
@param port 端口号 */ public void bind(int port) {
//1.创建线程组用于接受服务端链接 NioEventLoopGroup bossGroup = new NioEventLoopGroup();
//2.创建线程组用户socketChannel读写 NioEventLoopGroup workGroup = new NioEventLoopGroup(); try {
//3.创建服务端
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildChannelHandler());
//3.绑定端口,开启同步等待
ChannelFuture sync = server.bind(port).sync();
System.out.println("服务端已启动!");
//4.等待服务端监听端口关闭
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
} }
private class ChildChannelHandler extends ChannelInitializer
{ @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new TimeServerHandler());
} }
private class TimeServerHandler extends ChannelHandlerAdapter {
@Override public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("服务端已与" + ctx.channel().id() + "建立链接");
}
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//1.字符串判定
ByteBuf byteBuf = (ByteBuf) msg;
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
System.out.println("收到" + ctx.channel().id() + "渠道的请求:" + new String(bytes, "utf-8"));
//2.定义返回结果
String responseMessage = "响应信息\r\n";
//3.相应数据
ByteBuf response = Unpooled.copiedBuffer(responseMessage.getBytes());
ctx.write(response);
}
@Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
5.不支持拆包粘包的客户端
```java package demo1;
import io.netty.bootstrap.Bootstrap; 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.NioSocketChannel;
/**
- @author 冯铁城 [17615007230@163.com]
- @date 2022-05-09 21:09:16
@describe: 时间服务器客户端 */ public class TimeClient {
/**
- 创建客户端链接 *
- @param host 主机
@param port 端口号 */ public void connect(String host, int port) {
//1. 创建用于IO读写的线程组 EventLoopGroup group = new NioEventLoopGroup(); try {
//2.创建客户端启动类
Bootstrap bootstrap = new Bootstrap();
//3.启动类初始化
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new TimeClientHandler());
}
});
//4.发起异步链接操作
ChannelFuture sync = bootstrap.connect(host, port).sync();
//5.等待客户端链路链接关闭
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
} }
/**
- @author 冯铁城 [17615007230@163.com]
- @date 2022-05-09 21:01:23
@describe: 时间服务器客户端 */ public class TimeClientHandler extends ChannelHandlerAdapter {
@Override public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 20; i++) {
String requestMessage = "发送消息\r\n";
ByteBuf message = Unpooled.buffer(requestMessage.getBytes().length);
message.writeBytes(requestMessage.getBytes());
ctx.writeAndFlush(message);
}
}
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String response = new String(bytes, "UTF-8");
System.out.println("收到服务端答复:" + response);
}
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("链接出现异常,异常信息:" + cause.getMessage());
ctx.close();
} } } ``` 客户端在与服务端建立TCP链接时,触发channelActive接口,理论上发送给服务端20条消息,会收到20条服务端的响应
6.调试结果
服务端实际上只收到了一条消息
客户端也只收到了一条回复
由此可见出现了粘包问题