所謂的協議,是由語法、語義、時序這三個要素組成的一種規範,通訊雙方按照該協議規範來實現網路資料傳輸,這樣通訊雙方才能實現資料正常通訊和解析。
由於不同的中介軟體在功能方面有一定差異,所以其實應該是沒有一種標準化協議來滿足不同差異化需求,因此很多中介軟體都會定義自己的通訊協議,另外通訊協議可以解決粘包和拆包問題。
在本篇文章中,我們來實現一個自定義訊息協議。
自定義協議的要素
自定義協議,那這個協議必須要有組成的元素,
- 魔數: 用來判斷資料包的有效性
- 版本號: 可以支援協議升級
- 序列化演算法: 訊息正文采用什麼樣的序列化和反序列化方式,比如json、protobuf、hessian等
- 指令型別:也就是當前傳送的是一個什麼型別的訊息,像zookeeper中,它傳遞了一個Type
- 請求序號: 基於雙工協議,提供非同步能力,也就是收到的非同步訊息需要找到前面的通訊請求進行響應處理
- 訊息長度
- 訊息正文
協議定義
sessionId | reqType | Content-Length | Content |
其中Version
,Content-Length
,SessionId
就是Header資訊,Content
就是互動的主體。
定義專案結構以及引入包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
專案結構如圖4-1所示:
- netty-message-mic : 表示協議模組。
- netty-message-server :表示nettyserver。
<center>圖4-1</center>
- 引入log4j.properties
在nettyMessage-mic中,包的結構如下。
定義Header
表示訊息頭
@Data
public class Header{
private long sessionId; //會話id : 佔8個位元組
private byte type; //訊息型別: 佔1個位元組
private int length; //訊息長度 : 佔4個位元組
}
定義MessageRecord
表示訊息體
@Data
public class MessageRecord{
private Header header;
private Object body;
}
OpCode
定義操作型別
public enum OpCode {
BUSI_REQ((byte)0),
BUSI_RESP((byte)1),
PING((byte)3),
PONG((byte)4);
private byte code;
private OpCode(byte code) {
this.code=code;
}
public byte code(){
return this.code;
}
}
定義編解碼器
分別定義對該訊息協議的編解碼器
MessageRecordEncoder
@Slf4j
public class MessageRecordEncoder extends MessageToByteEncoder<MessageRecord> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MessageRecord record, ByteBuf byteBuf) throws Exception {
log.info("===========開始編碼Header部分===========");
Header header=record.getHeader();
byteBuf.writeLong(header.getSessionId()); //儲存8個位元組的sessionId
byteBuf.writeByte(header.getType()); //寫入1個位元組的請求型別
log.info("===========開始編碼Body部分===========");
Object body=record.getBody();
if(body!=null){
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(body);
byte[] bytes=bos.toByteArray();
byteBuf.writeInt(bytes.length); //寫入訊息體長度:佔4個位元組
byteBuf.writeBytes(bytes); //寫入訊息體內容
}else{
byteBuf.writeInt(0); //寫入訊息長度佔4個位元組,長度為0
}
}
}
MessageRecordDecode
@Slf4j
public class MessageRecordDecode extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
MessageRecord record=new MessageRecord();
Header header=new Header();
header.setSessionId(byteBuf.readLong()); //讀取8個位元組的sessionid
header.setType(byteBuf.readByte()); //讀取一個位元組的操作型別
record.setHeader(header);
//如果byteBuf剩下的長度還有大於4個位元組,說明body不為空
if(byteBuf.readableBytes()>4){
int length=byteBuf.readInt(); //讀取四個位元組的長度
header.setLength(length);
byte[] contents=new byte[length];
byteBuf.readBytes(contents,0,length);
ByteArrayInputStream bis=new ByteArrayInputStream(contents);
ObjectInputStream ois=new ObjectInputStream(bis);
record.setBody(ois.readObject());
list.add(record);
log.info("序列化出來的結果:"+record);
}else{
log.error("訊息內容為空");
}
}
}
測試協議的解析和編碼
EmbeddedChannel是netty專門改進針對ChannelHandler的單元測試而提供的
public class CodesMainTest {
public static void main( String[] args ) throws Exception {
EmbeddedChannel channel=new EmbeddedChannel(
new LoggingHandler(),
new MessageRecordEncoder(),
new MessageRecordDecode());
Header header=new Header();
header.setSessionId(123456);
header.setType(OpCode.PING.code());
MessageRecord record=new MessageRecord();
record.setBody("Hello World");
record.setHeader(header);
channel.writeOutbound(record);
ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();
new MessageRecordEncoder().encode(null,record,buf);
channel.writeInbound(buf);
}
}
編碼包分析
執行上述程式碼後,會得到下面的一個資訊
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 00 00 01 e2 40 03 00 00 00 12 ac ed 00 |.......@........|
|00000010| 05 74 00 0b 48 65 6c 6c 6f 20 57 6f 72 6c 64 |.t..Hello World |
+--------+-------------------------------------------------+----------------+
按照協議規範:
- 前面8個位元組表示sessionId
- 一個位元組表示請求型別
- 4個位元組表示長度
- 後面部分內容表示訊息體
測試粘包和半包問題
通過slice方法進行拆分,得到兩個包。
ByteBuf中提供了一個slice方法,這個方法可以在不做資料拷貝的情況下對原始ByteBuf進行拆分。
public class CodesMainTest {
public static void main( String[] args ) throws Exception {
//EmbeddedChannel是netty專門針對ChannelHandler的單元測試而提供的類。可以通過這個類來測試channel輸入入站和出站的實現
EmbeddedChannel channel=new EmbeddedChannel(
//解決粘包和半包問題
// new LengthFieldBasedFrameDecoder(2048,10,4,0,0),
new LoggingHandler(),
new MessageRecordEncoder(),
new MessageRecordDecode());
Header header=new Header();
header.setSessionId(123456);
header.setType(OpCode.PING.code());
MessageRecord record=new MessageRecord();
record.setBody("Hello World");
record.setHeader(header);
channel.writeOutbound(record);
ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();
new MessageRecordEncoder().encode(null,record,buf);
//*********模擬半包和粘包問題************//
//把一個包通過slice拆分成兩個部分
ByteBuf bb1=buf.slice(0,7); //獲取前面7個位元組
ByteBuf bb2=buf.slice(7,buf.readableBytes()-7); //獲取後面的位元組
bb1.retain();
channel.writeInbound(bb1);
channel.writeInbound(bb2);
}
}
執行上述程式碼會得到如下異常, readerIndex(0) +length(8)表示要讀取8個位元組,但是隻收到7個位元組,所以直接報錯。
2021-08-31 15:53:01,385 [io.netty.handler.logging.LoggingHandler]-[DEBUG] [id: 0xembedded, L:embedded - R:embedded] READ: 7B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 00 00 01 e2 |....... |
+--------+-------------------------------------------------+----------------+
2021-08-31 15:53:01,397 [io.netty.handler.logging.LoggingHandler]-[DEBUG] [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE
Exception in thread "main" io.netty.handler.codec.DecoderException: java.lang.IndexOutOfBoundsException: readerIndex(0) + length(8) exceeds writerIndex(7): UnpooledSlicedByteBuf(ridx: 0, widx: 7, cap: 7/7, unwrapped: PooledUnsafeDirectByteBuf(ridx: 0, widx: 31, cap: 256))
解決拆包問題
LengthFieldBasedFrameDecoder是長度域解碼器,它是解決拆包粘包最常用的解碼器,基本上能覆蓋大部分基於長度拆包的場景。其中開源的訊息中介軟體RocketMQ就是使用該解碼器進行解碼的。
首先來說明一下該解碼器的核心引數
- lengthFieldOffset,長度欄位的偏移量,也就是存放長度資料的起始位置
- lengthFieldLength,長度欄位鎖佔用的位元組數
- lengthAdjustment,在一些較為複雜的協議設計中,長度域不僅僅包含訊息的長度,還包含其他資料比如版本號、資料型別、資料狀態等,這個時候我們可以使用lengthAdjustment進行修正,它的值=包體的長度值-長度域的值
- initialBytesToStrip,解碼後需要跳過的初始位元組數,也就是訊息內容欄位的起始位置
- lengthFieldEndOffset,長度欄位結束的偏移量, 該屬性的值=lengthFieldOffset+lengthFieldLength
public class CodesMainTest {
public static void main( String[] args ) throws Exception {
EmbeddedChannel channel=new EmbeddedChannel(
//解決粘包和半包問題
new LengthFieldBasedFrameDecoder(1024,
9,4,0,0),
new LoggingHandler(),
new MessageRecordEncoder(),
new MessageRecordDecode());
Header header=new Header();
header.setSessionId(123456);
header.setType(OpCode.PING.code());
MessageRecord record=new MessageRecord();
record.setBody("Hello World");
record.setHeader(header);
channel.writeOutbound(record);
ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();
new MessageRecordEncoder().encode(null,record,buf);
//*********模擬半包和粘包問題************//
//把一個包通過slice拆分成兩個部分
ByteBuf bb1=buf.slice(0,7);
ByteBuf bb2=buf.slice(7,buf.readableBytes()-7);
bb1.retain();
channel.writeInbound(bb1);
channel.writeInbound(bb2);
}
}
新增一個長度解碼器,就解決了拆包帶來的問題。執行結果如下
2021-08-31 16:09:35,115 [com.netty.example.codec.MessageRecordDecode]-[INFO] 序列化出來的結果:MessageRecord(header=Header(sessionId=123456, type=3, length=18), body=Hello World)
2021-08-31 16:09:35,116 [io.netty.handler.logging.LoggingHandler]-[DEBUG] [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE
基於自定義訊息協議通訊
下面我們把整個通訊過程編寫完整,程式碼結構如圖4-2所示.
<center>圖4-2</center>
服務端開發
@Slf4j
public class ProtocolServer {
public static void main(String[] args){
EventLoopGroup boss = new NioEventLoopGroup();
//2 用於對接受客戶端連線讀寫操作的執行緒工作組
EventLoopGroup work = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(boss, work) //繫結兩個工作執行緒組
.channel(NioServerSocketChannel.class) //設定NIO的模式
// 初始化繫結服務通道
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline()
.addLast(
new LengthFieldBasedFrameDecoder(1024,
9,4,0,0))
.addLast(new MessageRecordEncoder())
.addLast(new MessageRecordDecode())
.addLast(new ServerHandler());
}
});
ChannelFuture cf= null;
try {
cf = b.bind(8080).sync();
log.info("ProtocolServer start success");
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
work.shutdownGracefully();
boss.shutdownGracefully();
}
}
}
ServerHandler
@Slf4j
public class ServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
MessageRecord messageRecord=(MessageRecord)msg;
log.info("server receive message:"+messageRecord);
MessageRecord res=new MessageRecord();
Header header=new Header();
header.setSessionId(messageRecord.getHeader().getSessionId());
header.setType(OpCode.BUSI_RESP.code());
String message="Server Response Message!";
res.setBody(message);
header.setLength(message.length());
ctx.writeAndFlush(res);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("伺服器讀取資料異常");
super.exceptionCaught(ctx, cause);
ctx.close();
}
}
客戶端開發
public class ProtocolClient {
public static void main(String[] args) {
//建立工作執行緒組
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024,
9,4,0,0))
.addLast(new MessageRecordEncoder())
.addLast(new MessageRecordDecode())
.addLast(new ClientHandler());
}
});
// 發起非同步連線操作
try {
ChannelFuture future = b.connect(new InetSocketAddress("localhost", 8080)).sync();
Channel c = future.channel();
for (int i = 0; i < 500; i++) {
MessageRecord message = new MessageRecord();
Header header = new Header();
header.setSessionId(10001);
header.setType((byte) OpCode.BUSI_REQ.code());
message.setHeader(header);
String context="我是請求資料"+i;
header.setLength(context.length());
message.setBody(context);
c.writeAndFlush(message);
}
//closeFuture().sync()就是讓當前執行緒(即主執行緒)同步等待Netty server的close事件,Netty server的channel close後,主執行緒才會繼續往下執行。closeFuture()在channel close的時候會通知當前執行緒。
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
}
ClientHandler
@Slf4j
public class ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
MessageRecord record=(MessageRecord)msg;
log.info("Client Receive message:"+record);
super.channelRead(ctx, msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
ctx.close();
}
}
版權宣告:本部落格所有文章除特別宣告外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自Mic帶你學架構
!
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注同名微信公眾號獲取更多技術乾貨!