【Netty4.X】TCP粘包/拆包问题的解决办法(二)

 最近在读李林峰写的<<Netty权威指南(第2版)>>,所以本系列有部分内容是参考书籍来写的,感谢作者提供的学习资料。

----------------------------------------------------------------------华丽的分割线----------------------------------------------------------------------------

上一篇: 【Netty4.X】Unity客户端与Netty服务器的网络通信(一)

 什么是TCP粘包/拆包


如图所示,假如客户端分别发送两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4中情况

1. 第一种情况:Server端分别读取到D1和D2,没有产生粘包和拆包的情况。

2. 第二种情况:Server端一次接收到两个数据包,D1和D2粘合在一起,被称为TCP粘包。

3. 第三种情况:Server端分2次读取到2个数据包,第一次读取到D1包和D2包的部分内容D2_1,第二次读取到D2包的剩余内容,被称为TCP拆包。

4. 第四中情况:Server端分2次读取到2个数据包,第一次读取到D1包的部分内容D1_1 ,第二次读取到D1包的剩余内容D1_2和D2包的整包。


通过代码看TCP粘包

  修改服务器端

   修改上一篇【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);
	}
	


 修改客户端

  修改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粘包。


客户端运行结果如下

【Netty4.X】TCP粘包/拆包问题的解决办法(二)_第1张图片

按照设计初衷,客户端应该收到100条AD ORDER消息,但实际上只收到了一条。

粘包问题的解决办法

粘包的解决办法有很多,可以归纳如下。

1.消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格。

2.在包尾增加回车换行符进行分割,例如FTP协议。

3.将消息分为消息头和消息体,消息头中包含消息长度的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度


在本案例中,我使用的是第2个解决办法在包尾增加回车换行符进行分割。

服务器端代码修改

第一步新建一个类ServerChannelHandler继承于ChannelInitializer<SocketChannel>,代码如下

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());
	}
}

重点代码在26,27行,在原来的ServerHandler之前新增了两个解码器:LineBasedFrameDecoder和StringDecoder。

第二部修改原来的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;


客户端代码修改

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


客户端运行结果

【Netty4.X】TCP粘包/拆包问题的解决办法(二)_第2张图片

客户端收到服务器端消息结果只有3次,和预期的不一样。

你可能感兴趣的:(TCP粘包和拆包问题,TCP粘包问题解决办法,StringDecoder)