netty系列之:protobuf在UDP協議中的使用

flydean發表於2022-05-28

簡介

netty中提供的protobuf編碼解碼器可以讓我們直接在netty中傳遞protobuf物件。同時netty也提供了支援UDP協議的channel叫做NioDatagramChannel。如果直接使用NioDatagramChannel,那麼我們可以直接從channel中讀寫UDP物件:DatagramPacket。

但是DatagramPacket中封裝的是ByteBuf物件,如果我們想要向UDP channel中寫入物件,那麼需要一個將物件轉換成為ByteBuf的方法,很明顯netty提供的protobuf編碼解碼器就是一個這樣的方法。

那麼可不可以將NioDatagramChannel和ProtobufDecoder,ProtobufEncoder相結合呢?

NioDatagramChannel中channel讀寫的物件都是DatagramPacket。而ProtobufDecoder與ProtobufEncoder是將protoBuf物件MessageLiteOrBuilder跟ByteBuf進行轉換,所以兩者是不能直接結合使用的。

怎麼才能在UDP中使用protobuf呢?今天要向大家介紹netty專門為UDP建立的編碼解碼器DatagramPacketEncoder和DatagramPacketDecoder。

UDP在netty中的表示

UDP的資料包在netty中是怎麼表示呢?

netty提供了一個類DatagramPacket來表示UDP的資料包。netty中的UDP channel就是使用DatagramPacket來進行資料的傳遞。先看下DatagramPacket的定義:

public class DatagramPacket
        extends DefaultAddressedEnvelope<ByteBuf, InetSocketAddress> implements ByteBufHolder

DatagramPacket繼承自DefaultAddressedEnvelope,並且實現了ByteBufHolder介面。

其中的ByteBuf是資料包中需要傳輸的資料,InetSocketAddress是資料包需要傳送到的地址。

而這個DefaultAddressedEnvelope又是繼承自AddressedEnvelope:

public class DefaultAddressedEnvelope<M, A extends SocketAddress> implements AddressedEnvelope<M, A>

DefaultAddressedEnvelopee中有三個屬性,分別是message,sender和recipient:

    private final M message;
    private final A sender;
    private final A recipient;

這三個屬性分別代表了要傳送的訊息,傳送方的地址和接收方的地址。

DatagramPacketEncoder

DatagramPacketEncoder是一個DatagramPacket的編碼器,所以要編碼的物件就是DatagramPacket。上一節我們也提到了DatagramPacket實際上繼承自AddressedEnvelope。所有的DatagramPacket都是一個AddressedEnvelope物件,所以為了通用起見,DatagramPacketEncoder接受的要編碼的物件是AddressedEnvelope。

我們先來看下DatagramPacketEncoder的定義:

public class DatagramPacketEncoder<M> extends MessageToMessageEncoder<AddressedEnvelope<M, InetSocketAddress>> {

DatagramPacketEncoder是一個MessageToMessageEncoder,它接受一個AddressedEnvelope的泛型,也就是我們要encoder的物件型別。

那麼DatagramPacketEncoder會將AddressedEnvelope編碼成什麼呢?

DatagramPacketEncoder中定義了一個encoder,這個encoder可以在DatagramPacketEncoder初始化的時候傳入:

private final MessageToMessageEncoder<? super M> encoder;

    public DatagramPacketEncoder(MessageToMessageEncoder<? super M> encoder) {
        this.encoder = checkNotNull(encoder, "encoder");
    }

實際上DatagramPacketEncoder中實現的encode方法,底層就是呼叫encoder的encode方法,我們來看下他的實現:

    protected void encode(
            ChannelHandlerContext ctx, AddressedEnvelope<M, InetSocketAddress> msg, List<Object> out) throws Exception {
        assert out.isEmpty();

        encoder.encode(ctx, msg.content(), out);
        if (out.size() != 1) {
            throw new EncoderException(
                    StringUtil.simpleClassName(encoder) + " must produce only one message.");
        }
        Object content = out.get(0);
        if (content instanceof ByteBuf) {
            // Replace the ByteBuf with a DatagramPacket.
            out.set(0, new DatagramPacket((ByteBuf) content, msg.recipient(), msg.sender()));
        } else {
            throw new EncoderException(
                    StringUtil.simpleClassName(encoder) + " must produce only ByteBuf.");
        }
    }

上面的邏輯就是從AddressedEnvelope中呼叫msg.content()方法拿到AddressedEnvelope中的內容,然後呼叫encoder的encode方法將其編碼並寫入到out中。

最後呼叫out的get方法拿出編碼之後的內容,再封裝到DatagramPacket中去。

所以不管encoder最後返回的是什麼物件,最後都會被封裝到DatagramPacket中,並返回。

總結一下,DatagramPacketEncoder傳入一個AddressedEnvelope物件,呼叫encoder將AddressedEnvelope的內容進行編碼,最後封裝成為一個DatagramPacket並返回。

鑑於protoBuf的優異物件序列化能力,我們可以將ProtobufEncoder傳入到DatagramPacketEncoder中,做為真實的encoder:

 ChannelPipeline pipeline = ...;
pipeline.addLast("udpEncoder", new DatagramPacketEncoder(new ProtobufEncoder(...));

這樣就把ProtobufEncoder和DatagramPacketEncoder結合起來了。

DatagramPacketDecoder

DatagramPacketDecoder是和DatagramPacketEncoder相反的操作,它是將接受到的DatagramPacket物件進行解碼,至於解碼成為什麼物件,也是由傳入其中的decoder屬性來決定的:

public class DatagramPacketDecoder extends MessageToMessageDecoder<DatagramPacket> {

    private final MessageToMessageDecoder<ByteBuf> decoder;

    public DatagramPacketDecoder(MessageToMessageDecoder<ByteBuf> decoder) {
        this.decoder = checkNotNull(decoder, "decoder");
    }

DatagramPacketDecoder要解碼的物件是DatagramPacket,而傳入的decoder要解碼的物件是ByteBuf。

所以我們需要一個能夠解碼ByteBuf的decoder實現,而和protoBuf對應的就是ProtobufDecoder。

先來看下DatagramPacketDecoder的decoder方法是怎麼實現的:

    protected void decode(ChannelHandlerContext ctx, DatagramPacket msg, List<Object> out) throws Exception {
        decoder.decode(ctx, msg.content(), out);
    }

可以看到DatagramPacketDecoder的decoder方法很簡單,就是從DatagramPacket中拿到content內容,然後交由decoder去decode。

如果使用ProtobufDecoder作為內建的decoder,則可以將ByteBuf物件decode成為ProtoBuf物件,剛好和之前講過的encode相呼應。

將ProtobufDecoder傳入DatagramPacketDecoder也非常簡單,我們可以這樣做:

 ChannelPipeline pipeline = ...;
   pipeline.addLast("udpDecoder", new DatagramPacketDecoder(new ProtobufDecoder(...));

這樣一個DatagramPacketDecoder就完成了。

總結

可以看到,如果直接使用DatagramPacketEncoder和DatagramPacketDecoder加上ProtoBufEncoder和ProtoBufDecoder,那麼實現的是DatagramPacket和ByteBuf直接的互相轉換。

當然這裡的ProtoBufEncoder和ProtoBufDecoder可以按照使用者的需要被替換成為不同的編碼解碼器。

可以自由組合編碼解碼方式,就是netty編碼器的最大魅力。

本文已收錄於 http://www.flydean.com/17-1-netty-protobuf-udp/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章