5、私有协议开发

协议栈功能概述

1、异步通信,基于Netty的NIO

2、提供消息的编解码

3、提供基于IP地址的白名单接入认证机制

4、链路的有效性校验机制

5、链路的断连重连机制

通信模型

5、私有协议开发_第1张图片

1、客户端发送握手请求消息,携带节点ID等有效身份认证信息

2、服务端对握手请求消息进行合法性校验,包括节点ID有效性校验、节点重复登录校验和IP合法性校验,校验通过后,返回登录成功的握手应答消息

3、链路建立成功后,客户端发送业务消息

4、服务端发送心跳消息

5、客户端发送心跳消息

6、服务端发送业务消息

7、服务端退出后,服务端关闭连接,客户端感知对方关闭连接后,被动关闭客户端连接

 

客户端服务端建立通信链路后,心跳采用Ping-Pong机制,当链路空闲时,客户端主动发送Ping消息给服务端,服务端在接收到Ping消息后,发送Pong消息应发给客户端,如果客户端连续发送N条Ping消息都没有接收到服务端返回的Pong消息,说明链路已经挂死或者对方处于异常状态,客户端主动关闭连接,间隔周期T后发起重连操作,知道重连成功。

消息定义

NettyMessage.java

5、私有协议开发_第2张图片

Header.java

5、私有协议开发_第3张图片

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;
    }
}

5、私有协议开发_第4张图片

 

协议支持的字段类型

5、私有协议开发_第5张图片

编解码规范

编码

NettyMessageEncoder.java

public final class NettyMessageEncoder extends MessageToByteEncoder {

    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 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 attch = new HashMap(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 nodeCheck = new ConcurrentHashMap();
    /**
     * 白名单
     */
    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的时候,能够花尽可能少的时间从头学基础,只需要回过头来复习一下这些文章就可以直接根据项目需要编码。

 

你可能感兴趣的:(netty)