前一陣在工作中用到了RabbitMQ,因此對幾種常見的消息隊列產生了興趣。首先從GitHub上下載了RocketMQ的源碼打算一探究竟。在閱讀remoting這個模塊時遇到了很大的障礙。RocketMQ的網絡編程完全基于Netty,而本人對Netty的理解還只停留在了知道這是一款封裝了NIO的優秀框架上。于是正好就借此機會先揭開Netty的面紗。
閱讀完《Netty IN ACTION》后有些手癢,就用Netty實現了一個簡易的應用層協議以及一個同步調用的方法。
github:https://github.com/ztglcy/netty-protocol
整體結構
圖里demo中是client和server的簡易demo;handler中則是自定義協議的編碼器和解碼器;message中是與傳輸的消息相關的類;processor是服務端的業務處理類;service中則是client和server的啟動類。
傳輸消息格式及編解碼
length是一個表示消息大小的int型數字,自定義長度解碼器解決TCP黏包問題。headerLength則是表示消息頭大小的int型數字,用以將傳輸的消息頭與消息體分開進行序列化。header和content分別存儲消息的消息頭以及消息體。
public class MessageHeader{
private int messageId;
private int clientId;
private int serverId;
private int code;
private MessageHeader(){}
public MessageHeader(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public int getMessageId() {
return messageId;
}
public void setMessageId(int messageId) {
this.messageId = messageId;
}
public int getClientId() {
return clientId;
}
public void setClientId(int clientId) {
this.clientId = clientId;
}
public int getServerId() {
return serverId;
}
public void setServerId(int serverId) {
this.serverId = serverId;
}
public void setCode(int code) {
this.code = code;
}
}
消息頭包含messageId,clientId,serverId,code四個參數,分別用以表征Message的ID,客戶端ID,服務端ID,以及消息體的格式code(維護在一個常量中)。公有的構造方法中必傳消息體格式code,私有的構造方法用于fastjson的反序列化。
public class Message {
private MessageHeader messageHeader;
private byte[] content;
private Message(){
}
public static Message createMessage(MessageHeader messageHeader){
Message message = new Message();
message.messageHeader = messageHeader;
return message;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
public MessageHeader getMessageHeader() {
return messageHeader;
}
//還有編碼與解碼的方法
......
}
Message的結構比較直白,包含消息頭和消息體以及編碼與解碼的方法。解碼與編碼的方法在解碼器與編碼器中進行調用。先來介紹一下編碼的過程。
public class ProtocolEncoder extends MessageToByteEncoder<Message> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf byteBuf) throws Exception {
ByteBuffer byteBuffer = message.encode();
byteBuf.writeBytes(byteBuffer);
}
}
編碼器繼承了MessageToByteEncoder并實現了其encode()方法進行編碼。編碼器直接調用傳進來的Message自己的編碼方法,將編碼后的ByteBuffer寫入ByteBuf中。再來看一下Message怎么實現這個方法的。
public ByteBuffer encode(){
int length = 4;
byte[] bytes = SerializableHelper.encode(messageHeader);
if(bytes != null){
length += bytes.length;
}
if(content!=null){
length += content.length;
}
ByteBuffer byteBuffer = ByteBuffer.allocate(length + 4);
byteBuffer.putInt(length);
if(bytes != null){
byteBuffer.putInt(bytes.length);
byteBuffer.put(bytes);
}else{
byteBuffer.putInt(0);
}
if(content!=null){
byteBuffer.put(content);
}
byteBuffer.flip();
return byteBuffer;
}
length用以表示整個消息大小,計算方式為表示消息頭大小的int+序列化后的消息頭大小+消息體大小。計算完成后申請一塊length+4大小的ByteBuffer(因為length本身存儲也要4個字節)。將消息內容按照上面給出的格式依次寫入Buffer中。
public class ProtocolDecoder extends LengthFieldBasedFrameDecoder {
public ProtocolDecoder() {
super(16777216, 0, 4,0,4);
}
@Override
public Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf byteBuf = (ByteBuf) super.decode(ctx, in);
if(byteBuf == null){
return null;
}
ByteBuffer byteBuffer = byteBuf.nioBuffer();
return Message.decode(byteBuffer);
}
}
解碼器繼承了LengthFieldBasedFrameDecoder用以解決粘包的問題。構造函數中的參數分別表示包的最大值、長度字段的偏移量、長度字段占的字節數、添加到長度字段的補償值以及從解碼幀中第一次去除的字節數。因為消息的頭部存儲了4個字節的表示消息大小的Int型,所以后4個參數為0、4、0、4。經過處理后的消息已經剝離掉了最消息頭部的Int型。再調用Message自身的decode()方法進行解碼。
public static Message decode(ByteBuffer byteBuffer){
int length = byteBuffer.limit();
int headerLength = byteBuffer.getInt();
byte[] headerData = new byte[headerLength];
byteBuffer.get(headerData);
MessageHeader messageHeader = SerializableHelper.decode(headerData,MessageHeader.class);
byte[] content = new byte[length - headerLength -4];
byteBuffer.get(content);
Message message = Message.createMessage(messageHeader);
message.setContent(content);
return message;
}
解碼時首先將消息頭的長度從ByteBuffer中取出,然后讀取該長度的字節作為消息頭進行反序列化,其他部分則作為消息體,重新組裝成新的Message。
服務端與客戶端的引導
public interface ProtocolService {
void start();
void shutdown();
}
服務端與客戶端都繼承了ProtocolService接口,實現了start()和shutdown()兩個方法。
public class NettyProtocolServer implements ProtocolService {
private EventLoopGroup bossGroup = new NioEventLoopGroup(1);
private EventLoopGroup workerGroup = new NioEventLoopGroup();
private Map<Integer,ProtocolProcessor> processorMap = new HashMap<>();
@Override
public void start(){
try{
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.localAddress(8888)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new ProtocolEncoder()
,new ProtocolDecoder()
,new ProtocolServerProcessor()
);
}
});
ChannelFuture cf = bootstrap.bind().sync();
} catch (InterruptedException e) {
}
}
@Override
public void shutdown() {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
public void registerProcessor(Integer code,ProtocolProcessor protocolProcessor){
processorMap.put(code,protocolProcessor);
}
public class ProtocolServerProcessor extends SimpleChannelInboundHandler<Message> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Message message) throws Exception {
Integer code = message.getMessageHeader().getCode();
ProtocolProcessor processor = processorMap.get(code);
if(processor != null){
Message response = processor.process(message);
channelHandlerContext.writeAndFlush(response);
}
}
}
}
服務端的start()方法中完成了服務端的初始化,很常見的netty寫法,將編碼器、解碼器以及業務處理器ProtocolServerProcessor加入了Worker的Pipeline中。這里業務處理器也可以放在線程池里執行,防止業務處理時間太長造成堵塞。shutdown()方法則將兩個EventLoopGroup進行關閉,防止資源泄露。registerProcessor()方法則是將業務處理器以KV的形式注冊到服務端中,ProtocolServerProcessor會根據消息頭中的code在map中查找對應的業務處理器進行業務的處理。
public class DemoProcessor implements ProtocolProcessor{
@Override
public Message process(Message message) {
byte[] bodyDate = message.getContent();
DemoMessageBody messageBody = SerializableHelper.decode(bodyDate,DemoMessageBody.class);
System.out.println(messageBody.getDemo());
MessageHeader messageHeader = new MessageHeader(1);
messageHeader.setMessageId(message.getMessageHeader().getMessageId());
DemoMessageBody responseBody = new DemoMessageBody();
responseBody.setDemo("I received!");
Message response = Message.createMessage(messageHeader);
response.setContent(SerializableHelper.encode(responseBody));
return response;
}
}
DemoProcessor是一個示例的業務處理器,將傳來的消息體解碼后返回一個I received!的回復,這里注意的是messageId要與請求的消息一致,用以表征這是哪個請求的返回。
public class NettyProtocolClient implements ProtocolService {
private EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
private Bootstrap bootstrap = new Bootstrap();
private ConcurrentHashMap<Integer,Response> responseMap = new ConcurrentHashMap<>();
@Override
public void start() {
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new ProtocolDecoder(),
new ProtocolEncoder(),
new ProtocolClientProcessor());
}
});
}
@Override
public void shutdown() {
eventLoopGroup.shutdownGracefully();
}
public Message send(String address, Message message){
try {
Response response = new Response();
responseMap.put(message.getMessageHeader().getMessageId(),response);
Channel channel = bootstrap.connect(address,8888).sync().channel();
channel.writeAndFlush(message);
Message responseMessage = response.waitResponse();
responseMap.remove(message.getMessageHeader().getMessageId());
channel.close();
return responseMessage;
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
public class ProtocolClientProcessor extends SimpleChannelInboundHandler<Message> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Message message) throws Exception {
Response response = responseMap.get(message.getMessageHeader().getMessageId());
if (response != null){
response.putResponse(message);
}
}
}
}
客戶端與服務端的start()方法和shutdown()方法類似。客戶端提供了一個send()方法用以消息的同步調用,send()方法在發送信息后以消息的messageId為key生成一個Response的實例緩存在responseMap中,調用Response中的countDownLatch.await()方法堵塞住等待返回(這里應該加一個時間限制以防止線程無限期地堵塞住)。ProtocolClientProcessor會處理返回的消息,將其存入對應的Response中,并調用countDownLatch.countDown()。這樣客戶端線程就可以收到結果同步返回。還可以改進的一點在于保持客戶端與服務端的長連接,將其緩存在客戶端中,每次發送消息都用已緩存的連接,減少開銷。
DEMO
最后分別編寫一個客戶端與服務端的demo用以測試我們的協議。
public class ServerDemo {
public static void main(String[] args) {
NettyProtocolServer nettyProtocolServer = new NettyProtocolServer();
nettyProtocolServer.registerProcessor(1,new DemoProcessor());
nettyProtocolServer.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
nettyProtocolServer.shutdown();
}
}
服務端的demo首先構建一個NettyProtocolServer的實例,將DemoProcessor注冊到服務端中之后掛起主線程等待客戶端的消息,最后shutdown掉NettyProtocolServer。
public class ClientDemo {
public static void main(String[] args) {
NettyProtocolClient client = new NettyProtocolClient();
client.start();
Message message = demoMessage();
Message messageResponse = client.send("localhost",message);
System.out.println(SerializableHelper.decode(messageResponse.getContent(),DemoMessageBody.class).getDemo());
client.shutdown();
}
private static Message demoMessage(){
MessageHeader messageHeader = new MessageHeader(1);
messageHeader.setMessageId(1);
messageHeader.setClientId(1);
messageHeader.setServerId(1);
Message message =Message.createMessage(messageHeader);
DemoMessageBody responseBody = new DemoMessageBody();
responseBody.setDemo("Hello World!");
message.setContent(SerializableHelper.encode(responseBody));
return message;
}
}
客戶端的demo也很簡單。構建一個NettyProtocolClient的實例,拼裝一個消息,調用send()方法,再對返回的消息稍加處理就OK啦(客戶端拼裝和處理消息可以再抽出一個中間層)。