netty4+protobuf3最佳實踐

本文要點:

  1. netty4+protobuf3多類型傳輸實現
  2. 優雅的實現消息分發

做後臺服務經常有這樣的流程:

2eeae9b5-4a34-4171-b0e8-4f695c29d2d9.png


如何優雅的完成這個過程呢?下面分享下基於netty+protobuf的方案:

 

首先要解決的是如何在netty+protobuf中傳輸多個protobuf協議,這裏採取的方案是使用一個類來做爲描述協議的方案,也就是需要二次解碼的方案,IDL文件如下:

syntax = "proto3";
option java_package = "com.nonpool.proto";
option java_multiple_files = true;

message Frame {
   string messageName = 1;
   
   bytes payload = 15;
}

message TextMessage {
   string  text = 1;
}

Frame爲描述協議,所有消息在發送的時候都序列化成byte數組寫入Frame的payload,messageName約定爲要發送的message的類名,生成的時候設置java_multiple_files = true可以讓類分開生成,更清晰些,也更方便後面利用反射來獲取這些類.

生成好了protobuf,我們解包的過程就應該是這樣的:

 

71d1665d-db17-48bf-a7b2-86f4c2faf652.png

 

其中protobuf的序列化和反序列化netty已經爲我們編寫好了對應的解碼/編碼器,直接調用即可,我們只需要編寫二次解碼/編碼器即可:

public class SecondProtobufCodec extends MessageToMessageCodec<Frame, MessageLite> {

    @Override
    protected void encode(ChannelHandlerContext ctx, MessageLite msg, List<Object> out) throws Exception {
    
        out.add(Frame.newBuilder()
                .setMessageType(msg.getClass().getSimpleName())
                .setPayload(msg.toByteString())
                .build());
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, Frame msg, List<Object> out) throws Exception {
        out.add(ParseFromUtil.parse(msg));
    }

}
public abstract class ParseFromUtil {

    private final static ConcurrentMap<String, Method> methodCache = new ConcurrentHashMap<>();

    static {
        //找到指定包下所有protobuf實體類
        List<Class> classes = ClassUtil.getAllClassBySubClass(MessageLite.class, true, "com.nonpool.proto");
        classes.stream()
                .filter(protoClass -> !Objects.equals(protoClass, Frame.class))
                .forEach(protoClass -> {
                    try {
                        //反射獲取parseFrom方法並緩存到map
                        methodCache.put(protoClass.getSimpleName(), protoClass.getMethod("parseFrom", ByteString.class));
                    } catch (NoSuchMethodException e) {
                        throw new RuntimeException(e);
                    }
                });
    }
    
    /**
     * 根據Frame類解析出其中的body
     *
     * @param msg
     * @return
     */
    public static MessageLite parse(Frame msg) throws InvocationTargetException, IllegalAccessException {
        String type = msg.getMessageType();
        ByteString body = msg.getPayload();

        Method method = methodCache.get(type);
        if (method == null) {
            throw new RuntimeException("unknown Message type :" + type);
        }
        return (MessageLite) method.invoke(null, body);
    }
}

至此,我們收發數據的解碼/編碼已經做完配合自帶的解碼/編碼器,此時pipeline的處理鏈是這樣的:

public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline()
                                    .addLast(new ProtobufVarint32FrameDecoder())
                                    .addLast(new ProtobufDecoder(Frame.getDefaultInstance()))
                                    .addLast(new ProtobufVarint32LengthFieldPrepender())
                                    .addLast(new ProtobufEncoder())
                                    .addLast(new SecondProtobufCodec())
                            ;

數據收發完成,接下來就是把消息分發到對應的處理方法。處理方法也利用多態特性+泛型+註解優雅的實現分發。首先定義一個泛型接口:interface DataHandler<T extends MessageLite>,該接口上定義一個方法void handler(T t, ChannelHandlerContext ctx),然後每一個類型的處理類都使用自己要處理的類型實現該接口,並使用自定義註解映射處理類型。在項目啓動的掃描實現該接口的所有類,使用跟上述解析Message類型相同的方法來緩存這些處理類(有一點不同的是這裏只需要緩存一個處理類的實例而不是方法,因爲parseFromstatic的無法統一調用),做完這些我們就可以編寫我們的pipeline上的最後一個處理器:

public class DispatchHandler extends ChannelInboundHandlerAdapter {

    @Override
    @SuppressWarnings("unchecked")
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        HandlerUtil.getHandlerInstance(msg.getClass().getSimpleName()).handler((MessageLite) msg,ctx);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
    }
}
public abstract class HandlerUtil {

    private final static ConcurrentMap<String,DataHandler> instanceCache = new ConcurrentHashMap<>();

    static {
        try {
            List<Class> classes = ClassUtil.getAllClassBySubClass(DataHandler.class, true,"com.onescorpion");
            for (Class claz : classes) {
                HandlerMapping annotation = (HandlerMapping) claz.getAnnotation(HandlerMapping.class);
                instanceCache.put(annotation.value(), (DataHandler) claz.newInstance());
            }
            System.out.println("handler init success handler Map: " +  instanceCache);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static DataHandler getHandlerInstance(String name) {
        return instanceCache.get(name);
    }
}

這樣一個優雅的處理分發就完成了。
由於代碼規律性極強,所以所有handler類均可以使用模版來生成,完整代碼請看這裏

ps:其實本例中由於使用了protobuf還有比較強約定性,所以理論上來說每個消息處理器上的自定義註解是不需要的,通過獲取泛型的真實類型即可,但是註解可以大大增加handler的靈活性,如果採用其他方案(例如json)也更有借鑑的價值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章