记一次netty数据中转服务开发

307 阅读4分钟

背景

近日需要开发一个服务去对接上游的TCP数据源,同时将数据转发给下游去处理,该服务只做前置的登录鉴权、心跳等操作。 架构如下:

  • 启动netty client去对接上游数据源
  • 启动netty server去对接下游客户端转发数据

在这里插入图片描述

netty server开发

因为上游的数据是从多个端口和ip中获取的,实际上转发给下游的时候,也要启动多个server将对应端口的数据转发给下游建立了该端口连接的客户端 在这里插入图片描述 为了增加代码的可复用性,这里我们定义一个公共的nettyServer父类,实现公共操作的统一实现,比如启动,重启等。下面以start方法为例,这里定义一个公用启动方法

        bossGroup = new NioEventLoopGroup(1);
        workGroup = new NioEventLoopGroup();
        bootstrap = new ServerBootstrap()
                .group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MILLIS)
                .handler(getChannelHandler())
                .childHandler(getChlidChannelHandler());
        addChildOption(bootstrap);
        startUp();

为了增加父类的可扩展性,这里的handler和childhandler定一个为一个子方法,供子类实现自定义的处理逻辑。 同时option方法的配置,也下放到子类可以进行扩展,这么做的主要原因是之前运行server时遇到缓存写满的问题,需要配置高低位的option,增加缓存区大小,但是每个子类所需要的缓存区大小不一致,所以下放到子类进行扩展 在这里插入图片描述 当然getChannelHandler我们也能定义为公用模板,类似于我下面的设计,在该方法中在添加一个子方法让子类去扩展,这样我们可以定义全局公用的handler,如:LoggingHandler

 public ChannelInitializer getChannelHandler() {
        return new ChannelInitializer<NioServerSocketChannel>() {
            @Override
            protected void initChannel(NioServerSocketChannel ch) throws Exception {
                ChannelPipeline pipeline = ch.pipeline();
                pipeline.addLast(new LoggingHandler(LogLevel.INFO));
                addPipeline(pipeline);
            }
        };
    }

netty client开发

有了作为服务的server,我们还需要定义client去对接上游数据源获取数据,跟server一样定一个公共的nettyClient父类,让子类进行扩展实现,与server不同的是,没有childHandler 同时需要注意,netty client所对应的channel是SocketChannel,而server的是NioServerSocketChannel,在定义公共ChannelInitializerd要注意区分

如何维护下游客户端连接

通过上面我们知道,我们会启动多个netty server去转发多个端口的数据,同时下游也有可能会有多个client连接我们获取数据,所以这里是个多server对应多client的场景 在这里插入图片描述 每个server需要维护自己的连接列表,这里我们定义一个childHandler和一个ClientManager,在处理器中产生客户端连接事件的时候,将client的信息加入到Map中,这个Map维护在Server的对象里,每个server对象独有

@Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
InetSocketAddress remoteAddress = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIP = remoteAddress.getAddress().getHostAddress();
        Client client = new Client();
        clientInfo.setIp(clientIP);
        clientInfo.setCtx(ctx);
        map.put(clientIp, ctx);
}

在这里插入图片描述

如何建立客户端-服务端数据订阅

这里有一个问题需要思考,我们的client从上游读取到数据时,我们怎么知道要从哪个server转发数据出去给到哪些client。如果client:1001 只订阅了server:1001的数据,那么我们是不能把server:1000的数据转给他的,同时server:1000 也不能转发错数据,比如转的是数据源:1001 在这里插入图片描述

订阅管理器

为了解决互相之间的关联匹配,这里我们定一个订阅管理器,用来存储服务端和上游的数据源之间的关联,因为之前我们在server定义了客户端存储器,所以server-下游client之间的订阅关系我们是已经有了,并且存在每个server中独一份 在这里插入图片描述 这里在server的handler中定一个subscribeHandler用来监听服务端连接事件,在连接事件中加入到subscriptionManager

public class ServerConnectHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
         // ...简略
        SubscriptionManager.addSubscription(name, server);
        super.channelActive(ctx);
    }
}

那么server该如何准确知道自己订阅哪个客户端呢,这里用springboot的配置文件实现

server:
  list:
   - name: server
     port: 1000
     subscribe_name: 数据源1的name

数据源1:
	name: xxxx
	ip: xxx
	port: xxx

这样我们在启动server的时候,取配置里的name进行订阅绑定,这里我是没做强校验上游是否存在的,只是单纯添加一个map的绑定关系,这么做的好处是上游即便短暂的挂了重启中,也不会导致服务启动失败 最后我们整体的一个架构图就如下 在这里插入图片描述

如何监控netty运行状态

这边开发仍然使用的springboot服务,在启动时,启动了netty的server和client,那么如何监控netty运行健康状态就变得比较困难了,下面提供一个简易思路 使用redis做注册中心,添加一个handler以及IdleStateHandler,定期向redis写入更新时间等信息,做一个简单的心跳机制

new IdleStateHandler(0, 15, 0, TimeUnit.SECONDS)

@Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            switch (event.state()) {
                case WRITER_IDLE:
                    NettyClientInfo nettyClientInfo = new NettyClientInfo();
                    nettyClientInfo.setIp(abstractNettyConnector.getServerAddress());
                    nettyClientInfo.setPort(abstractNettyConnector.getServerPort()); 
                    nettyClientInfo.setLastUpdateTime(System.currentTimeMillis());
                    redis.set(name, nettyClientInfo);
                    log.debug("name:[{}] --- ip:[{}] --- name:[{}] 注册心跳消息成功", xxx.getName(), xxx.getServerAddress(), xxx.getServerPort());
                    }
                    break;
                default:
                    break;
            }
        }
        super.userEventTriggered(ctx, evt);
    }

这样我们随便找一个定时job,定期去检测redis中的时间,根据redis存储的信息进行一些健康检测操作在这里插入图片描述