Netty4.xNetty TCP粘包/拆包问题的解决办法
Posted 程序员小毛驴
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty4.xNetty TCP粘包/拆包问题的解决办法相关的知识,希望对你有一定的参考价值。
上一篇:【Netty4.X】Unity客户端与Netty服务器的网络通信(一)
[[toc]]
目录
一、什么是TCP粘包/拆包
如图所示,假如客户端分别发送两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4中情况:
- 第一种情况:Server端分别读取到D1和D2,没有产生粘包和拆包的情况。
- 第二种情况:Server端一次接收到两个数据包,D1和D2粘合在一起,被称为TCP粘包。
- 第三种情况:Server端分2次读取到2个数据包,第一次读取到D1包和D2包的部分内容D2_1,第二次读取到D2包的剩余内容,被称为TCP拆包。
- 第四中情况:Server端分2次读取到2个数据包,第一次读取到D1包的部分内容D1_1 ,第二次读取到D1包的剩余内容D1_2和D2包的整包。
二、重现TCP粘包
2.1 代码示例(服务器端)
修改上一篇【Netty4.X】Unity客户端与Netty服务器的网络通信(一)的ServerHandler类代码。在类中申明一个计数常量count,当每读到一条消息后,就count++,然后发送应答消息给客户端,代码如下:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception
ByteBuf buf = (ByteBuf)msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req,"UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
count++;
System.out.println("body"+body+";"+ ++count);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)?
new Date(System.currentTimeMillis()).toString():"BAD ORDER";
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
2.2 代码示例(客户端)
修改HttpClient的send()方法,当客户端与服务器链路建立成功之后,循环发送100条消息类代码。
public void Send()
if(client == null)
start ();
byte[] buffer = Encoding.UTF8.GetBytes("userName:"+userName.text+" password"+password.text);
for(int i = 0;i < 100;i++)
client.Send(buffer);
控制台(服务器): +------------------------------------------------------------------+ 七月 07, 2016 8:09:35 下午 com.game.lll.net.HttpServer main 信息: 服务已启动... ad4ea569进来了 bodyuserName:aaa password:bbb ...此处省略36条 userName:aaa password:b;count:1 userName:aaa password:bbb ...此处省略36条 userName:aaa password;count:2 userName:aaa password:bbb ...此处省略22条 userName:aaa password:bbb;count:3 +------------------------------------------------------------------+
服务端运行结果表明它只接收到三条消息,三条加起来一共是100条(如下图)。我们期待的是收到100条消息,每条消息都会包含一条“count:”.这说明发生了TCP粘包。
2.3 控制台(客户端)
按照设计初衷,客户端应该收到100条AD ORDER消息,但实际上只收到了一条。
2.4 粘包问题的解决办法
粘包的解决办法有很多,可以归纳如下。
- 消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格。
- 在包尾增加回车换行符进行分割,例如FTP协议。
- 将消息分为消息头和消息体,消息头中包含消息长度的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度
在本案例中,我使用的是第2个解决办法在包尾增加回车换行符进行分割。
2.5 代码修改(服务器端)
新建一个类ServerChannelHandler继承于ChannelInitializer,重点代码在26,27行,在原来的ServerHandler之前新增了两个解码器:LineBasedFrameDecoder和StringDecoder。代码如下
package com.game.lll.net;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
public class ServerChannelHandler extends ChannelInitializer<SocketChannel>
public static void main(String[] args) throws Exception
int port = 8844;
if(args!=null&&args.length>0)
try
port = Integer.valueOf(args[0]);
catch (Exception e)
// TODO: handle exception
System.out.println(port);
new HttpServer().bind(port);
@Override
protected void initChannel(SocketChannel ch) throws Exception
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ServerHandler());
修改原来的HttpServer类,修改代码如下:
package com.game.lll.net;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioserverSocketChannel;
public class HttpServer
private static Log log = LogFactory.getLog(HttpServer.class);
public void bind(int port) throws Exception
log.info("服务器已启动");
配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>()
@Override
public void initChannel(SocketChannel ch) throws Exception
ch.pipeline().addLast(new ServerChannelHandler());
).option(ChannelOption.SO_BACKLOG, 128) //最大客户端连接数为128
.childOption(ChannelOption.SO_KEEPALIVE, true);
//绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
//等待服务端监听端口关闭
f.channel().closeFuture().sync();
finally
//优雅退出,释放线程池资源
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
修改ServerHandler类,代码如下:
package com.game.lll.net;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class ServerHandler extends ChannelInboundHandlerAdapter
private static Log log = LogFactory.getLog(ServerHandler.class);
private int count = 0;
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception
super.handlerAdded(ctx);
System.out.println(ctx.channel().id()+"进来了");
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception
super.handlerRemoved(ctx);
System.out.println(ctx.channel().id()+"离开了");
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception
String body = (String)msg;
System.out.println("body"+body+";count:"+ ++count);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)?new Date(System.currentTimeMillis()).toString():"BAD ORDER";
currentTime = currentTime+System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception
ctx.flush();
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception
// TODO Auto-generated method stub
ctx.close();
直接看第33行,修改前后代码比较。修改前:
ByteBuf buf = (ByteBuf)msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req,"UTF-8");
修改后:
String body = (String)msg;
2.6 代码修改(客户端端)
byte[] buffer = Encoding.UTF8.GetBytes("userName:"+userName.text+" password:"+password.text+"\\r\\n");
在每一条消息尾巴后添加“\\r\\n’”
控制台(服务器): +------------------------------------------------------------------+ 8844 七月 07, 2016 8:37:58 下午 com.game.lll.net.HttpServer bind 信息: 服务器已启动 04d575ff进来了 bodyuserName:aaa password:bbb;count:1 此处省略很多条...... bodyuserName:aaa password:bbb;count:99 bodyuserName:aaa password:bbb;count:100 +------------------------------------------------------------------+
本章参考书籍
<<Netty权威指南(第2版)>>
以上是关于Netty4.xNetty TCP粘包/拆包问题的解决办法的主要内容,如果未能解决你的问题,请参考以下文章