背景
近日需要开发一个服务去对接上游的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存储的信息进行一些健康检测操作