5、私有協議開發

協議棧功能概述

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的時候,能夠花儘可能少的時間從頭學基礎,只需要回過頭來複習一下這些文章就可以直接根據項目需要編碼。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章