Netty之私有协议栈开发(五)

2021SC@SDUSC

今天的这一篇博客主要对我上次留下的部分,也就是可靠性设计,进行详细解释。
这里主要涉及下面这几个方面:

  1. 握手和安全认证
  2. 心跳机制
  3. 重连机制

这几个方面其实还是比较大的,其中包括心跳机制其实是由一位组员专门进行源码分析的。这一部分的内容我还是主要参考老师推荐的《Netty权威指南》这本书,来进行学习分析。

目录

握手和安全认证

心跳机制

重连机制

小结


握手和安全认证

首先来看握手和安全认证。

握手的发起是在客户端和服务端TCP链路建立成功通道激活时,握手消息的接入和安全认证在服务端处理。

这里书中提供了两段实现的源码,我们先来分别看一下:

首先开发一个握手认证的客户端ChannelHandler,用于在通道激活时发起握手请求,具体代码实现如下:


public class LoginAuthReqHandler extends ChannelInboundHandlerAdapter {


    private static final Logger LOGGER = LoggerFactory.getLogger(LoginAuthReqHandler.class);


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        LOGGER.info("通道激活,握手请求认证..................");

        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 != ResultType.SUCCESS.value()) {
                ctx.close();
            } else {
                System.out.println("Login is OK : " + message);
                ctx.fireChannelRead(msg);
            }
        } else {
            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);
    }
}

我们先关注channelActive:

@Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        LOGGER.info("通道激活,握手请求认证..................");

        ctx.writeAndFlush(buildLoginReq());
    }

当客户端跟服务端TCP三次握手成功之后,由客户端构造握手请求消息发送给服务端,由于采用IP白名单认证机制,因此,不需要携带消息体,消息体为空,消息类型为3即握手请求消息。握手请求发送之后,按照协议规范,服务端需要返回握手应答消息。

再来看第二个方法channelRead:

 @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 != ResultType.SUCCESS.value()) {
                ctx.close();
            } else {
                System.out.println("Login is OK : " + message);
                ctx.fireChannelRead(msg);
            }
        } else {
            ctx.fireChannelRead(msg);
        }
    }

这里对握手应答消息进行处理,首先判断消息是否是握手应答消息,如果不是,直接透传给后面的ChannelHandler进行处理,和我们本次要处理的消息没有关系。如果是握手应答消息,则对应答结果进行判断,如果非0,说明认证失败,那么就关闭链路,重新发起连接。


下面看服务端的握手接入和安全认证代码  LoginAuthRespHandler:


public class LoginAuthRespHandler extends ChannelInboundHandlerAdapter {

    private static final Logger LOGGER = LoggerFactory.getLogger(LoginAuthRespHandler.class);

    /**
     * 考虑到安全,链路的建立需要通过基于IP地址或者号段的黑白名单安全认证机制,本例中,多个IP通过逗号隔开
     */
    private Map<String, Boolean> nodeCheck = new ConcurrentHashMap<String, Boolean>();
    private String[] whitekList = { "127.0.0.1", "192.168.56.1" };

    @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)) {
                LOGGER.error("重复登录,拒绝请求!");
                loginResp = buildResponse(ResultType.FAIL);
            } 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(ResultType.SUCCESS) : buildResponse(ResultType.FAIL);
                if (isOK)
                    nodeCheck.put(nodeIndex, true);
            }
            LOGGER.info("The login response is : {} body [{}]",loginResp,loginResp.getBody());
            ctx.writeAndFlush(loginResp);
        } else {
            ctx.fireChannelRead(msg);

        }


    }

    /**
     * 服务端接到客户端的握手请求消息后,如果IP校验通过,返回握手成功应答消息给客户端,应用层成功建立链路,否则返回验证失败信息。消息格式如下:
     * 1.消息头的type为4
     * 2.可选附件个数为0
     * 3.消息体为byte类型的结果,0表示认证成功,1表示认证失败
     */
    private NettyMessage buildResponse(ResultType result) {
        NettyMessage message = new NettyMessage();
        Header header = new Header();
        header.setType(MessageType.LOGIN_RESP.value());
        message.setHeader(header);
        message.setBody(result.value());
        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);    }
}

首先我们会看到两个数据结构:

 private Map<String, Boolean> nodeCheck = new ConcurrentHashMap<String, Boolean>();
    private String[] whitekList = { "127.0.0.1", "192.168.56.1" };

