netty 解決粘包 和 分包的問題

chenshiying007發表於2018-06-20

netty 解決粘包 和 分包的問題

更多幹貨

概述

netty和tcp協議的關係

netty → Java Runtime Socket (io、nio、nio2) → OS Socket → TCP (當然也可以是UDP、SCTP);

作系統層的Socket都必須做三次握手(僅對TCP而言),Netty當然無法跳過,只不過它對使用者遮蔽了三次握手(當然還有四次揮手)的部分細節。

粘包現象

客戶端在一個for迴圈內連續傳送1000個hello給Netty伺服器端

 		Socket socket = new Socket("127.0.0.1", 10101);
        for(int i = 0; i < 1000; i++){
           socket.getOutputStream().write(“hello”.getBytes());
        }
        socket.close();

而在伺服器端接受到的資訊並不是預期的1000個獨立的Hello字串.

實際上是無序的hello字串混合在一起, 如圖所示. 這種現象我們稱之為粘包.

ctoedu
  1. 為什麼會出現這種現象呢? TCP是個”流”協議,流其實就是沒有界限的一串資料。

  2. TCP底層中並不瞭解上層業務資料的具體含義,它會根據TCP緩衝區的實際情況進行包劃分,

  3. 所以在TCP中就有可能一個完整地包會被TCP拆分成多個包,也有可能吧多個小的包封裝成一個大的資料包傳送。

分包處理

顧名思義, 我們要對傳輸的資料進行分包. 一個簡單的處理邏輯是在傳送資料包之前, 先用四個位元組佔位, 表示資料包的長度.

資料包結構為:

		Socket socket = new Socket("127.0.0.1", 10101);

		String message = "hello";

		byte[] bytes = message.getBytes();

		ByteBuffer buffer = ByteBuffer.allocate(4 + bytes.length);
		buffer.putInt(bytes.length);
		buffer.put(bytes);

		byte[] array = buffer.array();

		for(int i=0; i<1000; i++){
			socket.getOutputStream().write(array);
		}

		socket.close();

伺服器端程式碼, 我們需要藉助於FrameDecoder類來分包.

		if(buffer.readableBytes() > 4){

			if(buffer.readableBytes() > 2048){
				buffer.skipBytes(buffer.readableBytes());
			}


			//標記
			buffer.markReaderIndex();
			//長度
			int length = buffer.readInt();

			if(buffer.readableBytes() < length){
				buffer.resetReaderIndex();
				//快取當前剩餘的buffer資料,等待剩下資料包到來
				return null;
			}

			//讀資料
			byte[] bytes = new byte[length];
			buffer.readBytes(bytes);
			//往下傳遞物件
			return new String(bytes);
		}
		//快取當前剩餘的buffer資料,等待剩下資料包到來
		return null;
  1. 這邊可能有個疑問, 為什麼MyDecoder中資料沒有讀取完畢, 需要return null,

  2. 正常的pipeline在資料處理完都是要sendUpstream, 給下一個pipeline的.

  3. 這個需要看下FrameDecoder.messageReceived 的原始碼. 他在其中快取了一個cumulation物件,

  4. 如果return了null, 他會繼續往快取裡寫資料來實現分包

     public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
          Object m = e.getMessage();
          if (!(m instanceof ChannelBuffer)) {
            // 資料讀完了, 轉下一個pipeline
              ctx.sendUpstream(e);
          } else {
              ChannelBuffer input = (ChannelBuffer)m;
              if (input.readable()) {
                  if (this.cumulation == null) {
                     try {
                         this.callDecode(ctx, e.getChannel(), input, e.getRemoteAddress());
                     } finally {
                         this.updateCumulation(ctx, input);
                     }
                 } else {
                  // 快取上一次沒讀完整的資料
                     input = this.appendToCumulation(input);

                     try {
                         this.callDecode(ctx, e.getChannel(), input, e.getRemoteAddress());
                     } finally {
                         this.updateCumulation(ctx, input);
                     }
                 }

             }
         }
     }

Socket位元組流攻擊

在上述程式碼中, 我們會在伺服器端為客戶端傳送的資料包長度, 預先分配byte陣列.

