Netty编程之基于Netty的私有协议设计与开发

通信协议从广义上来区分,可以分为公有协议和私有协议。由于私有协议的灵活性,它往往会在某个公司或者组织内部使用,按需定制,因因如此,升级起来会非常方便,灵活性较好。本博客基于《Netty 权威指南》,设计并实现私有协议。

1. 什么是私有协议

私有协议本质上是厂商内部发展和采用的标准,除非授权,其他厂商一般无权使用该协议。私有协议也称非标准协议,就是未经国际或国家标准化组织采纳或批准,由某个企业自己制订,协议实现细节不愿公开,只在企业自己生产的设备之间使用的协议。私有协议具有封闭性、垄断性、排他性等特点。如果网上大量存在私有(非标准)协议,现行网络或用户一旦使用了它,后进入的厂家设备就必须跟着使用这种非标准协议,才能够互连互通,否则根本不可能进入现行网络。这样,使用非标准协议的厂家就实现了垄断市场的愿望。

在传统的Java应用中,通常使用以下4种方式进行跨节点通信。

  • 通过RMI进行远程服务调用;
  • 通过Java的Socket+Java序列化的方式进行跨节点调用;
  • 利用一些开源的RPC框架进行远程服务调用,例如Facebook的Thrift,Apache的Avro等;
  • 利用标准的公有协议进行跨节点服务调用,例如HTTP+XML、RESTful+JSON或者WebService。

跨节点的远程服务调用,除了链路层的物理连接外,还需要对请求和响应消息进行编解码。在请求和应答消息本身以外,也需要携带一些其他控制和管理类指令,例如链路建立的握手请求和响应消息、链路检测的心跳消息等。当这些功能组合到一起之后,就会形成私有协议。

2. Netty私有协议功能设计与开发

2.1. Netty私有协议功能

本博客中介绍的基于Netty的私有协议主要有以下5个功能:

  • 基于Netty的NIO通信框架,提供高性能的异步通信能力;
  • 提供消息的编解码框架,可以实现POJO的序列化和反序列化;
  • 提供基于IP地址的白名单接入认证机制;
  • 链路的有效性校验机制;
  • 链路的断连重连机制。

2.2. Netty私有协议通信模型

本文设计的私有协议通信过程如下:

  1. Netty协议栈客户端发送握手请求消息,携带节点ID等有效身份认证信息;
  2. Netty协议栈服务端对握手请求消息进行合法性校验,包括节点ID有效性校验、节点重复登录校验和IP地址合法性校验,校验通过后,返回登录成功的握手应答消息;
  3. 链路建立成功之后,客户端发送业务消息;
  4. 链路成功之后,服务端发送心跳消息;
  5. 链路建立成功之后,客户端发送心跳消息;
  6. 链路建立成功之后,服务端发送业务消息;
  7. 服务端退出时,服务端关闭连接,客户端感知对方关闭连接后,被动关闭客户端连接。

本文的私有协议通信模型如下图所示:
在这里插入图片描述

2.3. Netty消息定义与实现

本文的消息定义与博客中第二个案例定义相同,在这里就不再介绍。

2.4. 握手和安全验证设计与实现

2.4.1. 功能设计

考虑到安全,链路建立需要通过基于IP地址或者号段的黑白名单安全认证机制,作为阳历,本协议使用IP地址的安全认证机制,如果有多个IP,通过逗号进行分割。

客户端与服务器链路建立成功之后,由客户端发送握手请求消息,握手请求消息的定义如下:

  • 消息头的type字段值为3;
  • 可选附件个数为0;
  • 消息体为空;
  • 握手消息的长度为22个字节。

服务端接受客户端的握手请求消息之后,如果IP校验中国,返回握手成功的应答消息给客户端,应用层链路建立成功之后,握手应答消息定义如下:

  • 消息头的type字段值为4;
  • 可选附件个数为0;
  • 消息体为byte类型的结果,0表示认证成功,-1表示认证失败

链路建立连接之后,客户端和服务端就可以互相发送消息。下面将分客户端和服务端分别介绍实现代码。

2.4.2. 客户端实现代码

package netty.protocol.client;

import io.netty.channel.*;

import netty.protocol.MessageType;
import netty.protocol.struct.Header;
import netty.protocol.struct.NettyMessage;

