前言
對於應用層通訊協議而言,目前流行的協議雖然可以很好地支援業務的快速迭代,但是不可否認存在安全性、可擴充性等問題。在訊息佇列或者微服務框架中,利用自定義協議提高通訊效率很常見的現象。是否你也曾想自定義協議但無從入手而苦惱,跟著小棧一起動手實現一個自定義協議吧!
請求響應協議設計
通用設計:考慮多協議通訊,利用版本號以及協議型別使得協議可以平滑引入新協議擴充;已有的協議升級則利用協議版本供擴充。
請求-響應設計:區分請求或響應型別,引入訊息型別標識REQUEST(byte)0、RESPONCE(byte)1;訊息主體需利用編碼器序列化框架進行編碼解碼,引入編碼型別;在請求響應過程中,利用訊息id作為唯一標識,利用超時時間來檢測伺服器處理時效,超時直接不返回;引入body_length來標識訊息主體的長度,使得協議可變長。
協議處理實現
編碼
在UniqueEncoder構建可變儲存ChannelBuffer,讀取協議型別並在協議工廠查詢協議對業務物件編碼。
public class UniqueEncoder extends OneToOneEncoder {
@Override
protected Object encode(ChannelHandlerContext ctx, Channel channel, Object msg) throws Exception {
ChannelBuffer channelBuffer = ChannelBuffers.dynamicBuffer();
int type = 0;
if (msg instanceof ResponceWrapper) {
type = ((ResponceWrapper) msg).getProtocolType();
} else if (msg instanceof RequestWrapper) {
type = ((RequestWrapper) msg).getProtocolType();
}
return ProtocolFactory.getProtocol(type).encode(msg, channelBuffer);
}
}複製程式碼
那編碼到底是怎麼處理的呢?
可以清晰看到,ProcessorProtocol類前面明確地說明了協議頭、協議主體的結構資訊,其實跟剛開始貼的圖是一致的。其實這個方便可以清晰地根據說明對業務編碼,可以養成這個好的習慣。編碼過程先校驗業務物件型別,是請求 OR 響應型別,並呼叫ChannelBuffer將協議相關資訊依次寫入,不再贅述。可以看到寫入長度是5 * 1B + 3 * 4B + body主體。
/**
* PROTOCOL HEADER
* VERSION (1B) version
* TYPE (1B) type
* PROCESSOR PROTOCOL
* VERSION (1B)
* TYPE (1B) REQUEST RESPONCE
* CODEC (1B) serialize/deserialize
* ID (4B) msg id
* TIMEOUT (4B) timeout
* BODYLENGHT (4B) body length
* BODY CONTEXT (BODYLENGTH)
* @author duxuan
*/
public class ProcessorProtocol implements Protocol {
private final int type = 1;
private final int version = 1;
private final int CUSTOMPROTOCOL_HEADER_LENGTH = 3 * 1 + 3 * 4;
public static final byte REQUEST = (byte) 1;
public static final byte RESPONCE = (byte) 0;
/**
* @param msg 訊息實體
* @param channelBuffer 接收編碼資料
* @return
* @throws Exception
*/
@Override
public ChannelBuffer encode(Object msg, ChannelBuffer channelBuffer) throws Exception {
if (!(msg instanceof RequestWrapper ||
msg instanceof ResponceWrapper)) {
throw new Exception();
}
int processType = REQUEST;
int codec = 0;
int id = 0;
int timeout = 0;
int bodyLength = 0;
byte[] body = new byte[0];
if (msg instanceof RequestWrapper) {
RequestWrapper requestWrapper = (RequestWrapper) msg;
processType = REQUEST;
codec = requestWrapper.getCodecType();
id = requestWrapper.getId();
timeout = requestWrapper.getTimeout();
body = Codecs.getEncoder(codec).encode(requestWrapper.getMsg());
} else if (msg instanceof ResponceWrapper) {
processType = RESPONCE;
ResponceWrapper responceWrapper = (ResponceWrapper) msg;
codec = responceWrapper.getCodecType();
id = responceWrapper.getId();
body = Codecs.getEncoder(codec).encode(responceWrapper.getBody());
}
bodyLength = body.length;
/**
* 5 * 1B
*/
// default version
channelBuffer.writeByte(1);
channelBuffer.writeByte(type);
channelBuffer.writeByte(version);
channelBuffer.writeByte(processType);
channelBuffer.writeByte(codec);
/**
* 3 * 4B
*/
channelBuffer.writeInt(id);
channelBuffer.writeInt(timeout);
channelBuffer.writeInt(bodyLength);
/**
* body
*/
channelBuffer.writeBytes(body);
return channelBuffer;
}
}複製程式碼
解碼
利用FrameDecoder實現無感知粘包拆包處理,而UniqueDecoder只需要負責協議解析即可,如解析失敗,則重置讀指標originPos;解析成功則返回已解析物件。
(粘包拆包如何處理?未了解的同學可點選檢視)
public class UniqueDecoder extends FrameDecoder {
@Override
protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception {
final int originPos = buffer.readerIndex();
if (buffer.readableBytes() < 2) {
return null;
}
int version = buffer.readByte();
if (version == 1) {
int type = buffer.readByte();
// 協議支援 協議解析+body
Protocol protocol = ProtocolFactory.getProtocol(type);
if (protocol == null) {
throw new Exception("UnSupport protocol");
}
// reset
protocol.decode(buffer, null, originPos);
} else {
throw new Exception("UnSupport version:" + version);
}
return null;
}
}複製程式碼
可以看到在UniqueDecoder中讀取通訊版本version、協議型別type,並查詢已註冊到協議工廠的協議來負責解析,如果查詢不到,則丟擲異常。
/**
* PROTOCOL HEADER
* VERSION (1B) version
* TYPE (1B) type
* PROCESSOR PROTOCOL
* VERSION (1B)
* TYPE (1B) REQUEST RESPONCE
* CODEC (1B) serialize/deserialize
* ID (4B) msg id
* TIMEOUT (4B) timeout
* BODYLENGHT (4B) body length
* BODY CONTEXT (BODYLENGTH)
* @author duxuan
*/
public class ProcessorProtocol implements Protocol {
private final int type = 1;
private final int version = 1;
private final int CUSTOMPROTOCOL_HEADER_LENGTH = 3 * 1 + 3 * 4;
public static final byte REQUEST = (byte) 1;
public static final byte RESPONCE = (byte) 0;
/**
* @param channelBuffer 已讀取過通訊版本以及協議型別(2B)
* @param errorObject 設定解碼失敗返回的型別
* @param originPos 已記錄解碼前的readerIndex,用於讀取失敗重置
* @return * @throws Exception
*/
@Override
public Object decode(ChannelBuffer channelBuffer, Object errorObject, final int originPos) throws Exception {
if (channelBuffer.readableBytes() < CUSTOMPROTOCOL_HEADER_LENGTH) {
channelBuffer.readerIndex(originPos);
return errorObject;
}
int version = channelBuffer.readByte();
if (version == 1) {
int type = channelBuffer.readByte();
int codec = channelBuffer.readByte();
int msgId = channelBuffer.readInt();
int timeout = channelBuffer.readInt();
int bodyLength = channelBuffer.readInt();
byte[] body = new byte[bodyLength];
channelBuffer.readBytes(body);
// decode
Decoder decoder = Codecs.getDecoder(codec);
if (decoder == null) {
throw new Exception("could not support codec decoder");
}
if (type == REQUEST) {
RequestWrapper requestWrapper = new RequestWrapper(body, msgId, timeout, codec, type);
return requestWrapper;
} else if (type == RESPONCE) {
ResponceWrapper responceWrapper = new ResponceWrapper(body, msgId, codec, type);
return responceWrapper;
}
}else {
throw new Exception("could not support processorProtocol version");
}
return null;
}
} 複製程式碼
解碼時校驗可讀位元組數是否小於協議頭長度,如果小於,重置readerIndex;否則依次讀取協議版本、請求或者響應型別、編碼型別、訊息id、超時時間、訊息主體長度、訊息主體,並根據編碼型別呼叫序列化框架將訊息解碼成業務物件供業務處理器使用。
常用序列化框架介紹
Kryo | 速度快,序列化後體積小 | 跨語言支援較複雜 |
Hessian | 預設支援跨語言 | 較慢 |
Protostuff | 速度快,基於protobuf | 需靜態編譯 |
Protostuff-Runtime | 無需靜態編譯,但序列化前需預先傳入schema | 不支援無預設建構函式的類,反序列化時需使用者自己初始化序列化後的物件,其只負責將該物件進行賦值 |
Java | 使用方便,可序列化所有類 | 速度慢,佔空間 |
序列化處理可以根據需要實現並註冊到協議編解碼器即可!
可以掃描關注路上小棧或者加wx(arron1126912882)備註掘金歡迎交流!