这两行是分别定义了重复登录保护和IP认证白名单列表,主要用于提升握手的可靠性。

再来看服务器端的 channelRead的实现:

首先根据客户端的源地址进行重复登录判断,如果客户端已经登录成功,拒绝重复登录,以防止由于客户端重复登录导致的其他问题。

随后通过ChannelHandlerContext 的 Channel接口获取客户端的InetSocketAddress地址,从中取得发送方的源地址信息,通过源地址进行白名单校验,校验通过握手成功,否则握手失败。

最后通过buildResponse构造握手应答消息返回给客户端。
当发生异常关闭链路的时候,会进入最后的异常处理代码,将客户端的信息从登录注册表中去注册,以保证后续客户端可以重连成功。

这里我在查阅资料的时候,在别的博客中发现了几个很好的图示表示:

心跳机制

什么是心跳消息呢?

心跳消息的目的是为了检测链路的可用性。

客户端收到登录成功的消息后,也会将消息传递下去,然后就会进行我们的心跳检测机制了。

握手成功之后,客户端主动发送心跳消息,服务端接收到心跳消息之后,返回心跳应答消息。由于心跳消息的目的是为了检测链路的可用性,因此不需要携带消息体。

下面我们先根据这个流程图,来阅读源代码:

 

客户端发送:

public class HeartBeatReqHandler extends ChannelInboundHandlerAdapter {

    private static final Logger LOGGER = LoggerFactory.getLogger(HeartBeatReqHandler.class);

    private volatile ScheduledFuture<?> heartBeat;

    @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()) {
            heartBeat = ctx.executor().scheduleAtFixedRate(new HeartBeatReqHandler.HeartBeatTask(ctx), 0, 5000,
                    TimeUnit.MILLISECONDS);
        } else if (message.getHeader() != null && message.getHeader().getType() == MessageType.HEARTBEAT_RESP.value()) {
            LOGGER.info("Client receive server heart beat message : ---> {}", message);
        } else
            ctx.fireChannelRead(msg);

    }


    private class HeartBeatTask implements Runnable {

        private final ChannelHandlerContext ctx;

        public HeartBeatTask(final ChannelHandlerContext ctx) {
            this.ctx = ctx;
        }

        @Override
        public void run() {
            NettyMessage heatBeat = buildHeatBeat();
            LOGGER.info("Client send heart beat messsage to server : ---> {}", heatBeat);
            ctx.writeAndFlush(heatBeat);
        }

        private NettyMessage buildHeatBeat() {
            NettyMessage message = new NettyMessage();
            Header header = new Header();
            header.setType(MessageType.HEARTBEAT_REQ.value());
            message.setHeader(header);
            return message;
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        //断连期间,心跳定时器停止工作,不再发送心跳请求信息
        if (heartBeat != null) {
            heartBeat.cancel(true);
            heartBeat = null;
        }
        ctx.fireExceptionCaught(cause);    }
}

如下客户端在登录成功后就会启动一个定时指定的任务,这里每个10秒钟就会发送一个心跳消息给服务端,其发送心跳的任务是一个内部类HeartBeatTask进行实现的:

    private volatile ScheduledFuture<?> heartBeat;
          heartBeat = ctx.executor().scheduleAtFixedRate(
                 new HeartBeatReqHandler.HeartBeatTask(ctx), 0, 5000,
                    TimeUnit.MILLISECONDS);

 当握手成功之后,握手请求Handler会继续将握手成功消息向下透传,HeartBeatReqHandler接收到之后对消息进行判断,如果是握手成功消息,则启动无限循环定时器用于定期发送心跳消息,这也就是我们上面解释的那一部分。由于 NioEventLoop是一个schedule,这个我们组的另一位同学专门进行过任务队列的分析,因此它支持定时器的执行。心跳定时器的单位是毫秒,默认为5000,那么就是每5秒发送一条心跳消息。心跳定时器HeartBeatTask的实现很简单,通过构造函数获取ChannelHandlerContext,构造心跳消息并发送。

服务端处理:


public class HeartBeatRespHandler extends ChannelInboundHandlerAdapter {

    private static final Logger LOGGER = LoggerFactory.getLogger(HeartBeatRespHandler.class);

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        NettyMessage message = (NettyMessage) msg;