/**
 * created by LMR on 2020/5/23
 */
public class LoginAuthReqHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
    //客户端激活时就向服务端发送连接请求
        ctx.writeAndFlush(buildLoginReq());
    }
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        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 {
                System.out.println("Login is ok : " + message);
                //传递给下一个handler
                ctx.fireChannelRead(msg);
            }
        } else
        //传递给下一个handler
            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;
    }

    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        ctx.fireExceptionCaught(cause);
    }
}

2.4.3. 服务端实现代码

package netty.protocol.server;

import io.netty.channel.*;

import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import netty.protocol.MessageType;
import netty.protocol.struct.Header;
import netty.protocol.struct.NettyMessage;

/**
 1. created by LMR on 2020/5/23
 */
public class LoginAuthRespHandler extends ChannelInboundHandlerAdapter {


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

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        NettyMessage message = (NettyMessage) msg;

        // 如果是握手请求消息,处理,其它消息透传
        if (message.getHeader() != null && message.getHeader().getType() == MessageType.LOGIN_REQ.value()) {
        //获取请求的ip地址
            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;
    }

    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        nodeCheck.remove(ctx.channel().remoteAddress().toString());// 删除缓存
        ctx.close();
        ctx.fireExceptionCaught(cause);
    }
}

相较于客户端的diamagnetic,服务端现得复杂得多,这是由于我们需要在服务端进行安全认证。与上面设计得一样,我们设置了重复登陆检查和白名单检查。成功连接标识为0,失败则标识为-1。

2.5. 心跳机制设计与实现

2.5.1 功能设计

心跳检测是为了防止网络状况波动,网络通信失败,影响正常得业务。具体的设计思路如下:

  1. 连续时间T没有读写消息时,客户端主动发送心跳信息给服务端;
  2. 如果下一个周期T到来时,客户端还没有收到服务端发送来得心跳消息或者读写消息,则认为心跳失败,进行计数;
  3. 客户端如果收到消息,则将心跳失败计数置0.如果连续N次没有收到服务端应答,则关闭链路,等待一段时间再发起重连;
  4. 服务端连续T时间没有收到消息,失败计数加1,收到消息就置0;
  5. 服务端连续N次没有收到消息,则关闭链路,释放资源,等待客户端重连。

2.5.2. 客户端代码

package netty.protocol.client;

import io.netty.channel.ChannelHandlerContext;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import netty.protocol.MessageType;
import netty.protocol.struct.Header;
import netty.protocol.struct.NettyMessage;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * created by LMR on 2020/5/23
 */
public class HeartBeatReqHandler extends ChannelInboundHandlerAdapter {


