上一篇介紹了粘包和半包及其通用的解決方案,今天重點來看一下 Netty 是如何實現封裝成幀(Framing)方案的。 git
以前介紹過三種解碼器FixedLengthFrameDecoder
、DelimiterBasedFrameDecoder
、LengthFieldBasedFrameDecoder
,它們都繼承自ByteToMessageDecoder
,而ByteToMessageDecoder
繼承自ChannelInboundHandlerAdapter
,其核心方法爲channelRead
。所以,咱們來看看ByteToMessageDecoder
的channelRead
方法:github
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
CodecOutputList out = CodecOutputList.newInstance();
try {
// 將傳入的消息轉化爲data
ByteBuf data = (ByteBuf) msg;
// 最終實現的目標是將數據所有放進cumulation中
first = cumulation == null;
// 第一筆數據直接放入
if (first) {
cumulation = data;
} else {
// 不是第一筆數據就進行追加
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
}
// 解碼
callDecode(ctx, cumulation, out);
}
// 如下代碼省略,由於不屬於解碼過程
}複製代碼
再來看看callDecode
方法:ide
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
int outSize = out.size();
if (outSize > 0) {
// 如下代碼省略,由於初始狀態時,outSize 只多是0,不可能進入這裏
}
int oldInputLength = in.readableBytes();
// 在進行 decode 時,不執行handler的remove操做。
// 只有當 decode 執行完以後,開始清理數據。
decodeRemovalReentryProtection(ctx, in, out);
// 省略如下代碼,由於後面的內容也不是解碼的過程複製代碼
再來看看decodeRemovalReentryProtection
方法:spa
final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
// 設置當前狀態爲正在解碼
decodeState = STATE_CALLING_CHILD_DECODE;
try {
// 解碼
decode(ctx, in, out);
} finally {
// 執行hander的remove操做
boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
decodeState = STATE_INIT;
if (removePending) {
handlerRemoved(ctx);
}
}
}
// 子類都重寫了該方法,每種實現都會有本身特殊的解碼方式
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;複製代碼
從上面的過程能夠總結出,在解碼以前,須要先將數據寫入cumulation
,當解碼結束後,須要經過 handler 進行移除。code
剛剛說到decode
方法在子類中都有實現,那針對咱們說的三種解碼方式,一一看其實現。cdn
其源碼爲:繼承
@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
Object decoded = decode(ctx, in);
if (decoded != null) {
out.add(decoded);
}
}
protected Object decode(
@SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
// 收集到的數據是否小於固定長度,小於就表明沒法解析
if (in.readableBytes() < frameLength) {
return null;
} else {
return in.readRetainedSlice(frameLength);
}
}複製代碼
就和這個類的名字同樣簡單,就是固定長度進行解碼,所以,在設置該解碼器的時候,須要在構造方式裏傳入frameLength
。ip
其源碼爲:rem
@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
Object decoded = decode(ctx, in);
if (decoded != null) {
out.add(decoded);
}
}
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
// 當前的分割符是不是換行分割符(\n或者\r\n)
if (lineBasedDecoder != null) {
return lineBasedDecoder.decode(ctx, buffer);
}
// Try all delimiters and choose the delimiter which yields the shortest frame.
int minFrameLength = Integer.MAX_VALUE;
ByteBuf minDelim = null;
// 其餘分割符進行一次切分
for (ByteBuf delim: delimiters) {
int frameLength = indexOf(buffer, delim);
if (frameLength >= 0 && frameLength < minFrameLength) {
minFrameLength = frameLength;
minDelim = delim;
}
}
// 如下代碼省略複製代碼
根據它的名字能夠知道,分隔符纔是它的核心。它將分割符分紅兩類,只有換行分割符(n或者rn)
和其餘
。所以,須要注意的是,你能夠定義多種分割符,它都是支持的。get
該類比較複雜,若是直接看方法容易把本身看混亂,所以我準備結合類上的解釋,先看看其私有變量。
2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field, the length field represents the length of the
whole message
Let's give another twist to the previous example. The only difference from the previous example is that the length field represents the length of the whole message instead of the message body, just like the third example. We have to count the length of HDR1 and Length into lengthAdjustment. Please note that we don't need to take the length of HDR2 into account because the length field already includes the whole header length.
* BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
* +------+--------+------+----------------+ +------+----------------+
* | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
* | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
* +------+--------+------+----------------+ +------+----------------+複製代碼
lengthFieldOffset
: 該字段表明 Length 字段是從第幾個字節開始的。上面的例子裏,Length 字段是從第1個字節開始(HDR1 是第0個字節),所以該值即爲0。
lengthFieldLength
: 該字段表明 Length 字段所佔用的字節數。上面的例子裏,Length 字段佔用2個字節,所以該值爲2。
lengthAdjustment
: 該字段表明 Length 字段結束位置到真正的內容開始位置的距離。上面例子裏,由於 Length 字段的含義是整個消息(包括 HDR一、Length、HDR二、Actual Content,通常 Length 指的只是 Actual Content),因此 Length 末尾到真正的內容開始位置(HDR1的開始處),至關於減小3個字節,因此是-3。
initialBytesToStrip
: 展現時須要從 Length 字段末尾開始跳過幾個字節。上面例子裏,由於真正的內容是從 HDR1 開始的,最終展現的內容是從 HDR2 開始的,因此中間差了3個字節,因此該值是3。
該類的解碼方法比較複雜,有興趣的同窗能夠試着本身分析一下。
這一篇主要是結合 Netty 裏的源代碼講解了 Netty 中封裝成幀(Framing)的三種方式,相信你必定有了不同的理解。
有興趣的話能夠訪問個人博客或者關注個人公衆號、頭條號,說不定會有意外的驚喜。