協議棧功能概述
1、異步通信,基於Netty的NIO
2、提供消息的編解碼
3、提供基於IP地址的白名單接入認證機制
4、鏈路的有效性校驗機制
5、鏈路的斷連重連機制
通信模型
1、客戶端發送握手請求消息,攜帶節點ID等有效身份認證信息
2、服務端對握手請求消息進行合法性校驗,包括節點ID有效性校驗、節點重複登錄校驗和IP合法性校驗,校驗通過後,返回登錄成功的握手應答消息
3、鏈路建立成功後,客戶端發送業務消息
4、服務端發送心跳消息
5、客戶端發送心跳消息
6、服務端發送業務消息
7、服務端退出後,服務端關閉連接,客戶端感知對方關閉連接後,被動關閉客戶端連接
客戶端服務端建立通信鏈路後,心跳採用Ping-Pong機制,當鏈路空閒時,客戶端主動發送Ping消息給服務端,服務端在接收到Ping消息後,發送Pong消息應發給客戶端,如果客戶端連續發送N條Ping消息都沒有接收到服務端返回的Pong消息,說明鏈路已經掛死或者對方處於異常狀態,客戶端主動關閉連接,間隔週期T後發起重連操作,知道重連成功。
消息定義
NettyMessage.java
Header.java
MessageType.java
public enum MessageType {
SERVICE_REQ((byte) 0), SERVICE_RESP((byte) 1), ONE_WAY((byte) 2),
LOGIN_REQ((byte) 3), LOGIN_RESP((byte) 4), HEARTBEAT_REQ((byte) 5), HEARTBEAT_RESP((byte) 6);
private byte value;
private MessageType(byte value) {
this.value = value;
}
public byte value() {
return this.value;
}
}
協議支持的字段類型
編解碼規範
編碼
NettyMessageEncoder.java
public final class NettyMessageEncoder extends MessageToByteEncoder<NettyMessage> {
MarshallingEncoder marshallingEncoder;
public NettyMessageEncoder() throws IOException {
this.marshallingEncoder = new MarshallingEncoder();
}
@Override
protected void encode(ChannelHandlerContext ctx, NettyMessage msg, ByteBuf sendBuf) throws Exception {
if (msg == null || msg.getHeader() == null)
throw new Exception("The encode message is null");
sendBuf.writeInt((msg.getHeader().getCrcCode()));
sendBuf.writeInt((msg.getHeader().getLength()));
sendBuf.writeLong((msg.getHeader().getSessionID()));
sendBuf.writeByte((msg.getHeader().getType()));
sendBuf.writeByte((msg.getHeader().getPriority()));
sendBuf.writeInt((msg.getHeader().getAttachment().size()));
String key = null;
byte[] keyArray = null;
Object value = null;
for (Map.Entry<String, Object> param : msg.getHeader().getAttachment()
.entrySet()) {
key = param.getKey();
keyArray = key.getBytes("UTF-8");
sendBuf.writeInt(keyArray.length);
sendBuf.writeBytes(keyArray);
value = param.getValue();
marshallingEncoder.encode(value, sendBuf);
}
key = null;
keyArray = null;
value = null;
if (msg.getBody() != null) {
marshallingEncoder.encode(msg.getBody(), sendBuf);
} else
sendBuf.writeInt(0);
sendBuf.setInt(4, sendBuf.readableBytes() - 8);
}
}
解碼
NettyMessageDecoder.java
/**
* 繼承自LengthFieldBasedFrameDecoder,支持自動的TCP粘包半包處理
*/
public class NettyMessageDecoder extends LengthFieldBasedFrameDecoder {
MarshallingDecoder marshallingDecoder;
public NettyMessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength) throws IOException {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength);
marshallingDecoder = new MarshallingDecoder();
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf frame = (ByteBuf) super.decode(ctx, in);
if (frame == null) {
return null;
}
NettyMessage message = new NettyMessage();
Header header = new Header();
header.setCrcCode(frame.readInt());
header.setLength(frame.readInt());
header.setSessionID(frame.readLong());
header.setType(frame.readByte());
header.setPriority(frame.readByte());
int size = frame.readInt();
if (size > 0) {
Map<String, Object> attch = new HashMap<String, Object>(size);
int keySize = 0;
byte[] keyArray = null;
String key = null;
for (int i = 0; i < size; i++) {
keySize = frame.readInt();
keyArray = new byte[keySize];
frame.readBytes(keyArray);
key = new String(keyArray, "UTF-8");
attch.put(key, marshallingDecoder.decode(frame));
}
keyArray = null;
key = null;
header.setAttachment(attch);
}
if (frame.readableBytes() > 4) {
message.setBody(marshallingDecoder.decode(frame));
}
message.setHeader(header);
return message;
}
}
鏈路的建立
服務調用方爲客戶端,服務被調用方爲服務端
LoginAuthReqHandler.java
/**
* 基於IP白名單的登錄認證請求處理和服務端應答處理
*/
public class LoginAuthReqHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(buildLoginReq());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyMessage message = (NettyMessage) msg;
// 如果是握手應答消息,需要判斷是否認證成功
if (message.getHeader() != null
&& message.getHeader().getType() == MessageType.LOGIN_RESP.value()) {
byte loginResult = (byte) message.getBody();
//握手應答消息沒有消息體
if (loginResult != (byte) 0) {
// 握手失敗,關閉連接
ctx.close();
} else {
//握手成功,將消息傳給後面的ChannelHandler處理
System.out.println("Login is ok : " + message);
ctx.fireChannelRead(msg);
}
} else
//如果不是握手請求,將消息傳給後面的ChannelHandler處理
ctx.fireChannelRead(msg);
}
private NettyMessage buildLoginReq() {
NettyMessage message = new NettyMessage();
Header header = new Header();
header.setType(MessageType.LOGIN_REQ.value());
message.setHeader(header);
return message;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
}
}
LoginAuthRespHandler.java
/**
* 認證應答和客戶端請求認證處理
*/
public class LoginAuthRespHandler extends ChannelInboundHandlerAdapter {
/**
* 重複登錄保護
*/
private Map<String, Boolean> nodeCheck = new ConcurrentHashMap<String, Boolean>();
/**
* 白名單
*/
private String[] whitekList = { "127.0.0.1", "192.168.1.104" };
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyMessage message = (NettyMessage) msg;
// 接入認證邏輯。如果是握手請求消息,處理,其它消息透傳
if (message.getHeader() != null
&& message.getHeader().getType() == MessageType.LOGIN_REQ.value()) {
String nodeIndex = ctx.channel().remoteAddress().toString();
NettyMessage loginResp = null;
// 重複登陸,拒絕,防止客戶端由於重複登錄導致的句柄泄露
if (nodeCheck.containsKey(nodeIndex)) {
loginResp = buildResponse((byte) -1);
} else {
//獲取發送方的原地址
InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
String ip = address.getAddress().getHostAddress();
boolean isOK = false;
for (String WIP : whitekList) {
if (WIP.equals(ip)) {
isOK = true;
break;
}
}
loginResp = isOK ? buildResponse((byte) 0)
: buildResponse((byte) -1);
if (isOK)
nodeCheck.put(nodeIndex, true);
}
System.out.println("The login response is : " + loginResp
+ " body [" + loginResp.getBody() + "]");
ctx.writeAndFlush(loginResp);
} else {
ctx.fireChannelRead(msg);
}
}
private NettyMessage buildResponse(byte result) {
NettyMessage message = new NettyMessage();
Header header = new Header();
header.setType(MessageType.LOGIN_RESP.value());
message.setHeader(header);
message.setBody(result);
return message;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
//異常關閉鏈路,需要移除發送方的地址信息,保證後續客戶端可以重連
nodeCheck.remove(ctx.channel().remoteAddress().toString());
ctx.close();
ctx.fireExceptionCaught(cause);
}
}
鏈路的關閉
哪些場景需要關閉鏈路
1、當對方宕機或者重啓,會主動關閉鏈路,另一方讀取到操作系統的通知信號,需要關閉資源,釋放自身的句柄資源,由於採用TCP全雙工通信,通信雙方都需要關閉連接,釋放資源。
2、消息讀寫過程中,發生IO異常,需要主動關閉鏈路
3、心跳消息讀寫過程中發生IO異常,需要主動關閉鏈路
4、心跳超時,需要主動關閉連接
5、發生編碼異常等不可恢復錯誤時,需要主動關閉連接
可靠性設計
爲了保證能夠在極端環境下協議棧依然可以正常工作或者自動恢復,需要對它的可靠性進行統一的規劃和設計
1、心跳機制
在網絡空閒時採用心跳機制來檢測鏈路的互通性,一旦發生網絡故障立即關閉鏈路。
2、重連機制
3、重複登錄保護
4、消息緩存重發
安全性設計
如果暴露在公網中,需要更嚴格的安全認證機制,例如基於密鑰和AES加密的用戶名+密碼認證機制,也可以採用SSL/TSL安全傳輸。
可擴展性設計
預留的attachment字段,可選
協議棧開發
數據結構定義
消息編解碼
握手和安全認證
心跳檢測機制
斷連重連
客戶端
服務端
這篇文章是這個系列的第六篇筆記。由於是自學netty,沒有在工作中實踐netty,所以代碼需要自己敲、運行、調試,概念性的東西也需要記筆記,反覆讀和思考。目的是讓以後寫網絡編程使用netty的時候,能夠花儘可能少的時間從頭學基礎,只需要回過頭來複習一下這些文章就可以直接根據項目需要編碼。