    private volatile ScheduledFuture<?> heartBeat;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        NettyMessage message = (NettyMessage) msg;
        // 握手成功,主动发送心跳消息
        if (message.getHeader() != null && message.getHeader().getType() == MessageType.LOGIN_RESP.value()) {
            heartBeat = ctx.executor().scheduleAtFixedRate(new HeartBeatTask(ctx), 0, 5000, TimeUnit.MILLISECONDS);
        } else if (message.getHeader() != null && message.getHeader().getType() == MessageType.HEARTBEAT_RESP.value()) {
            System.out.println("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();
            System.out.println("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);
    }
}

在客户端代码中如果登陆验证成功,会创建一个线程来定时发送心跳消息。在HeartBeatTask 得run方法中,会创建一个心跳NettyMessage消息,用于心跳验证,然后调用传入得ChannelHandlerContext 对象将消息传递给服务端。

2.5.3. 服务端代码

package netty.protocol.server;

import io.netty.channel.ChannelHandlerContext;
import netty.protocol.MessageType;
import netty.protocol.struct.Header;
import netty.protocol.struct.NettyMessage;
import io.netty.channel.ChannelInboundHandlerAdapter;


/**
 * created by LMR on 2020/5/23
 */
public class HeartBeatRespHandler extends ChannelInboundHandlerAdapter {


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        NettyMessage message = (NettyMessage) msg;
        // 返回心跳应答消息
        if (message.getHeader() != null && message.getHeader().getType() == MessageType.HEARTBEAT_REQ.value()) {
            System.out.println("Receive client heart beat message : ---> " + message);
            NettyMessage heartBeat = buildHeatBeat();
            System.out.println("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;
    }

}

服务端得新体哦啊检测十分简单,接收到心跳消息之后,构造心跳应答消息返回,并打印接受和发送得心跳消息。

心跳超时得实现,我们直接利用Netty得ReadTimeouthandler机制来实现,当一定周期内(默认50s)没有读取到对方得任何消息时,需要主动关闭链路,如果是客户端需要自己主动重连。如果是服务端则释放资源等待客户端重连。

2.6. 断连重连

重连机制主要是在客户端实现,当发现连接断开是就需要进行重连,在这里我们是客户端启动类使用一个线程池来重新连接

 executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        try {
                            connect(NettyConstant.PORT, NettyConstant.REMOTEIP);// 发起重连操作
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });

2.7. 启动类实现

2.7.1. 服务端启动类

package netty.protocol.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.io.IOException;

import io.netty.handler.timeout.ReadTimeoutHandler;
import netty.protocol.codec.NettyMessageDecoder;
import netty.protocol.codec.NettyMessageEncoder;

/**
 * created by LMR on 2020/5/23
 */
public class NettyServer {

    public static void main(String[] args) throws Exception {
        new NettyServer().bind(8080);
    }

    public void bind(int port) throws Exception {
        // 配置服务端的NIO线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 100)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch)
                            throws IOException {
                        ch.pipeline().addLast(
                                //自定义消息解码器
                                new NettyMessageDecoder(1024 * 1024, 4, 4));
                        //自定义消息编码器
                        ch.pipeline().addLast(new NettyMessageEncoder());
                        //处理超时
                        ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));
                        //用户验证
                        ch.pipeline().addLast(new LoginAuthRespHandler());
                        ch.pipeline().addLast("ServerHandler", new ServerHandler());
                        //心跳检测
                        ch.pipeline().addLast("HeartBeatHandler", new HeartBeatRespHandler());


                    }
                });

        // 绑定端口,同步等待成功
        b.bind(port).sync();
        System.out.println("Netty server start ok : " + port);
    }

}

2.7.2. 客户端启动类

package netty.protocol.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.ReadTimeoutHandler;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import netty.protocol.NettyConstant;
import netty.protocol.codec.NettyMessageDecoder;
import netty.protocol.codec.NettyMessageEncoder;


/**
 * created by LMR on 2020/5/23
 */
public class NettyClient {

    private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

    EventLoopGroup group = new NioEventLoopGroup();

    public void connect(int port, String host) throws Exception {
        // 配置客户端NIO线程组
        try {
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch)
                                throws Exception {
                            ch.pipeline().addLast(new NettyMessageDecoder(1024 * 1024, 4, 4));
                            ch.pipeline().addLast("MessageEncoder", new NettyMessageEncoder());
                            ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));
                            ch.pipeline().addLast("LoginAuthHandler", new LoginAuthReqHandler());
                            ch.pipeline().addLast("ClientHandler", new ClientHandler());
                            ch.pipeline().addLast("HeartBeatHandler", new HeartBeatReqHandler());

                        }
                    });
            // 发起异步连接操作
            ChannelFuture future = b.connect(new InetSocketAddress(host, port), new InetSocketAddress(NettyConstant.LOCALIP, NettyConstant.LOCAL_PORT)).sync();
            // 当对应的channel关闭的时候,就会返回对应的channel。
            future.channel().closeFuture().sync();
        } finally {
            // 所有资源释放完成之后,清空资源,再次发起重连操作
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        try {
                            connect(NettyConstant.PORT, NettyConstant.REMOTEIP);// 发起重连操作
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    public static void main(String[] args) throws Exception {
        new NettyClient().connect(NettyConstant.PORT, NettyConstant.REMOTEIP);
    }

}

在客户端与服务端启动类中的消息编解码器以及客户端和服务端的处理类均在博客中有讲解,在这里我们就不再进行重复介绍。

2.8. 运行截图

服务端截图:
在这里插入图片描述
客户端截图:
在这里插入图片描述
由于截图时间不一致,所以心跳消息不一致。



参考书籍和博客:
《Netty权威指南》

如果喜欢的话希望点赞收藏,关注我,将不间断更新博客。

希望热爱技术的小伙伴私聊,一起学习进步

来自于热爱编程的小白

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