1. 程式人生 > >【Netty4 簡單專案實踐】三、壓縮訊息體:使用google的protocol buff

【Netty4 簡單專案實踐】三、壓縮訊息體:使用google的protocol buff

原始碼:https://github.com/arctan90/NettyApplication

這裡能看到

怎麼 配置處理器

怎麼 生成protocol buff的訊息類

怎麼 接收多個型別的protocol buff

怎麼 傳送 protocol buff

ChannelHandlerContext原理

有幾個認知需要明確一下。在每個處理類(比如編解碼)的輸出,如果不滿足下一個類的輸入型別要求,下一個類是根本不會響應。 比如之前看到的:

public class ByteStreamHandler extends SimpleChannelInboundHandler<String>{

} 如果寫成

public class ByteStreamHandler  extends SimpleChannelInboundHandler<byte[]>{

} 那麼StringDecoder的輸出根本不會傳遞到 ByteStreamHandler,奇妙的地方就在SimpleChannelInboundHandler指定的型別。 所以想用Protocol Buff的話,最終的處理器要定義成Protocol Buff型別的處理器

開始Protocol Buff

https://github.com/google/protobuf/releases到這來下載,不要自己去編譯,因為自己編譯要先編譯C++版本,而編譯C++版本要從google下程式碼。。。

開啟之後到google/protobuf目錄下隨便拷貝個.proto檔案出來,咱們改一改

比如把 any.proto 拷貝出來做修改

syntax = "proto3"; //第一行一定要寫成這樣;第一行一定要寫成這樣;第一行一定要寫成這樣
package seeplant.protobuf; //注意包名格式,這裡的包名是本檔案的包名
option java_package = "com.seeplant.protobuf"; //這裡的包名是生成的java檔案所包含的包名
option java_outer_classname = "ProtoMsg";
option java_multiple_files = true; // 會生成多個物件檔案,一個Message一個
option java_generate_equals_and_hash = true;
//然後定義一個Lgin訊息,1234567是佔位符,按順序寫就好了,注意第一個一定是1
message Login {
  uint32 msgType = 1;
  string userType = 2;
  int64 uid = 3;
  string userName = 4;
  string password = 5;
  int64 clsId = 6;
  int64 roomId = 7;
};
//到此為止

生成java物件

在下載的protocol程式包的目錄底下(有readme的地方),執行下面兩行命令,之後你要做的就是把本地的com資料夾複製到eclipse裡面!

rm -rf ./com/
./protoc --java_out=./ any.proto

多個訊息體怎麼辦

第二講說了,在我們的Netty裡面的處理器只能接收一個物件,所以呢,假如你有8個型別的訊息,你是要寫8個處理器掛接到pipline上麼? 這裡有個變通的方法---封裝類。 舉個例子,假如有兩個訊息體(按protocol buff 3協議)
message Login{
    int32 id=1;
};
message Message{
    int64 id=1;
    string msg=2;
};</span>
定義一個包裝類
message Protocol{
Login login=1;
Message msg=2;
};

這樣就好了!用上面的./protoc --java_out=./ any.proto 命令生成對應的.java檔案。(在protocol buff3裡面,所有的域預設都是optional的

那麼如何使用呢?現在,處理器可以實現為

public class MyHandler  extends SimpleChannelInboundHandler<Protocol>{
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Protocol msg)throws Exception {
        if(msg.hasLogin()) { //處理login
            Login login = msg.getLogin();
        } else if (msg.hasMessage()) {}
    }
}

這樣就實現了物件分類。當然一口氣寫倆處理類也不是不行。

事情遠沒有結束--之前的連包問題解決不了

之前string服務裡面靠終止字元來切割訊息的辦法現在不好用了,因為傳的都是二進位制碼流。按照C++程式設計師的習慣,不得不加上兩位元組表示訊息體長度的訊息頭,甚至要加兩位元組的“命令字”表示是哪種訊息(看到這種C++程式設計師直接打死好嗎,完全沒有面向物件的能力)。現在傳送的訊息變成這樣了

