Netty中TCP粘包拆包问题
熟悉tcp编程的可能都知道,无论是服务器还是客户端,当我们读取或发送数据的时候都需要TCP底层的粘包拆包机制。
TCP是一个流协议,所谓流就是没有界限的遗传数据。大家可以想象一下河里的水就好比数据,他们是练成一片的没有分界线,TCP底层并不了解上层的业务数据具体的含义,它会根据TCP缓冲区的实际情况进行包划分,也就是说,在业务上,我们一个完整的包可能被TCP分成多个包进行发送,也可能把多个小包封装成一个大的数据包发送出去,这就是所谓的TCP粘包拆包问题。
分析TCP粘包拆包产生的原因
1. 应用程序write写入的字节大小大于套接口发送缓冲区的大小。
2. 进行MSS大小的TCP分段。
3. 以太网帧的payload大于MTU进行IP分片。
TCP拆包粘包问题的解决方案
- 消息定长,例如每个报文的大小固定为200个字节,如果不够,空位补空格。
- 在包尾部增加特殊字符进行分割,例如加回车。
- 将消息分为消息头和消息体,在消息头中包含表示消息总长度的字段,然后进行业务逻辑的处理。
Netty如何解决粘包拆包的问题
- 分隔符类DelimiterBasedFrameDecoder(自定义分隔符)
Client.java
public class Client {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup workgroup = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(workgroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,buf));
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ClientHandler());
}
});
ChannelFuture cf1 = b.connect("127.0.0.1",8765).sync();
// ChannelFuture cf2 = b.connect("127.0.0.1",8764).sync();
//buf
cf1.channel().writeAndFlush(Unpooled.copiedBuffer("bbbb$_".getBytes()));
cf1.channel().writeAndFlush(Unpooled.copiedBuffer("cccccccc&_".getBytes()));
cf1.channel().writeAndFlush(Unpooled.copiedBuffer("aaaaaaaaaaaaa&_".getBytes()));
// cf1.channel().flush();
cf1.channel().closeFuture().sync();
workgroup.shutdownGracefully();
}
}
ClientHandler.java
public class ClientHandler extends ChannelHandlerAdapter{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client channel active...");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
try{
String response = (String)msg;
System.out.println("Client:"+response);
}finally{
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
Server.java
public class Server {
public static void main(String[] args) throws InterruptedException {
//1 第一个线程组用于接受client端连接的
NioEventLoopGroup pGroup = new NioEventLoopGroup();
//2 第二个线程组用于实际的业务处理操作的
NioEventLoopGroup cGroup = new NioEventLoopGroup();
//3 创建一个辅助类Bootstrap,就是对我们的Server进行一系列的配置
ServerBootstrap b = new ServerBootstrap();
b.group(pGroup,cGroup)//把两个工作线程组加入进来
.channel(NioServerSocketChannel.class)//使用NIOServerSocketChannel这种类型的通道
.childHandler(new ChannelInitializer<SocketChannel>() {//使用childHandler绑定具体的事件处理器
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//设置特殊分隔符,当出现下滑线
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
//设置字符串形式的解码
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ServerHandler());
}
})
/**
* 服务器端TCP内核模块维护两个队列,我们称之为A和B
* 客户端向服务器端connect的时候 会发送带有SYN标志的包(第一次握手)
* 服务器收到客户端发来的SYN时,向客户端发送SYN ACK确认(第二次握手)
* 此时TCP内核模块将客户端连接加入A队列中,然后服务器收到客户端发来的ACK时(第三次握手)
* TCP内核模块将客户端从A队列移动到B队列,连接完成,应用程序的accept会返回。
* 也就是说accept从B队列中取出完成三次握手的连接
* A队列和B队列的长度之和是backlog。当A、B队列的长度之和大于backlog时,新连接将会被TCP内核拒绝
* 所以,如果backlog过小,可能会出现accept速度跟不上,A、B队列满了,导致新的客户无法连接。
* 要注意的是:backlog对程序的连接数并无影响,backlog影响的知识还没有被accept取出的连接。
*/
//设置TCP缓冲区
// .option(ChannelOption.SO_BACKLOG, 128)
//保持连接
// .childOption(ChannelOption.SO_KEEPALIVE, true)
;
//绑定指定的端口进行监听
ChannelFuture f = b.bind(8765).sync();
// ChannelFuture f2 = b.bind(8764).sync();
//将服务器阻塞
f.channel().closeFuture().sync();
// f2.channel().closeFuture().sync();
pGroup.shutdownGracefully();
cGroup.shutdownGracefully();
}
}
ServerHandler.java
public class ServerHandler extends ChannelHandlerAdapter{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("server channel active...");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
String request = (String)msg;
System.out.println("Server:"+msg);
String response = "我是响应数据$_";
ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
ctx.close();
}
}
- FixedLengthFrameDecoder(定长)
Client.java
public class Client {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup workgroup = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(workgroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//
sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ClientHandler());
}
});
ChannelFuture cf1 = b.connect("127.0.0.1",8765).sync();
// ChannelFuture cf2 = b.connect("127.0.0.1",8764).sync();
//buf
cf1.channel().writeAndFlush(Unpooled.copiedBuffer("bbbbbaaaaa".getBytes()));
cf1.channel().writeAndFlush(Unpooled.copiedBuffer("cccccdd ".getBytes()));
// cf1.channel().flush();
cf1.channel().closeFuture().sync();
workgroup.shutdownGracefully();
}
}
ClientHandler.java
public class ClientHandler extends ChannelHandlerAdapter{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client channel active...");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
try{
Response resp = (Response)msg;
System.out.println("Client : " + resp.getId() + ", " + resp.getName() + ", " + resp.getResponseMessage());
}finally{
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
Server.java
public class Server {
public static void main(String[] args) throws InterruptedException {
//1 第一个线程组用于接受client端连接的
NioEventLoopGroup pGroup = new NioEventLoopGroup();
//2 第二个线程组用于实际的业务处理操作的
NioEventLoopGroup cGroup = new NioEventLoopGroup();
//3 创建一个辅助类Bootstrap,就是对我们的Server进行一系列的配置
ServerBootstrap b = new ServerBootstrap();
b.group(pGroup,cGroup)//把两个工作线程组加入进来
.channel(NioServerSocketChannel.class)//使用NIOServerSocketChannel这种类型的通道
.childHandler(new ChannelInitializer<SocketChannel>() {//使用childHandler绑定具体的事件处理器
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//设置字长字符串接收
sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
//设置字符串形式的解码
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ServerHandler());
}
})
/**
* 服务器端TCP内核模块维护两个队列,我们称之为A和B
* 客户端向服务器端connect的时候 会发送带有SYN标志的包(第一次握手)
* 服务器收到客户端发来的SYN时,向客户端发送SYN ACK确认(第二次握手)
* 此时TCP内核模块将客户端连接加入A队列中,然后服务器收到客户端发来的ACK时(第三次握手)
* TCP内核模块将客户端从A队列移动到B队列,连接完成,应用程序的accept会返回。
* 也就是说accept从B队列中取出完成三次握手的连接
* A队列和B队列的长度之和是backlog。当A、B队列的长度之和大于backlog时,新连接将会被TCP内核拒绝
* 所以,如果backlog过小,可能会出现accept速度跟不上,A、B队列满了,导致新的客户无法连接。
* 要注意的是:backlog对程序的连接数并无影响,backlog影响的知识还没有被accept取出的连接。
*/
//设置TCP缓冲区
// .option(ChannelOption.SO_BACKLOG, 128)
//保持连接
// .childOption(ChannelOption.SO_KEEPALIVE, true)
;
//绑定指定的端口进行监听
ChannelFuture f = b.bind(8765).sync();
// ChannelFuture f2 = b.bind(8764).sync();
//将服务器阻塞
f.channel().closeFuture().sync();
// f2.channel().closeFuture().sync();
pGroup.shutdownGracefully();
cGroup.shutdownGracefully();
}
}
ServerHandler.java
public class ServerHandler extends ChannelHandlerAdapter{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("server channel active...");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
String request = (String)msg;
System.out.println("Server:"+msg);
String response = request;
ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
ctx.close();
}
}