本文要點:
- netty4+protobuf3多型別傳輸實現
- 優雅的實現訊息分發
做後臺服務經常有這樣的流程:
如何優雅的完成這個過程呢?下面分享下基於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,我們解包的過程就應該是這樣的:
其中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型別相同的方法來快取這些處理類(有一點不同的是這裡只需要快取一個處理類的例項而不是方法,因為parseFrom
是static
的無法統一呼叫),做完這些我們就可以編寫我們的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)也更有借鑑的價值。