|-0x00 0x10(兩位元組0x0010,表示訊息體長度為16)-||-0x00 0x01(兩位元組命令字0x0001)-||-  16個位元組 (16位元組的訊息體)-|

所以不得不增加解碼這種訊息體的解碼器,Netty4有這種解碼器 LengthFieldBasedFrameDecoder。解碼器引數採用

lengthFieldOffset   0  表示長度域開始的位置,這裡從0開始

lengthFieldLength   2  表示長度域的位元組數,這裡是2位元組

lengthAdjustment    2  表示長度域長度需要加2。解碼器從長度域下一個位元組開始,捕獲“長度+2”個位元組作為訊息的剩餘部分位元組

initialBytesToStrip 4  表示訊息體的起始位置,從第四個位元組開始

類似的還有一個編碼器LengthFieldPrepender

lengthFieldLength 2 表示訊息體長度2位元組

lengthAdjustment  -4表示訊息體的真實長度為 編碼後資料長度-4

然後還需要一對兒 Protocol編解碼器

new ProtobufDecoder(Protocol.getDefaultInstance())

new MyProtobufEncoder()

原有的ProtobufEncoder()處理不了中間多出來的兩個位元組的命令字,所以得自己寫一個來剝離命令字

MyProtobufEncoder.java

@Sharable
public class MyProtobufEncoderextends MessageToMessageEncoder<MyMessageLite> {
    @Override
    protected void encode(
            ChannelHandlerContext ctx, MyMessageLite msg, List<Object>out)throws Exception {
        MessageLiteOrBuilder frame = msg.getMessageLit();
        
        if (frame instanceof MessageLite) {
            byte[] command = msg.getCommand();
            byte[] load = ((MessageLite) frame).toByteArray();        
            ByteBufAllocator allocor = ctx.alloc();
            ByteBuf buff = allocor.buffer(command.length + load.length);
            buff.writeBytes(command);
            buff.writeBytes(load);
            
            byte[] dst = new byte[load.length + command.length];
            buff.getBytes(0, dst);
            out.add(buff);
            return;
        }
        if (frame instanceof MessageLite.Builder) {
            out.add(wrappedBuffer(((MessageLite.Builder)frame).build().toByteArray()));
        }
    }
}

訊息封裝類
public class MyMessageLite {
    private MessageLiteOrBuilder messageLit = null;
    byte[] command =new byte[2];
    
    public MyMessageLite(byte[]command, MessageLiteOrBuilder messageLit){
        this.command = command;
        this.messageLit = messageLit;
    }
    // setter getter略
}


改造bootstrap

終於萬事俱備了。

現在來改造適合protocol buff的bootstrap。直接拿String服務的例子改造,TCP連線引數不變。只需要修改掛接的編解碼器和處理器。

    pipeline.addLast("frameEncoder",new LengthFieldPrepender(2, -4,true));

    pipeline.addLast("protoBufEncoder",new MyProtobufEncoder());

    //包頭有2位元組包長,2位元組型別,型別在本地用反射取出來,不依靠傳遞的型別

    pipeline.addLast("frameDecoder",newLengthFieldBasedFrameDecoder(65535, 0, 2, 2, 4));

    pipeline.addLast("protoBufDecoder",newProtobufDecoder(Protocol.getDefaultInstance())); 

    pipeline.addLast(new MyHandler());

至此一個protocol buff的服務完成了。。。。麼?

如何發訊息

事情並沒有結束,我們發訊息也是訊息頭+命令字+protocol buff形式。在向ChannelHandlerContex寫資料的時候也必須封裝兩次才行。

這裡從內到外構造。

先構造一個Message物件: Message msg =Message.newBuilder().setId(1).setMsg("OK").build();

再構造Protocol物件:Protocol p = Protocol.newBuilder().setMessage(msg).build();

再封裝成編碼器可以理解的,上文中的MyMessageLite物件。

最後向ctx寫入:ctx.writeAndFlush(MyMessageLite)

整個流程到此結束