在使用Netty服務端做消息接收服務時,有時客戶端設備不會直接給服務端發送消息,需要先向客戶端發送讀取消息的命令,然後客戶端纔會給服務端返回消息,本例中就是實現這個操作。
過程描述如下:
1、客戶端設備在首次與服務端建立連接時,將此連接的通道保存下來,存入線程安全的ConcurrentHashMap中。
2、定時遍歷ConcurrentHashMap,如果連接是存活狀態,就通過此通道發送消息給客戶端,如果不是存活狀態,就從map中刪除此通道信息。
3、正常接收客戶端傳來的信息。
public class NettyChannelService {
private static ConcurrentHashMap<String, ChannelHandlerContext> map = new ConcurrentHashMap<>();
public static Map<String, ChannelHandlerContext> getChannels() {
return map;
}
public static void saveChannel(String key, ChannelHandlerContext ctx) {
if (map == null) {
map = new ConcurrentHashMap<>();
}
map.put(key, ctx);
}
public static ChannelHandlerContext getChannel(String key) {
if (map == null || map.isEmpty()) {
return null;
}
return map.get(key);
}
public static void removeChannel(String key) {
map.remove(key);
}
}
@Component
public class NettyServerStartRollerCone implements CommandLineRunner {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Value("${tcpServerPort.rollerCone}")
private int port;
private Environment environment = SpringUtils.getBean(Environment.class);
private String frequencyMin = environment.getProperty("rollerCone.frequency_min", String.class);
@Override
public void run(String... args) {
// netty服務器初始化
Executors.newSingleThreadExecutor().submit(new NettyServerRollerCone());
// 定期向客戶端發送請求
Executors.newSingleThreadExecutor().submit(new NettyTaskRollerCone());
}
class NettyServerRollerCone implements Runnable {
@Override
public void run() {
logger.info("NettyServerRollerCone is beginning...");
// 創建線程組BossGroup,處理連接請求
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 創建線程組workerGroup,完成和客戶端具體的業務處理
// 無參:則workerGroup含有的子線程個數=NettyRuntime.availableProcessors() * 2,即CPU核數*2
EventLoopGroup workerGroup = new NioEventLoopGroup(2);
// 創建服務器端啓動對,用來配置參數
ServerBootstrap bootstrap = new ServerBootstrap();
try {
// 設置兩個線程組
bootstrap.group(bossGroup, workerGroup);
// 使用NioServerSocketChannel作爲服務器的通道
bootstrap.channel(NioServerSocketChannel.class);
// 設置線程隊列得到連接個數
bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
// 允許重複使用本地地址和端口
bootstrap.option(ChannelOption.SO_REUSEADDR, true);
// 當SO_KEEPALIVE=true的時候,服務端可以探測客戶端的連接是否還存活着,如果客戶端關閉了,那麼服務端的連接可以關閉掉,釋放資源
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
bootstrap.childHandler(
new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder", new NettyServerDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new NettyServerHandlerRollerCone());
}
});
ChannelFuture future;
try {
//綁定一個端口並啓動服務器,生成一個ChannelFuture對象 綁定:bind
future = bootstrap.bind(port).sync();
//對關閉通道進行監聽
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
logger.info("NettyServerStartRollerCone 數據異常 -> " + e.getMessage());
}
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
class NettyTaskRollerCone implements Runnable {
@Override
@SuppressWarnings("InfiniteLoopStatement")
public void run() {
logger.info("NettyTaskRollerCone is beginning...");
while (true) {
try {
for (String key : NettyChannelService.getChannels().keySet()) {
ChannelHandlerContext ctx = NettyChannelService.getChannel(key);
if (ctx == null) {
continue;
}
if (ctx.channel().isActive()) {
String reqStr = "此處是向客戶端發送的命令";
byte[] reqStrBytes = getHexBytes(reqStr);
ByteBuf reqStrByteBuf = ctx.alloc().buffer(reqStrBytes.length);
reqStrByteBuf.writeBytes(reqStrBytes);
ctx.writeAndFlush(reqStrByteBuf);
logger.info("發送請求 --> 請求地址: " + ctx.channel().remoteAddress() + " channel id: " + key);
logger.info("發送請求 --> 請求數據: " + reqStr);
// 這裏暫停一下是防止channelRead收到的數據粘包
Thread.sleep(1000);
} else {
NettyChannelService.removeChannel(key);
}
}
Thread.sleep(Integer.parseInt(frequencyMin) * 60 * 1000);
} catch (Exception e) {
logger.error("NettyTaskRollerCone發生異常: " + e.getMessage());
}
}
}
}
public byte[] getHexBytes(String str) {
str = str.replaceAll(" ", "");
byte[] bytes = new byte[str.length() / 2];
for (int i = 0; i < str.length() / 2; i++) {
String subStr = str.substring(i * 2, i * 2 + 2);
bytes[i] = (byte) Integer.parseInt(subStr, 16);
}
return bytes;
}
}
@Component
public class NettyServerHandlerRollerCone extends ChannelInboundHandlerAdapter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String uuid = ctx.channel().id().asLongText();
NettyChannelService.saveChannel(uuid, ctx);
logger.info("連接請求進入: " + uuid + " 地址: " + ctx.channel().remoteAddress());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
logger.info("返回數據 --> " + msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}