Netty解决TCP沾包拆包

  对于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:如果使用1个Byte字节代表消息长度,
    则最大长度需要小于256个字节。对长度进行校验,如果校验失败,则抛出参数非法异常;
    若校验通过,则创建新的ByteBuf并通过writeByte将长度值写入到ByteBuf中;

  2. 长度字段所占字节为2:如果使用2个Byte字节代表消息长度,
    则最大长度需要小于65536个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;
    若校验通过,则创建新的ByteBuf并通过writeShort将长度值写入到ByteBuf中;

  3. 长度字段所占字节为3:如果使用3个Byte字节代表消息长度,
    则最大长度需要小于16777216个字节,对长度进行校验,如果校验失败,
    则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeMedium
    将长度值写入到ByteBuf中;

  4. 长度字段所占字节为4:创建新的ByteBuf,并通过writeInt将长度值写入到ByteBuf中;

  5. 长度字段所占字节为8:创建新的ByteBuf,并通过writeLong将长度值写入到ByteBuf中;

  6. 其它长度值:直接抛出Error。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值