因爲項目需要,需要自定義通信協議。序列化協議使用到了Google的ProtoBuf,這裏也是通過一個案例來實現基於Netty的私有化協議的開發。
protoBuf 介紹
Google Protocol Buffer(protoBuf)是一種平臺無關,語言無關,可擴展且輕便高效的序列化數據結構的協議,
相比傳統的XML、JSON序列化的方式,更小、序列化更快、傳輸速度更快。只需要定義一次你的結構化文件,就可以自己
生成源代碼,實現輕鬆的讀寫結構化文件。並且可以使用各種語言.
優點
- 跨平臺、跨語言,支持Java、Python、C++、Go、JavaScript;
- 序列化更快(比xml、json方式提升20~100倍);
- 代碼自動生成,生成不同語言代碼;
缺點
- 通用性較差,沒有json、xml普及;
- 數據結構化沒有json、xml表達能力那麼強;
- 以二進制數據流方式存儲(不可讀),需要通過.proto文件 才能瞭解到數據結構;
總結:protoBuf更側重數據序列化,應用場景更明確,xml、json的應用場景更爲豐富。
protoBuf 語法
這裏就不一一介紹語法,後面會開專門章節來講,直接貼上一段定義好的.proto消息文件。
syntax = "proto2";
package com.elisland.customprotocol;
option optimize_for = SPEED; //文件屬性
option java_package = "com.elisland.customprotocol";
option java_outer_classname = "CustomMessageData";
message MessageData{
required int64 length = 1;
optional Content content = 2;
enum DataType {
REQ_LOGIN = 0; //上線登錄驗證環節 等基礎信息上報
RSP_LOGIN = 1; //返回上線登錄狀態與基礎信息
PING = 2; //心跳
PONG = 3; //心跳
REQ_ACT = 4; //動作請求
RSP_ACT = 5; //動作響應
REQ_CMD = 6; //指令請求
RSP_CMD = 7; //指令響應
REQ_LOG = 8 ;//日誌請求
RSP_LOG = 9; //日誌響應
}
optional DataType order = 3;
message Content{
optional int64 contentLength = 1;
optional string data = 2;
}
}
文件生成,這裏使用Java語言生成,java文件。語法 protoc --java_out=生成java文件位置 .proto文件位置
代碼生成完成後,接下來就是基於Netty來傳輸protoBuf協議。
Netty 使用
Netty簡介:高效的Java NIO框架,通過對傳統NIO的封裝,提供更加便捷高效的非阻塞式開發。 後面會開具體的章節來講解Netty的學習。本次案例,通過Netty構建客戶端、服務端之間的數據傳輸、以及通過我們自定義編解碼器的方式來解析和編碼我們使用的.protoBuf消息。閒話少說,後面直接上代碼。
服務器Server端
服務端
public class MyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();//接收連接,將連接發送給worker
EventLoopGroup workerGroup = new NioEventLoopGroup();//處理連接
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyDecode());
pipeline.addLast(new MyEncode());
pipeline.addLast(new MyServerHandle());
}
});
ChannelFuture sync = serverBootstrap.bind(8899).sync();
sync.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();//關閉
workerGroup.shutdownGracefully();
}
}
}
自定義編碼器
public class MyEncode extends MessageToByteEncoder<CustomMessageData.MessageData> {
/**
* 編碼器
* @param ctx
* @param msg
* @param out
* @throws Exception
*/
@Override
protected void encode(ChannelHandlerContext ctx, CustomMessageData.MessageData msg, ByteBuf out) throws Exception {
System.out.println("MyEncode invoke...");
out.writeInt(((int) (msg.getLength())));
out.writeByte((byte)msg.getOrder().getNumber());
out.writeBytes(msg.getContent().getData().getBytes());
}
}
自定義解碼器
public class MyDecode extends ReplayingDecoder {
/**
* 解碼器
* @param ctx
* @param in
* @param out
* @throws Exception
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("Mydecode invoke...");
int length = in.readInt();
byte order = in.readByte();
byte[] content = new byte[length];
in.readBytes(content);
CustomMessageData.MessageData decodeData = CustomMessageData.MessageData.newBuilder()
.setLength(length)
.setOrder(CustomMessageData.MessageData.DataType.forNumber(order))
.setContent(CustomMessageData.MessageData.Content.newBuilder()
.setData(new String(content, Charset.forName("utf-8")))).build();
out.add(decodeData);
}
}
服務端Handler
public class MyServerHandle extends SimpleChannelInboundHandler<CustomMessageData.MessageData> {
int count;//記錄接收數據數量
/**
* 接受client發送來的消息
* @param ctx
* @param msg
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, CustomMessageData.MessageData msg) throws Exception {
long length = msg.getLength();
CustomMessageData.MessageData.DataType order = msg.getOrder();
String ContentData = msg.getContent().getData();
System.out.println("服務端接收到的數據長度:" + length);
System.out.println("服務端接收到的數據指令:" + order);
System.out.println("服務端接收到的數據內容:" + ContentData);
System.out.println("服務端接收到的數據數量:" + (++count));
String sendClientMessage = UUID.randomUUID().toString();
int sendClientMessageLength = sendClientMessage.getBytes("utf-8").length;
//每次收到客戶端消息後,向客戶端返回UUID字符串
CustomMessageData.MessageData message = CustomMessageData.MessageData.newBuilder()
.setLength(sendClientMessageLength)
.setOrder(CustomMessageData.MessageData.DataType.forNumber(1))
.setContent(CustomMessageData.MessageData.Content.newBuilder().setData(sendClientMessage)).build();
ctx.writeAndFlush(message);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
ctx.close();
}
}
服務器Client端
客戶端
public class MyClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyDecode());
pipeline.addLast(new MyEncode());
pipeline.addLast(new MyClientHandle());
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 8899).sync();
channelFuture.channel().closeFuture().sync();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}
客戶端handler
public class MyClientHandle extends SimpleChannelInboundHandler<CustomMessageData.MessageData> {
int count;
/**
* 客戶端和服務端建立連接後,向服務端發送消息
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
String message = "message for client";
int length = message.getBytes().length;
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLength(length);
messageProtocol.setContent(message);
//構建消息內容
CustomMessageData.MessageData messageData = CustomMessageData.MessageData.newBuilder()
.setLength(length)
.setOrder(CustomMessageData.MessageData.DataType.forNumber(1))
.setContent(CustomMessageData.MessageData.Content.newBuilder().setData(message)).build();
ctx.writeAndFlush(messageData);
}
}
/**
* 接受響應服務端消息
* @param ctx
* @param msg
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, CustomMessageData.MessageData msg) throws Exception {
long length = msg.getLength();
CustomMessageData.MessageData.DataType order = msg.getOrder();
String data = msg.getContent().getData();
System.out.println("客戶端接收到的數據長度:" + length);
System.out.println("客戶端接收到的數據指令:" + order);
System.out.println("客戶端接收到的數據內容:" + data);
System.out.println("客戶端接收到的數據數量:" + (++count));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
ctx.close();
}
}
注* .protoBuf生成的java文件未貼出。
流程:服務端啓動後,監聽客戶端,當客戶端啓動完成後,客戶端handler中的channelActive()方法會連續向服務器端發送10條消息。在傳輸過程中,先經過自定義的Encode編碼器,服務器端接受到發送過來的消息時,再通過自定義解碼器去解析消息。服務器向客戶端返回應答消息(UUID字符串),同樣是通過編碼器將消息編碼,客戶端收到消息後再通過解碼器將消息解析打印。