1. 程式人生 > >【Netty4.x】Netty TCP粘包/拆包問題的解決辦法(二)

【Netty4.x】Netty TCP粘包/拆包問題的解決辦法(二)

一、什麼是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 程式碼修改(伺服器端)

1新建一個類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());
	}
}

 2修改原來的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();
        }
    }
}

3修改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版)>>

若有錯誤之處,請多多諒解並歡迎批評指正。 本部落格中未標明轉載的文章歸作者小毛驢所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。