实现netty服务同时监听多个端口,处理多套协议
Netty
netty一个提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
在上一篇文章中介绍了Netty怎么去实现和硬件双工通信:https://blog.csdn.net/GBK_8/article/details/123256716?spm=1001.2014.3001.5502
1. 实现Netty服务端
@Slf4j
@Component
@RefreshScope
public class NettyServerOfLims {
/**
* 端口参数,配置与配置文件中具体参数类型如:
* netty:
* port: {6001: A, 6002: B, 6003: C}
* Netty服务启动后获取到配置参数,监听配置相对应的端口
*/
@Autowired
private PortDefinition portDefinition;
ChannelFuture future = null;
NioEventLoopGroup boss = null;
NioEventLoopGroup worker = null;
ServerBootstrap bootstrap = new ServerBootstrap();
public void start() {
boss = new NioEventLoopGroup();
worker = new NioEventLoopGroup();
//1.绑定group
bootstrap.group(boss, worker)
//2.设置并绑定Reactor线程池:EventLoopGroup,EventLoop就是处理所有注册到本线程的Selector上面的Channel
.channel(NioServerSocketChannel.class)
//3.保存连接数
.option(ChannelOption.SO_BACKLOG, 1024)
//4.有数据立即发送
//.option(ChannelOption.SO_SNDBUF, 4 * 1024)
//.option(ChannelOption.SO_RCVBUF, 4 * 1024)
.handler(new LoggingHandler(LogLevel.INFO))
//5.保持连接
.childOption(ChannelOption.SO_KEEPALIVE, true)
//6.handler作用于boss -- childHandler作用于worker
.childHandler(new SocketChannelInitHandler(portDefinition.getPort()));
Map<Integer, String> ports = portDefinition.getPort();
log.info("netty服务器在{}端口启动监听", JSONObject.toJSONString(ports));
try {
/*
绑定多个端口核心代码
*/
for (Map.Entry<Integer, String> p : ports.entrySet()) {
final int port = p.getKey();
// 绑定端口
ChannelFuture future1 = bootstrap.bind(new InetSocketAddress(port)).sync();
future1.addListener(future -> {
if (future.isSuccess()) {
log.info("netty 启动成功,端口:{}", port);
} else {
log.info("netty 启动失败,端口:{}", port);
}
});
future1.channel().closeFuture().addListener((ChannelFutureListener) channelFuture -> future1.channel().close());
}
} catch (Exception e) {
log.error("netty 启动时发生异常-------{}", e);
}
}
@PreDestroy
public void stop() {
if (future != null) {
future.channel().close().addListener(ChannelFutureListener.CLOSE);
future.awaitUninterruptibly();
boss.shutdownGracefully();
worker.shutdownGracefully();
future = null;
log.info(" 服务关闭 ");
}
}
}
端口配置类
@Data
@Configuration
@ConfigurationProperties(prefix = "netty")
public class PortDefinition {
Map<Integer, String> port;
}
2. 通道初始化
@Slf4j
public class SocketChannelInitHandler extends ChannelInitializer<SocketChannel> {
/**
* 用来存储每个连接上来的设备
*/
public static final Map<ChannelId, ChannelPipeline> CHANNEL_MAP = new ConcurrentHashMap<>();
/**
* 端口信息,用来区分这个端口属于哪种类型的连接 如:6001 属于 A
*/
Map<Integer, String> ports;
public SocketChannelInitHandler(Map<Integer, String> ports) {
this.ports = ports;
}
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//每次连接上来 对通道进行保存
CHANNEL_MAP.put(socketChannel.id(), socketChannel.pipeline());
ChannelPipeline pipeline = socketChannel.pipeline();
int port = socketChannel.localAddress().getPort();
String type = ports.get(port);
//不同类型连接,处理链中加入不同处理协议
switch (type) {
case GatewayType.LIMS:
pipeline.addLast(
new AMsgDecoder(),
new AMsgEncoder(),
new AServerHandler());
break;
case GatewayType.EXPLOSION_PROOF:
//防爆
pipeline.addLast(
new BMsgDecoder(),
new BMsgEncoder(),
new BServerHandler());
break;
case GatewayType.NON_EXPLOSION_PROOF:
pipeline.addLast(
new CExpMsgDecoder(),
new CExpMsgEncoder(),
new CExpServerHandler());
break;
default:
log.error("当前网关类型并不存在于配置文件中,无法初始化通道");
break;
}
pipeline.addLast(
new StringEncoder(StandardCharsets.UTF_8),
new StringDecoder(StandardCharsets.UTF_8));
}
}
3. 创建对应的解析器和编码器
3.1 信息解析器
@Slf4j
public class AMsgDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> list) throws Exception {
//读取数据信息
byte[] data = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(data);
StringBuilder msg = new StringBuilder();
for (byte aByte : data) {
msg.append(String.format("%02x", aByte).toUpperCase());
}
if (StringUtils.isEmpty(msg.toString())) {
log.error("接收的报文为空!");
}
log.info("<----初始数据: {} ", msg);
assemblyResult(ctx, msg.toString(), list);
}
private void assemblyResult(ChannelHandlerContext ctx, String data, List<Object> list) {
List<Result> limsMessageList = analysisMessageOfLims(data);
list.addAll(pclMessage);
}
/**
* 解析报文信息
*
* @param data 未解析数据
*/
private List<Result> analysisMessageOfLims(String data) {
// 解析报文,具体解析方式跟硬件约定协议有关。
/* 对于数据包会不会出现粘包或拆包,需要根据实际情况进行处理。netty中常用的方法是根据包头上
的长度字节来判断当前包是否出现粘包或拆包
*/
}
}
3.2 信息编码器
@Slf4j
public class AMsgEncoder extends MessageToByteEncoder {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
SendData message = (SendData) o;
//拼接需要发送的数据信息
log.info("----> 初始数据【{}】 : {}", message.getCode(), msg);
//将字符类型转换成字节类型
byte[] bytes = DataTypeConvert.hexStringToBytes(msg.toString());
//写入字节缓冲区
byteBuf.writeBytes(bytes);
}
}
4. 编写服务端处理器
//继承SimpleChannelInboundHandler的好处在于可以直接拿到解析后的实体类对象。
@Slf4j
@ChannelHandler.Sharable
public class AServerHandler extends SimpleChannelInboundHandler<Result> {
private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 在解析完数据后可以得到当前设备的一些信息,如ip 唯一标示等。可以用来绑定当前通道的唯一id。
* 业务不同这个步骤操作也不同,在初始化通道的时候,用了ChannelId 来绑定对应的通道,其实是
* 可以使用远端连接ip来进行绑定的,那么这里就不用再绑定通道id一次。这就需要根据各自的具体业务了。
*/
private static final Map<String, ChannelId> CHANNEL_MAP = new ConcurrentHashMap<>();
public static Map<String, ChannelId> getChannelMap() {
return CHANNEL_MAP;
}
/**
* 当从客户端接收到一个消息时被调用
* msg 解析后的数据信息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Result message) throws Exception {
String gwCode = message.getCode();
ChannelId channelId = ctx.channel().id();
CHANNEL_MAP.put(gwMac, channelId);
/*下面就是业务模块了。需要注意的是Spring框架默认@Socpe是singleton单例的,如果你不想当前处理
器被共用那么你就该指定该类为多例或者手动去new
*/
}
/**
* 通道关闭,清除该网关记录
*
* @param gwCode 网关
*/
public static void closeChannel(String gwCode) {
CHANNEL_MAP.remove(gwCode);
}
/**
* 在与客户端的连接已经建立之后将被调用
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("新增网关设备连接成功,远端地址为:{}", ctx.channel().remoteAddress());
}
/**
* 主动发送信息
*
* @param code code
* @param msg 信息
*/
public void send(String code, String msg) {
if (CHANNEL_MAP.containsKey(code)) {
ChannelPipeline pipeline = SocketChannelInitHandler.CHANNEL_MAP.get(CHANNEL_MAP.get(code));
if (pipeline == null) {
//
SocketChannelInitHandler.CHANNEL_MAP.remove(CHANNEL_MAP.get(code));
}
assert pipeline != null;
pipeline.writeAndFlush(msg);
} else {
System.out.println("-------设备已经断开连接-------");
}
}
/**
* 客户端与服务端断开连接时调用
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
ChannelId id = ctx.channel().id();
//关闭
SocketChannelInitHandler.CHANNEL_MAP.remove(id);
log.info("网关: {} 服务端连接关闭...", ctx.channel().remoteAddress());
}
/**
* 服务端接收客户端发送过来的数据结束之后调用
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
/**
* 在处理过程中引发异常时被调用
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("remoteAddress: {},连接异常:{}", ctx.channel().remoteAddress(), cause);
}
5. Netty服务启动类
@Configuration
public class InitNettyServerConfig implements CommandLineRunner {
@Autowired
private NettyServer nettyServer;
@Autowired
public void setNettyServer(NettyServer nettyServer) {
this.nettyServer = nettyServer;
}
@Override
public void run(String... args) throws Exception {
nettyServer.start();
}
}
结尾
到此netty服务同时监听多个端口,处理多套协议实现完毕,在具体的项目中碰到过许许多多的问题,后面有时间也会编写出来,记录分享。