一、Netty 简介
Netty 是一个基于 Java NIO 封装的高性能网络编程框架,它简化了网络编程的复杂性,提供了强大的异步事件驱动机制,广泛应用于构建高性能、可扩展的网络服务器和客户端。下面将详细介绍如何开发一个可被其他模块共用的独立 Netty 服务。
二、开发环境准备
在开始之前,你需要确保已经安装了 Java 开发环境(JDK 8 及以上),并且配置好了 Maven 或者 Gradle 项目管理工具。以 Maven 为例,在
pom.xml
中添加 Netty 依赖:<dependencies> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.87.Final</version> </dependency> </dependencies>
三、开发独立 Netty 服务
1. 服务启动类
首先创建一个服务启动类,用于初始化 Netty 服务并启动。
package com.xxx.xxx.netty.server; import com.xxx.xxx.netty.handler.heartbeat.HeartbeatChannelInitializer; import io.netty.bootstrap.ServerBootstrap; 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.nio.NioServerSocketChannel; import io.netty.handler.logging.LoggingHandler; import lombok.extern.slf4j.Slf4j; /** * TCP服务端 */ @Slf4j public class NettyTCPServer { private static final int INT_ZERO = 0; private static final int DEFAULT_BOOS_EVENT_LOOP_THREAD_NUM = 1; // 工作线程默认传入0,当传入0时取CPU逻辑核心数*2 private static final int DEFAULT_WORKER_EVENT_LOOP_THREAD_NUM = INT_ZERO; private static final String EMPTY_NAME = ""; private String name; private final int port; private int bossEventLoopGroupNum; private int workerEventLoopGroupNum; private ChannelInitializer<?> channelInitializer; public NettyTCPServer(String name, int port, int bossEventLoopGroupNum, int workerEventLoopGroupNum, ChannelInitializer<?> channelInitializer) { this.name = name; this.port = port; this.bossEventLoopGroupNum = bossEventLoopGroupNum; this.workerEventLoopGroupNum = workerEventLoopGroupNum; this.channelInitializer = channelInitializer; } public NettyTCPServer(int port) { this(EMPTY_NAME, port); } public NettyTCPServer(String name, int port) { this(name, port, DEFAULT_BOOS_EVENT_LOOP_THREAD_NUM, DEFAULT_WORKER_EVENT_LOOP_THREAD_NUM, new HeartbeatChannelInitializer(name)); } public NettyTCPServer setName(String name) { this.name = name; return this; } public NettyTCPServer setBossEventLoopGroupNum(int bossEventLoopGroupNum) { this.bossEventLoopGroupNum = bossEventLoopGroupNum; return this; } public NettyTCPServer setWorkerEventLoopGroupNum(int workerEventLoopGroupNum) { this.workerEventLoopGroupNum = workerEventLoopGroupNum; return this; } public NettyTCPServer setChannelInitializer(ChannelInitializer<?> channelInitializer) { this.channelInitializer = channelInitializer; return this; } /** * 启动服务 */ public void run() { this.validated(); EventLoopGroup bossEventLoopGroup = new NioEventLoopGroup(bossEventLoopGroupNum); EventLoopGroup workerEventLoopGroup = new NioEventLoopGroup(workerEventLoopGroupNum); try { ChannelFuture bindFuture = new ServerBootstrap().group(bossEventLoopGroup, workerEventLoopGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler()) .childHandler(channelInitializer) // 关闭Nagle算法 .childOption(ChannelOption.TCP_NODELAY, true) // 终端TCP心跳 .childOption(ChannelOption.SO_KEEPALIVE, true) .bind(port) .sync(); log.info("{}服务端启动成功:{}", name, bindFuture.channel()); ChannelFuture closeFuture = bindFuture.channel().closeFuture(); closeFuture.addListener(listener -> { if (listener.isSuccess()) { log.info("{}服务端关闭", name); } }); closeFuture.sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { workerEventLoopGroup.shutdownGracefully(); bossEventLoopGroup.shutdownGracefully(); } } /** * 参数校验 */ private void validated() { if (1024 > this.port || 49151 < this.port) { throw new RuntimeException("非法端口[" + this.port + "]"); } if (this.channelInitializer == null) { throw new RuntimeException("通道构造器不允许为空"); } if (this.bossEventLoopGroupNum <= INT_ZERO) { throw new RuntimeException("连接反应器线程数非法"); } if (this.workerEventLoopGroupNum < INT_ZERO) { throw new RuntimeException("工作反应器线程数非法"); } } }
在上述代码中,
NettyTCPServer
类负责启动 Netty 服务。EventLoopGroup
是 Netty 中的线程池,bossGroup
用于处理客户端连接请求,workerGroup
用于处理网络读写操作。ServerBootstrap
是服务端启动辅助类,通过它可以配置服务端的各种参数。
2. 客户端启动类
该客户端实现了 Netty TCP 连接的基本功能,具备可配置的通道处理器、连接状态监听和心跳机制。适用于需要长连接通信的场景(如即时通讯、物联网设备通信)。实际使用中需根据业务需求补充重连机制、异常处理和监控指标等功能,以提升系统的健壮性和可观测性。
package com.xxx.xxx.netty.client; import com.xxx.xxx.netty.handler.heartbeat.HeartbeatChannelInitializer; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; /** * NettyTCP客户端 */ @Slf4j public class NettyTCPClient { private final String host; private final int port; @Setter @Accessors(chain = true) private ChannelInitializer<?> channelInitializer; @Setter @Accessors(chain = true) private GenericFutureListener<? extends Future<? super Void>> closeListener; @Setter @Getter private EventLoopGroup eventLoopGroup; public NettyTCPClient(String host, int port, ChannelInitializer<?> channelInitializer, GenericFutureListener<? extends Future<? super Void>> closeListener, EventLoopGroup eventLoopGroup) { this.host = host; this.port = port; this.channelInitializer = channelInitializer; this.closeListener = closeListener; this.eventLoopGroup = eventLoopGroup; } public NettyTCPClient(String host, int port, ChannelInitializer<?> channelInitializer, GenericFutureListener<? extends Future<? super Void>> closeListener) { this.host = host; this.port = port; this.channelInitializer = channelInitializer; this.closeListener = closeListener; } public NettyTCPClient(String host, int port, ChannelInitializer<?> channelInitializer) { this(host, port, channelInitializer, null); } public NettyTCPClient(String host, int port) { this(host, port, new HeartbeatChannelInitializer(), null); } /** * 客户端启动 * * @param blockCurrentThread 开启客户端连接后当前调用线程是否保持阻塞 * @return 客户端channel */ public Channel run(boolean blockCurrentThread) { this.validated(); if (eventLoopGroup == null) { eventLoopGroup = new NioEventLoopGroup(); } try { ChannelFuture connectFuture = new Bootstrap().group(eventLoopGroup) .channel(NioSocketChannel.class) .remoteAddress(host, port) .handler(channelInitializer) .option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.SO_KEEPALIVE, true) .connect() .addListener((listener) -> { if (listener.isSuccess()) { log.info("连接[/{}:{}]成功", host, port); } else { log.info("连接[/{}:{}]失败", host, port); } }) .sync(); if (!connectFuture.isSuccess()) { return null; } Channel channel = connectFuture.channel(); // 添加关闭事件 ChannelFuture closeFuture = channel.closeFuture(); if (closeListener != null) { closeFuture.addListener(closeListener); } if (blockCurrentThread) { closeFuture.sync(); } return channel; } catch (InterruptedException e) { log.error("启动Netty客户端失败", e); return null; } } private void validated() { if (StringUtils.isBlank(this.host)) { throw new RuntimeException("远端主机地址不允许为空"); } if (1024 > this.port || 49151 < this.port) { throw new RuntimeException("非法远端主机端口[" + this.port + "]"); } if (this.channelInitializer == null) { throw new RuntimeException("通道构造器不允许为空"); } } }
3. 心跳处理器
package com.xxx.xxx.netty.handler.heartbeat; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.timeout.IdleStateHandler; import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.handler.timeout.WriteTimeoutHandler; import org.apache.commons.lang3.StringUtils; import java.util.concurrent.TimeUnit; /** * 心跳处理器初始化器 */ public class HeartbeatChannelInitializer extends ChannelInitializer<SocketChannel> { private static final int INT_ZERO = 0; private static final int DEFAULT_READ_TIMEOUT = 30; private static final int DEFAULT_WRITE_TIMEOUT = 30; private static final int DEFAULT_READ_WRITE_TIMEOUT = -1; private int readTimeout; private int writeTimeout; private int readOrWriteTimeout; private String handlerPrefix; public HeartbeatChannelInitializer(int readTimeout, int writeTimeout, int readOrWriteTimeout, String handlerPrefix) { this.readTimeout = readTimeout; this.writeTimeout = writeTimeout; this.readOrWriteTimeout = readOrWriteTimeout; this.handlerPrefix = StringUtils.isBlank(handlerPrefix) ? "" : handlerPrefix + "-"; } public HeartbeatChannelInitializer() { this(DEFAULT_READ_TIMEOUT, DEFAULT_WRITE_TIMEOUT, DEFAULT_READ_WRITE_TIMEOUT, null); } public HeartbeatChannelInitializer(String handlerPrefix) { this(DEFAULT_READ_TIMEOUT, DEFAULT_WRITE_TIMEOUT, DEFAULT_READ_WRITE_TIMEOUT, handlerPrefix); } public HeartbeatChannelInitializer(int readOrWriteTimeout) { this(INT_ZERO, INT_ZERO, readOrWriteTimeout); } public HeartbeatChannelInitializer(int readOrWriteTimeout, String handlerPrefix) { this(INT_ZERO, INT_ZERO, readOrWriteTimeout, handlerPrefix); } public HeartbeatChannelInitializer(int readTimeout, int writeTimeout) { this(readTimeout, writeTimeout, DEFAULT_READ_WRITE_TIMEOUT, null); } public HeartbeatChannelInitializer(int readTimeout, int writeTimeout, String handlerPrefix) { this(readTimeout, writeTimeout, DEFAULT_READ_WRITE_TIMEOUT, handlerPrefix); } public HeartbeatChannelInitializer(int readTimeout, int writeTimeout, int readOrWriteTimeout) { this(readTimeout, writeTimeout, readOrWriteTimeout, null); } public HeartbeatChannelInitializer setReadTimeout(int readTimeout) { this.readTimeout = readTimeout; return this; } public HeartbeatChannelInitializer setWriteTimeout(int writeTimeout) { this.writeTimeout = writeTimeout; return this; } public HeartbeatChannelInitializer setReadOrWriteTimeout(int readOrWriteTimeout) { this.readOrWriteTimeout = readOrWriteTimeout; return this; } public HeartbeatChannelInitializer setHandlerPrefix(String name) { this.handlerPrefix = StringUtils.isBlank(name) ? "" : name + "-"; return this; } public String getHandlerPrefix() { return handlerPrefix; } @Override protected void initChannel(SocketChannel socketChannel) { ChannelPipeline pipeline = socketChannel.pipeline(); if (readOrWriteTimeout != DEFAULT_READ_WRITE_TIMEOUT) { pipeline.addLast(this.handlerPrefix + "idleStateHandler", new IdleStateHandler(readTimeout, writeTimeout, readOrWriteTimeout, TimeUnit.SECONDS)); pipeline.addLast(this.handlerPrefix + "idleStateEventHandler", new IdleStateEventHandler()); } else { pipeline.addLast(this.handlerPrefix + "readTimeoutHandler", new ReadTimeoutHandler(readTimeout)); pipeline.addLast(this.handlerPrefix + "writeTimeoutHandler", new WriteTimeoutHandler(writeTimeout)); pipeline.addLast(this.handlerPrefix + "heartbeatHandler", new HeartbeatHandler()); } } }
package com.xxx.xxx.netty.handler.heartbeat; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.timeout.ReadTimeoutException; import io.netty.handler.timeout.WriteTimeoutException; import lombok.extern.slf4j.Slf4j; /** * 心跳处理器 */ @Slf4j @ChannelHandler.Sharable public class HeartbeatHandler extends ChannelInboundHandlerAdapter { /** * {@inheritDoc} */ @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { log.info("{} 通道注册", ctx); ctx.channel().closeFuture() .addListener(listener -> { if (listener.isSuccess()) { log.info("{} 通道关闭", ctx); } }); super.channelRegistered(ctx); } /** * {@inheritDoc} */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (cause instanceof ReadTimeoutException) { log.warn("通道[{}]长时间未发生读取事件,客户端失去连接", ctx); ctx.channel().close(); } if (cause instanceof WriteTimeoutException) { log.warn("通道[{}]长时间未发生写入事件,客户端失去连接", ctx); ctx.channel().close(); } } }
package com.xxx.xxx.netty.handler.heartbeat; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.timeout.IdleStateEvent; import lombok.extern.slf4j.Slf4j; /** * 空闲事件处理器 */ @Slf4j @ChannelHandler.Sharable public class IdleStateEventHandler extends ChannelDuplexHandler { /** * {@inheritDoc} */ @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { log.info("{} 通道注册", ctx); ctx.channel().closeFuture() .addListener(listener -> { if (listener.isSuccess()) { log.info("{} 通道关闭", ctx); } }); super.channelRegistered(ctx); } /** * {@inheritDoc} */ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object event) { if (event instanceof IdleStateEvent) { IdleStateEvent idleStateEvent = (IdleStateEvent) event; switch (idleStateEvent.state()) { case READER_IDLE: log.warn("{} 读空闲,关闭通道", ctx); ctx.channel().close(); break; case WRITER_IDLE: log.warn("{} 写空闲,关闭通道", ctx); ctx.channel().close(); break; case ALL_IDLE: log.warn("{} 读|写空闲,关闭通道", ctx); ctx.channel().close(); break; } } } }
@ChannelHandler.Sharable
是 Netty 框架中的一个注解,它主要用于标记一个 ChannelHandler
实例是否可以被多个 ChannelPipeline
共享。
四、其他服务使用Netty服务
1. 使用Netty服务启动类
package xxxxxxxxxxxxxxxx; import io.netty.channel.Channel; import io.netty.channel.ChannelId; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import lombok.Setter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; /** * xxx服务启动器 * */ @Component @EnableConfigurationProperties(XXXServerProperties.class) public class XXXStarter { public static final TierConcurrentMap<ChannelId, Short, Channel> CHANNEL_MAP = new TierConcurrentMap<>(); @Setter(onMethod_ = @Autowired) private XXXServerProperties xxxServerProperties; /** * 启动xxx服务端 */ @PostConstruct private void start() { // 启动xxx器服务端 new Thread(() -> new NettyTCPServer("xxxxx", xxxServerProperties.getPort()) .setChannelInitializer(new PagerChannelInitializer(xxxServerProperties.isOpenHeartbeat(), xxxServerProperties.getMaxFrameLength(), "xxxxx") .setReadTimeout(xxxServerProperties.getReadTimeout()) .setWriteTimeout(0)) .run()) .start(); } /** * xxx通道初始化器 */ private static class XXXChannelInitializer extends HeartbeatChannelInitializer { private final boolean openHeartbeat; private final int maxFrameLength; public XXXChannelInitializer(boolean openHeartbeat, int maxFrameLength, String name) { super(name); this.openHeartbeat = openHeartbeat; this.maxFrameLength = maxFrameLength; } /** * {@inheritDoc} */ @Override protected void initChannel(SocketChannel socketChannel) { socketChannel.pipeline().addLast(this.getHandlerPrefix() + "loggingHandler", new LoggingHandler(LogLevel.INFO)); socketChannel.pipeline().addLast(this.getHandlerPrefix() + "lengthFieldBasedFrameDecoder", new LengthFieldBasedFrameDecoder(maxFrameLength, 4, 2, 0, 6)); if (this.openHeartbeat) { super.initChannel(socketChannel); } socketChannel.pipeline().addLast(this.getHandlerPrefix() + "xxxInboundHandler", new XXXInboundHandler()); socketChannel.pipeline().addLast(this.getHandlerPrefix() + "xxxInstruct2ByteBufEncoder", new XXXInstruct2ByteBufEncoder()); } } }
/** * xxx服务端配置 */ @Setter @Getter @ConfigurationProperties(prefix = "xxx.app.xxx.server") public class XXXServerProperties { /** * 服务端口号 */ private int port = 8083; /** * 是否打开心跳检测 */ private boolean openHeartbeat = true; /** * 读数据超时时间 */ private int readTimeout = 20; /** * 拆包时一帧数据最大的长度 */ private int maxFrameLength = 2048; }
2. 使用Netty客户端启动类
package xxxxxxxxxxxxxxx; import io.netty.channel.Channel; import io.netty.channel.ChannelId; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * XXX客户端通道管理器 */ @Slf4j public class XXXClientChannelManage { /** * XXXID映射channel */ public static Map<Long, Channel> MACHINE_CHANNEL_MAP = new ConcurrentHashMap<>(); /** * channel id 映射XXXID */ public static Map<ChannelId, Long> CHANNEL_MACHINE_MAP = new ConcurrentHashMap<>(); /** * 事件循环组 */ public static EventLoopGroup eventLoopGroup = new NioEventLoopGroup();; /** * 获取XXX设备连接通道 * * @param config XXX配置 * @return 连接通道 */ public static Channel getConnectionChannel(XXXConfigDTO config) { try { Channel channel = XXXClientChannelManage.MACHINE_CHANNEL_MAP.get(config.getId()); if (channel != null) { // 连接通道保持活性直接返回 if (channel.isActive()) { return channel; } // 连接通道失活则先移除池中该通道 log.info("XXX[{}-{}]已连接但失活,准备重新打开连接", config.getXXXCode(), config.getXXXName()); XXXClientChannelManage.CHANNEL_MACHINE_MAP.remove(channel.id()); XXXClientChannelManage.MACHINE_CHANNEL_MAP.remove(config.getId()); } else { log.info("XXX[{}-{}]未连接,准备打开连接", config.getXXXCode(), config.getXXXName()); } Channel clientChannel = new NettyTCPClient( config.getXXXIp(), config.getXXXPort(), new XXXInitializer(), (listener) -> { if (listener.isSuccess()) { Channel closeChannel = XXXClientChannelManage.MACHINE_CHANNEL_MAP.get(config.getId()); if (closeChannel != null) { XXXClientChannelManage.CHANNEL_MACHINE_MAP.remove(closeChannel.id()); XXXClientChannelManage.MACHINE_CHANNEL_MAP.remove(config.getId()); } } }, eventLoopGroup) .run(false); if (clientChannel.isActive()) { XXXClientChannelManage.CHANNEL_MACHINE_MAP.put(clientChannel.id(), config.getId()); XXXClientChannelManage.MACHINE_CHANNEL_MAP.put(config.getId(), clientChannel); return clientChannel; } throw new ManageException(CommonMessage.FAIL.getCode(), "打开XXX[" + config.getXXXCode() + "-" + config.getXXXName() + "]连接失败"); } catch (Exception e) { log.error("打开XXX[" + config.getXXXCode() + "-" + config.getXXXName() + "]连接失败", e); throw new ManageException(CommonMessage.FAIL.getCode(), "打开XXX[" + config.getXXXCode() + "-" + config.getXXXName() + "]连接失败"); } } }
使用Netty客户端初始化特定客户端后,可以在业务代码中穿插使用,例如执行完业务逻辑代码后,需要通过Netty写入PLC,则可按以下模式实现:
XXXClientChannelManage.getConnectionChannel(xxxConfig); XXXClientChannelManage.MACHINE_CHANNEL_MAP.get(xxxConfig.getId()).writeAndFlush(new ReadXXXRequest());
五、总结
通过以上步骤,已经成功开发了一个可被其他模块共用的独立 Netty 服务。在实际应用中,你可以根据具体需求对业务处理逻辑进行扩展,例如添加编解码器、实现更复杂的协议、初始化服务器或客户端时根据业务需求定制逻辑等。实际情况中可以根据PLC不同,走不同的初始化逻辑与读写等逻辑,希望这篇教学文章能帮助你更好地理解和使用 Netty 框架。