TCP的粘包拆包問題
我們知道Dubbo的網路通訊框架Netty是基於TCP協議的,TCP協議的網路通訊會存在粘包和拆包的問題,先看下為什麼會出現粘包和拆包
- 當要傳送的資料大於TCP傳送緩衝區剩餘空間大小,將會發生拆包
- 待傳送資料大於MSS(最大報文長度),TCP在傳輸前將進行拆包
- 要傳送的資料小於TCP傳送緩衝區的大小,TCP將多次寫入緩衝區的資料一次傳送出去,將會發生粘包
- 接收資料端的應用層沒有及時讀取接收緩衝區中的資料,將發生粘包
以上四點基本上是出現粘包和拆包的原因,業界的解決方法一般有以下幾種:
- 將每個資料包分為訊息頭和訊息體,訊息頭中應該至少包含資料包的長度,這樣接收端在接收到資料後,就知道每一個資料包的實際長度了(Dubbo就是這種方案)
- 訊息定長,每個資料包的封裝為固定長度,不夠補0
- 在資料包的尾部設定特殊字元,比如FTP協議
Dubbo訊息協議頭規範
在dubbo.io官網上找到一張圖,協議頭約定
dubbo的訊息頭是一個定長的 16個位元組的資料包:- magic High & Magic Low:2byte:0-7位 8-15位:類似java位元組碼檔案裡的魔數,用來判斷是不是dubbo協議的資料包,就是一個固定的數字
- Serialization id:1byte:16-20位:序列id,21 event,22 two way 一個標誌位,是單向的還是雙向的,23 請求或響應標識,
- status:1byte: 24-31位:狀態位,設定請求響應狀態,request為空,response才有值
- Id(long):8byte:32-95位:每一個請求的唯一識別id(由於採用非同步通訊的方式,用來把請求request和返回的response對應上)
- data length:4byte:96-127位:訊息體長度,int 型別
看完這張圖,大致可以理解Dubbo通訊協議解決的問題,Dubbo採用訊息頭和訊息體的方式來解決粘包拆包,並在訊息頭中放入了一個唯一Id來解決非同步通訊關聯request和response的問題,下面以一次呼叫為入口分為四個部分來看下原始碼具體實現
Comsumer端請求編碼
private class InternalEncoder extends OneToOneEncoder {
@Override
protected Object encode(ChannelHandlerContext ctx, Channel ch, Object msg) throws Exception {
com.alibaba.dubbo.remoting.buffer.ChannelBuffer buffer =
com.alibaba.dubbo.remoting.buffer.ChannelBuffers.dynamicBuffer(1024);
NettyChannel channel = NettyChannel.getOrAddChannel(ch, url, handler);
try {
codec.encode(channel, buffer, msg);
} finally {
NettyChannel.removeChannelIfDisconnected(ch);
}
return ChannelBuffers.wrappedBuffer(buffer.toByteBuffer());
}
}
複製程式碼
這個InternalEncoder是一個NettyCodecAdapter的內部類,我們看到codec.encode(channel, buffer, msg)這裡,這個時候codec=DubboCountCodec,這個是在構造方法中傳入的,DubboCountCodec.encode-->ExchangeCodec.encode-->ExchangeCodec.encodeRequest
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
//獲取序列化方式,預設是Hessian序列化
Serialization serialization = getSerialization(channel);
// new了一個16位的byte陣列,就是request的訊息頭
byte[] header = new byte[HEADER_LENGTH];
// 往訊息頭中set magic數字,這個時候header中前2個byte已經填充
Bytes.short2bytes(MAGIC, header);
// set request and serialization flag.第三個byte已經填充
header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
if (req.isTwoWay()) header[2] |= FLAG_TWOWAY;
if (req.isEvent()) header[2] |= FLAG_EVENT;
// set request id.這個時候是0
Bytes.long2bytes(req.getId(), header, 4);
// 編碼 request data.
int savedWriteIndex = buffer.writerIndex();
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
//序列化
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
if (req.isEvent()) {
encodeEventData(channel, out, req.getData());
} else {
//編碼訊息體資料
encodeRequestData(channel, out, req.getData());
}
out.flushBuffer();
bos.flush();
bos.close();
int len = bos.writtenBytes();
checkPayload(channel, len);
//在訊息頭中設定訊息體長度
Bytes.int2bytes(len, header, 12);
// write
buffer.writerIndex(savedWriteIndex);
buffer.writeBytes(header); // write header.
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}
複製程式碼
就是這方法,對request請求進行了編碼操作,具體操作我寫在程式碼的註釋中,就是剛剛我們分析的訊息頭的程式碼實現
Provider端請求解碼
看到NettyCodecAdapter中的InternalDecoder這個類的messageReceived方法,這裡就是Provider端對於Consumer端的request請求的解碼
public void messageReceived(ChannelHandlerContext ctx, MessageEvent event) throws Exception {
···
try {
// decode object.
do {
saveReaderIndex = message.readerIndex();
try {
msg = codec.decode(channel, message);
} catch (IOException e) {
buffer = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER;
throw e;
}
···
複製程式碼
進入DubboCountCodec.decode--ExchangeCodec.decode
// 檢查 magic number.
if (readable > 0 && header[0] != MAGIC_HIGH
···
}
// check 長度如果小於16位繼續等待
if (readable < HEADER_LENGTH) {
return DecodeResult.NEED_MORE_INPUT;
}
// get 訊息體長度
int len = Bytes.bytes2int(header, 12);
checkPayload(channel, len);
//訊息體長度+訊息頭的長度
int tt = len + HEADER_LENGTH;
//如果總長度小於tt,那麼返回繼續等待
if (readable < tt) {
return DecodeResult.NEED_MORE_INPUT;
}
// limit input stream.
ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
try {
//解析訊息體內容
return decodeBody(channel, is, header);
} finally {
···
}
複製程式碼
這裡對於剛剛的request進行解碼操作,具體操作步驟寫在註釋中了
Provider端響應編碼
當服務端執行完介面呼叫,看下服務端的響應編碼,和消費端不一樣的地方是,服務端進入的是ExchangeCodec.encodeResponse方法
try {
//獲取序列化方式 預設Hession協議
Serialization serialization = getSerialization(channel);
// 初始化一個16位的header
byte[] header = new byte[HEADER_LENGTH];
// set magic 數字
Bytes.short2bytes(MAGIC, header);
// set request and serialization flag.
header[2] = serialization.getContentTypeId();
if (res.isHeartbeat()) header[2] |= FLAG_EVENT;
// set response status.這裡返回的是OK
byte status = res.getStatus();
header[3] = status;
// set request id.
Bytes.long2bytes(res.getId(), header, 4);
int savedWriteIndex = buffer.writerIndex();
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
// 編碼返回訊息體資料或者錯誤資料
if (status == Response.OK) {
if (res.isHeartbeat()) {
encodeHeartbeatData(channel, out, res.getResult());
} else {
encodeResponseData(channel, out, res.getResult());
}
} else out.writeUTF(res.getErrorMessage());
out.flushBuffer();
bos.flush();
bos.close();
int len = bos.writtenBytes();
checkPayload(channel, len);
Bytes.int2bytes(len, header, 12);
// write
buffer.writerIndex(savedWriteIndex);
buffer.writeBytes(header); // write header.
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
} catch (Throwable t) {
// 傳送失敗資訊給Consumer,否則Consumer只能等超時了
if (!res.isEvent() && res.getStatus() != Response.BAD_RESPONSE) {
try {
// FIXME 在Codec中列印出錯日誌?在IoHanndler的caught中統一處理?
logger.warn("Fail to encode response: " + res + ", send bad_response info instead, cause: " + t.getMessage(), t);
Response r = new Response(res.getId(), res.getVersion());
r.setStatus(Response.BAD_RESPONSE);
r.setErrorMessage("Failed to send response: " + res + ", cause: " + StringUtils.toString(t));
channel.send(r);
return;
} catch (RemotingException e) {
logger.warn("Failed to send bad_response info back: " + res + ", cause: " + e.getMessage(), e);
}
}
// 重新丟擲收到的異常
···
}
複製程式碼
基本上和消費方請求編碼一樣,多了一個步驟,一個是在訊息頭中加入了一個狀態位,第二個是如果傳送有異常,則繼續傳送失敗資訊給Consumer,否則Consumer只能等超時了
Conmsuer端響應解碼
和上面的解碼一樣,具體操作是在ExchangeCodec.decode--DubboCodec.decodeBody中