        // 判断是否 是心跳检测消息
        if (message.getHeader() != null && message.getHeader().getType() ==
                MessageType.HEARTBEAT_REQ.value()) {

            LOGGER.info("Receive client heart beat message : ---> {} " ,message);
            NettyMessage heartBeat = buildHeatBeat();
            LOGGER.info("Send heart beat response message to client : ---> {}" ,heartBeat);
            ctx.writeAndFlush(heartBeat);
        } else {
            ctx.fireChannelRead(msg);
        }
    }

    // 生成心跳检测消息
    private NettyMessage buildHeatBeat() {
        NettyMessage message = new NettyMessage();
        Header header = new Header();
        header.setType(MessageType.HEARTBEAT_RESP.value());
        message.setHeader(header);
        return message;
    }

}


服务端的心跳Handler 接收到心跳请求消息之后,构造心跳应答消息返回,并打印接收和发送的心跳消息。


这里我们涉及到一个心跳超时问题。

那么要是我们客户端和服务端之间的连接有问题,心跳报文发送有问题,那会怎么处理?

这里我们在客户端和服务端的实现上都添加了超时检测的ReadTimeoutHandler,该Handler是由Netty为我们提供的,如果在设置时间段内都没有数据读取了,那么就引发超时,然后关闭当前的Channel。

如果是客户端,重新发起连接。

如果是服务端,释放资源,清除客户端登录缓存信息,等待客户端重连。这里清楚缓存信息是很必要的,否则会由于登陆保护,而使重连失败。

我们先来看一下ReadTimeoutHandler,它都是IdleStateHandler的子类。

 我们继续看一下IdleStateHandler。首先看一下这个IdleStateHandler的参数:第一个参数设置未读时间,第二个参数设置为未写时间,第三个为都未进行操作的时间:

IdleStateHandler也是一种Handler,所以也是在启动时添加到ChannelPipeline管道中的,当有读写操作时消息在其中传递进行检查 。

其中的channelActive()方法在socket通道建立时被触发,channelActive()方法调用Initialize()方法,根据配置的readerIdleTime超时事件参数往任务队列taskQueue中添加定时任务task:

任务添加到对应线程EventLoopExecutor对应的任务队列taskQueue后,在对应线程的run()方法中循环执行。

主要步骤是用当前时间减去最后一次channelRead方法调用的时间判断是否空闲超时,如果空闲超时则创建空闲超时事件并传递到channelPipeline中,并且触发超时事件执行自定义userEventTrigger()方法,关闭连接:

public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    if (evt instanceof IdleStateEvent){
        IdleStateEvent event = (IdleStateEvent)evt;
        if (event.state() == IdleState.WRITER_IDLE){
            ctx.close();
        }
    } else {
        super.userEventTriggered(ctx, evt);
    }
}


 

重连机制

我们在客户端和服务端在一定在时间内未能读取到数据的话,就会关闭当前的Channel,关闭连接后。

当出现断连事件之后:

客户端:释放资源,重新发起连接。

首先监听网络断连事件,如果Channel关闭,则执行后续的重连任务,通过 Bootstrap重新发起连接,客户端挂在 closeFuture 上监听链路关闭信号,一旦关闭,则创建重连定时器,5s 之后重新发起连接,直到重连成功。

这部分我在源代码中没有翻到,因此直接讲书上的代码截图过来:

服务端:因为我们上面也解释过服务端是由登陆保护,不允许重复登陆的,因此服务端感知到断连事件之后,需要清空缓存的登录认证注册信息,以保证后续客户端能够正常重连。

小结

       这一部分的内容分析到这里就先结束了,这一块主要分析了握手和安全认证。其中安全认证分析的比较少,只提到了那两个数据结构,对于心跳消息和心跳超时的代码分析的比较多,这块也和我最开始分析的EventLoop关联比较大。我在分析这部分的时候,感觉因为前面组件分析的比较仔细,因此后面的调用的时机部分,我还有一点原来是这样的感觉,在前面分析的那些比较原理,通过后面的应用才知道怎么用,优点在哪里。

     最后感谢我们的指导老师,经常询问我们需不需要帮助,在我向老师求助的时候,老师也总能提醒我,让我知道应该分析的方向。最开始我打算先分析Netty的线程的框架,感觉这部分比较的关键,但是老师指出了这部分虽然比较关键,但是我们现在比较难掌握,因此还是建议我从小组件入手。我也慢慢找到了分析的感觉和方法。最后再次感谢戴老师和我们组的指导老师们对我们的帮助,让我们对这个源码从逐渐上手,到可以越来越熟练的分析。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值