如果遇到惡意攻擊, 傳入的資料長度與內容 不匹配. 例如宣告資料長度為Integer.MAX_VALUE.

這樣會消耗大量的伺服器資源生成byte[], 顯然是不合理的.

因此我們還要加個最大長度限制.

			if(buffer.readableBytes() > 2048){
             buffer.skipBytes(buffer.readableBytes());
           }

新的麻煩也隨之而來, 雖然可以跳過指定長度, 但是資料包本身就亂掉了.

因為長度和內容不匹配, 跳過一個長度後, 不知道下一段資料的開頭在哪裡了.

因此我們自定義資料包裡面, 不僅要引入資料包長度, 還要引入一個包頭來劃分各個包的範圍.

包頭用任意一段特殊字元標記即可, 例如$$$.

// 防止socket位元組流攻擊
          if(buffer.readableBytes() > 2048){
            buffer.skipBytes(buffer.readableBytes());
          }
          // 記錄包頭開始的index
          int beginReader = buffer.readerIndex();

          while(true) {
              if(buffer.readInt() == ConstantValue.FLAG) {
                 break;
             }
         }

新的資料包結構為:

|    包頭(4位元組)    |    長度(4位元組)    |    資料    |

Netty自帶拆包類

自己實現拆包雖然可以細粒度控制, 但是也會有些不方便, 可以直接呼叫Netty提供的一些內建拆包類.

  1. FixedLengthFrameDecoder 按照特定長度組包

  2. DelimiterBasedFrameDecoder 按照指定分隔符組包, 例如本文中的$$$

  3. LineBasedFrameDecoder 按照換行符進行組包, \r \n等等

程式碼

Server

package com.server;

import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;

public class Server {

	public static void main(String[] args) {
		//服務類
		ServerBootstrap bootstrap = new ServerBootstrap();

		//boss執行緒監聽埠,worker執行緒負責資料讀寫
		ExecutorService boss = Executors.newCachedThreadPool();
		ExecutorService worker = Executors.newCachedThreadPool();

		//設定niosocket工廠
		bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));

		//設定管道的工廠
		bootstrap.setPipelineFactory(new ChannelPipelineFactory() {

			@Override
			public ChannelPipeline getPipeline() throws Exception {

				ChannelPipeline pipeline = Channels.pipeline();
				pipeline.addLast("decoder", new MyDecoder());
				pipeline.addLast("handler1", new HelloHandler());
				return pipeline;
			}
		});

		bootstrap.bind(new InetSocketAddress(10101));

		System.out.println("start!!!");
	}

}

Client

package com.server;

import java.net.Socket;
import java.nio.ByteBuffer;

public class Client {

	public static void main(String[] args) throws Exception {
		Socket socket = new Socket("127.0.0.1", 10101);

		String message = "hello";

		byte[] bytes = message.getBytes();

		ByteBuffer buffer = ByteBuffer.allocate(4 + bytes.length);
		buffer.putInt(bytes.length);
		buffer.put(bytes);

		byte[] array = buffer.array();

		for(int i=0; i<1000; i++){
			socket.getOutputStream().write(array);
		}

		socket.close();
	}

}

HelloHandler

package com.server;

import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

public class HelloHandler extends SimpleChannelHandler {

	private int count = 1;

	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {

		System.out.println(e.getMessage() + "  " +count);
		count++;
	}
}

MyDecoder

package com.server;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;

public class MyDecoder extends FrameDecoder {

	@Override
	protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception {

		if(buffer.readableBytes() > 4){

			if(buffer.readableBytes() > 2048){
				buffer.skipBytes(buffer.readableBytes());
			}


			//標記
			buffer.markReaderIndex();
			//長度
			int length = buffer.readInt();

			if(buffer.readableBytes() < length){
				buffer.resetReaderIndex();
				//快取當前剩餘的buffer資料,等待剩下資料包到來
				return null;
			}

			//讀資料
			byte[] bytes = new byte[length];
			buffer.readBytes(bytes);
			//往下傳遞物件
			return new String(bytes);
		}
		//快取當前剩餘的buffer資料,等待剩下資料包到來
		return null;
	}

}

相關文章