這一期介紹如何解析二進制私有協議。
先說幾句題外話,就是絕大多數情況下,可能根本用不着使用私有二進制協議,除非你的業務對性能極其敏感,否則HTTP足矣。
協議
我們的協議非常簡單,先是一個4字節的整數表示數據長度,然後緊接着就是protobuf序列化後的字節數組。proto定義如下:
syntax = "proto2";
package cn.fh.vertx.demo.proto;
option java_multiple_files = true;
message Message {
required int32 type = 1;
required string content = 2;
}
在實際業務場景中,消息格式的定義是有講究的,這裏不深究,重點在於vert.x解析。
RecordParser
Vert.x中只有一個二進制協議解析輔助類,即RecordParser
,可以很好的解決粘包/拆包問題。它有兩種工作模式,一是delimited mode
, 即通過固定分隔符分隔數據包,這種用的其實比較少;二是fixed size mode
,即固定數據長度模式。詐一看可能很多人會有疑慮,多數協議都是可變長度的啊,這兩種模式看起來都不能滿足要求。其實模式二支持在處理的過程中隨時動態的改變size
值,這樣就可以間接完成對變長協議的解析。
首先,RecordParser
有兩類Builder
方法,RecordParser.newDelimited()
和RecordParser.newFixed(4)
,分別對應上面的模式一和模式二。構造完成以後,跟其它vert.x的方法一樣,我們需要定義一個Handler, RecordParser
每分割出一段字節數組都會調用一次Handler,業務邏輯處理就在此Handler中進行。對於上面"長度 + 數據"的協議,我們可以首先構造一個以4字節爲單位的RecordParser
,在收到長度數據後,再動態將解析器修改成指定長度狀態,從而完成對數據部分的分割。代碼示例如下:
// 先以長度4構造對象
RecordParser parser = RecordParser.newFixed(4);
// 設置處理器
parser.setOutput(new Handler<Buffer>() {
// 表示當前數據長度
int size = -1;
@Override
public void handle(Buffer buffer) {
// -1表示當前還沒有長度信息,需要從收到的數據中取出長度
if (-1 == size) {
// 取出長度
size = buffer.getInt(0);
// 動態修改長度
parser.fixedSizeMode(size);
} else {
// 如果size != -1, 說明已經接受到長度信息了,接下來的數據就是protobuf可識別的字節數組
byte[] buf = buffer.getBytes();
Message msg = null;
try {
msg = Message.parseFrom(buf);
} catch (InvalidProtocolBufferException e) {
System.out.println(e.getMessage());
socket.close();
return;
}
System.out.println(msg);
// 處理完後要將長度改回4
parser.fixedSizeMode(4);
// 重置size變量
size = -1;
}
}
});
雖然用起來比較彆扭,但這的確是標準使用方法。
如果表示長度的數據不是從0開始的,比如0 ~ 3爲消息類型,4 ~ 7才表示body的長度,那麼可以在構造解析器時先將長度設爲8, 然後取後4字節做爲新的長度即可。
其實這個類所做的事正是Netty裏LengthFieldBasedFrameDecoder
的功能,希望Vert.x以後能直接提供好類似的處理器,不要讓用戶再手動取長度了,畢竟低層的Netty都支持,你更高級的封裝怎麼可以沒有呢。
這個RecordParser
怎麼用呢?看一下完整的代碼吧:
Vertx vertx = Vertx.vertx();
// 創建TCP Server
NetServer server = vertx.createNetServer();
// 設置Handler
server.connectHandler(socket -> {
// 構造parser
RecordParser parser = RecordParser.newFixed(4);
parser.setOutput(new Handler<Buffer>() {
int size = -1;
@Override
public void handle(Buffer buffer) {
if (-1 == size) {
size = buffer.getInt(0);
parser.fixedSizeMode(size);
} else {
byte[] buf = buffer.getBytes();
Message msg = null;
try {
msg = Message.parseFrom(buf);
} catch (InvalidProtocolBufferException e) {
System.out.println(e.getMessage());
socket.close();
return;
}
parser.fixedSizeMode(4);
size = -1;
}
}
});
socket.handler(parser);
});
// 監聽
server.listen(8008, "localhost", res -> {
if (res.succeeded()) {
System.out.println("tcp server is listening at 8008");
} else {
System.out.println(res.cause());
}
});
寫起來會有一點點奇怪,適應就好。