对于TCP而言,当底层发送和接收消息时,都需要考虑TCP的沾包和拆包问题,一个完整的数据包可能会被TCP拆成多个包发送,或者将小的数据包封装成大的数据包发送。
那么什么是TCP沾包???
假如客户端分别发送两个数据包D1和D2给服务端,由于服务端每次读取的字节数是不确定的,所以存在以下四种情况:
1.server端分别读取到D1和D2,没有产生沾包和拆包的问题。
2.server端一次接收到两个数据包,D1和D2粘合在一起,产生TCP沾包现象。
3.server端分两次读取两个数据包,第一次读取到D1包和D2部分内容,产生TCP沾包现象。
4,server分两次读取到两个数据包,第一次读取到D1的部分内容,第二次读取D1的剩余内容和D2包,产生TCP沾包现象。
代码示例:
//客户端
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i=0;i<100;i++){
ctx.writeAndFlush("hello server"+i);
}
}
//服务端
try {
//表示要进行数据的读取操作,读取操作后也可直接回应
//对于客户端发来的消息,由于没有指定的数据类型,所以统一由Object接收
ByteBuf byteBuf = (ByteBuf) msg;
//在数据类型转换过程中,可以进行编码指定
String inputData = byteBuf.toString(CharsetUtil.UTF_8);//将字节缓冲区的数据转成字符串
String echoData = "ECHO" + inputData;
System.out.println(echoData);
if ("exit".equalsIgnoreCase(inputData)) {
echoData = "quit";//结束当前交互
}
byte[] data = echoData.getBytes();
ByteBuf echoBuf = Unpooled.buffer(data.length);
echoBuf.writeBytes(echoBuf);
ctx.writeAndFlush(echoBuf);
}finally {
ReferenceCountUtil.release(msg);//释放缓存
}
}
TCP沾包的解决办法:
主要有三种:
1.消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格。
2.在包尾,增加分隔符。
3.将消息分为消息体和消息头,消息头中包含消息长度的字段,通常消息长度字段占四个字节。
第二种方法主要依赖于LineBasedFrameDecoder和StringDecoder两种解码器:
LineBasedFrameDecode:会依次遍历ButyBuf中的可读字节,判断是否有\n或\r,其作为一行结束的标志,它是支持以换行符为结束标志的解码器,支持携带结束符和非结束符的两种解码方式,同时支持配置最大单行长度,此时如果连续读取最大长度后仍未发现换行符,则会抛出异常。
StringDecoder:将接收的对象转化为字符串,然后继续调用后面的handler,LineBasedFrameDecoder和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());
}
}
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();
}
}
}
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();
}
}
第三种主要依赖于LengthFieldBasedFrameDecoder和LengthFieldPrepender分割器:
简单代码示例:
try {
Bootstrap client = new Bootstrap(); // 创建客户端处理程序
client.group(group).channel(NioSocketChannel.class).remoteAddress(Host,port)
.option(ChannelOption.TCP_NODELAY, true) // 允许接收大块的返回数据
.handler(new ChannelInitializer<SocketChannel>() {//接收到消息后定义子处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new LengthFieldBasedFrameDecoder(65535,0,
4,0,4));//解析数据
socketChannel.pipeline().addLast(new JsonDecoder());//反序列化
socketChannel.pipeline().addLast(new LengthFieldPrepender(4));//加上数据长度域,长度占4个字节
socketChannel.pipeline().addLast(new JsonEncoder());//序列化
socketChannel.pipeline().addLast(new EchoClientHandler()); // 追加了处理器生产数据
}
});
LengthFieldBasedFrameDecoder
(1) maxFrameLength - 发送的数据包最大长度;
(2) lengthFieldOffset - 长度域偏移量,指的是长度域位于整个数据包字节数组中的下标;
(3) lengthFieldLength - 长度域的自己的字节数长度。
(4) lengthAdjustment – 长度域的偏移量矫正。 如果长度域的值,
除了包含有效数据域的长度外,还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。
矫正的值为:包长 - 长度域的值 – 长度域偏移 – 长度域长。
(5) initialBytesToStrip – 丢弃的起始字节数。丢弃处于有效数据前面的字节数量。
比如前面有4个节点的长度域,则它的值为4。
第一个参数为1024,表示数据包的最大长度为1024;
第二个参数0,表示长度域的偏移量为0,也就是长度域放在了最前面,处于包的起始位置;
第三个参数为4,表示长度域占用4个字节;
第四个参数为0,表示长度域保存的值,仅仅为有效数据长度,不包含其他域(如长度域)的长度;
第五个参数为4,表示最终的取到的目标数据包,抛弃最前面的4个字节数据,长度域的值被抛弃。
LengthFieldPrepender
-
长度字段所占字节为1:如果使用1个Byte字节代表消息长度,
则最大长度需要小于256个字节。对长度进行校验,如果校验失败,则抛出参数非法异常;
若校验通过,则创建新的ByteBuf并通过writeByte将长度值写入到ByteBuf中; -
长度字段所占字节为2:如果使用2个Byte字节代表消息长度,
则最大长度需要小于65536个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;
若校验通过,则创建新的ByteBuf并通过writeShort将长度值写入到ByteBuf中; -
长度字段所占字节为3:如果使用3个Byte字节代表消息长度,
则最大长度需要小于16777216个字节,对长度进行校验,如果校验失败,
则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeMedium
将长度值写入到ByteBuf中; -
长度字段所占字节为4:创建新的ByteBuf,并通过writeInt将长度值写入到ByteBuf中;
-
长度字段所占字节为8:创建新的ByteBuf,并通过writeLong将长度值写入到ByteBuf中;
-
其它长度值:直接抛出Error。