Netty 是一个高性能的 NIO 通信框架,提供异步的、事件驱动的网络编程模型。在使用 Netty 进行数据传输时,可能会遇到运行一段时间后读不到数据的情况。这种情况一般是由以下6个原因导致,重点讨论消息积压的情况:
一、原因分析
- 网络问题:首先检查网络是否正常连接,如果网络存在问题,可能会导致无法正常接收数据。
- 配置问题:检查 Netty 的配置是否正确,例如端口号、协议类型等。如果配置有误,可能会导致无法接收到数据。
- 接收方关闭连接:如果接收方主动关闭了连接,发送方将无法继续接收数据。可以检查接收方是否已关闭连接,或者检查接收方是否发送了关闭连接的信号。
- 超时设置:检查 Netty 的超时设置,例如读取超时、发送超时等。如果超时设置不合理,可能会导致无法接收到数据。
- 数据处理问题:检查数据处理逻辑是否正确,如果数据处理逻辑有误,可能会导致无法正确接收到数据。
- 消息积压:如果客户端发送数据较为频繁,且服务端处理数据时间比较长,这时候我们可以使用netty通道或者线程池来解决
二、消息积压代码示例
1.1、服务端启动类
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* 服务端
* @author syb
* @date 2023/9/6
*/
public class NettyServerDemo {
public static void main(String[] args) {
//配置连接线程组1个线程
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//配置数据读写线程组,默认线程
EventLoopGroup workGroup = new NioEventLoopGroup();
// 引导服务端的启动
ServerBootstrap serverBootstrap = new ServerBootstrap();
try {
serverBootstrap.group(bossGroup, workGroup)
// 指定IO模型为NIO,非阻塞模式
.channel(NioServerSocketChannel.class)
// 设置参数,这里设置的SO_BACKLOG,意思是客户端连接等待队列的长度为128
.option(ChannelOption.SO_BACKLOG, 128)
// SO_KEEPALIVE: 表示是否开启TCP底层心跳机制,true表示开启
//设置活动保持连接状态;
.childOption(ChannelOption.SO_KEEPALIVE, true)
// TCP_NODELAY:表示是否开启Nagle算法,true表示关闭,false表示开启
//使用TCP_NODELAY可以减少小包数量,可以禁用Nagle算法,坏处就是小包比较多,对网络交通会有负担
.childOption(ChannelOption.TCP_NODELAY, true)
//初始化连接通道
.childHandler(new ServerChannelInitializer());
//绑定本地端口号启动服务
ChannelFuture channelFuture = serverBootstrap.bind(8001).sync();
//等待服务端监听端口关闭
channelFuture.channel().closeFuture().sync();
channelFuture.addListener(future -> {
if (future.isSuccess()) {
System.out.println("ChannelFuture:关闭成功");
} else {
System.out.println("ChannelFuture:关闭失败");
}
});
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 优雅退出,释放线程池资源
workGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
1.2、服务端通道初始化
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
/**
*服务端连接通道初始化
*@author syb
*@date 2023/9/5
*/
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) {
// 基于换行符号
//channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
// 解码转String,注意调整自己的编码格式GBK、UTF-8
//channel.pipeline().addLast(new StringDecoder(Charset.forName("UTF-8")));
//这里设置下读空闲为30秒
channel.pipeline().addLast(new IdleStateHandler(30,0,0, TimeUnit.SECONDS));
// 在管道中添加我们自己的接收数据实现方法
channel.pipeline().addLast(new NettyServerHandler());
}
}
1.3、服务端handler处理类
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
*handler处理类
*@author syb
*@date 2023/9/6
*/
public class NettyServerHandler extends SimpleChannelInboundHandler {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 新的客户端连接
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Date connectTime = new Date(System.currentTimeMillis());
System.out.println("时间:"+formatter.format(connectTime)+" 连接信息:" +ctx.channel().remoteAddress().toString());
}
/**
* 断开连接
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Date stopTime = new Date(System.currentTimeMillis());
System.out.println("时间:"+formatter.format(stopTime)+" 断开连接:"+ ctx.channel().remoteAddress().toString());
}
/**
* 发生异常
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Date exceptionTime = new Date(System.currentTimeMillis());
System.out.println("时间:"+formatter.format(exceptionTime)+ ctx.channel().remoteAddress().toString()+":客户端异常断开链接"+cause.getMessage());
ctx.channel().close();
}
/**
* 读写超时
* @param ctx
* @param evt
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt){
Date eventTime = new Date(System.currentTimeMillis());
String stateMsg="";
if(evt instanceof IdleStateEvent){
IdleStateEvent idleStateEvent=(IdleStateEvent)evt;
switch (idleStateEvent.state()){
case ALL_IDLE:
stateMsg="读写空闲";
break;
case READER_IDLE:
stateMsg="读空闲";
break;
case WRITER_IDLE:
stateMsg="写空闲";
break;
}
}
System.out.println("时间:"+formatter.format(eventTime)+" IP:" + ctx.channel().remoteAddress().toString() + "stateMsg:"+stateMsg);
}
/**
* 获取客户端数据
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
Date createTime = new Date(System.currentTimeMillis());
//读取数据
ByteBuf buf=((ByteBuf)msg);
String clientMsg=buf.toString(CharsetUtil.UTF_8);
System.out.println(formatter.format(createTime)+"线程名:"+Thread.currentThread ().getName ()+" 连接信息:"+ ctx.channel().remoteAddress().toString()+" 数据内容:"+clientMsg);
try {
//休眠3分钟
Thread.sleep(180*1000);
}catch (Exception e){
e.printStackTrace();
}
}
}
2.1、客户端启动类
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
/**
* 客户端 循环启动1000个客户端并2秒向服务器发送一次数据
* @author syb
* @date 2023/9/6
*/
public class NettyClientDemo {
public static void main(String[] args) {
//启动1000个客户端
for(int i=1;i<1000;i++){
int j=10000+i;//设置端口号
new Thread(new Runnable() {
@Override
public void run() {
runClient(j);
}
}).start();
}
}
/**
* 启动客户端
* @param port
*/
public static void runClient(int port){
//区别于服务端,我们在客户端只创建了一个NioEventLoopGroup实例,
// 因为客户端你并不需要使用I/O多路复用模型,需要有一个Reactor来接受请求。只需要单纯的读写数据即可
EventLoopGroup group = new NioEventLoopGroup();
try {
//区别于服务端,我们在客户端只需要创建一个Bootstrap对象,它是客户端辅助启动类,功能类似于ServerBootstrap。
Bootstrap b = new Bootstrap();
//将NioEventLoopGroup实例绑定到Bootstrap对象中
b.group(group)
//创建Channel(典型的channel有NioSocketChannel,NioServerSocketChannel,
// OioSocketChannel,OioServerSocketChannel,EpollSocketChannel,
// EpollServerSocketChannel),区别与服务端,这里创建的是NIOSocketChannel.
.channel(NioSocketChannel.class)
//设置参数,这里设置的TCP_NODELAY为true,意思是关闭延迟发送,一有消息就立即发送,默认为false。
.option(ChannelOption.TCP_NODELAY, true)
//建立连接后的具体Handler。注意这里区别与服务端,使用的是handler()而不是childHandler()。
// handler和childHandler的区别在于,
// handler是接受或发送之前的执行器;childHandler为建立连接之后的执行器。
.handler(new ClientChannelInitializer());
//发起异步连接操作
//设置远程连接的ip和端口号
java.net.SocketAddress remoteAddress= new InetSocketAddress("127.0.0.1",8001) ;
//设置本地连接的端口号
SocketAddress localAddress =new InetSocketAddress("127.0.0.1",port) ;
ChannelFuture f = b.connect(remoteAddress, localAddress).sync();
//当代客户端链路关闭
f.channel().closeFuture().sync();
}catch (Exception e){
}
finally {
// 优雅退出,释放NIO线程组
group.shutdownGracefully();
}
}
}
2.2、客户端初始化通道
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
/**
*客户端连接通道初始化
*@author syb
*@date 2023/9/6
*/
public class ClientChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) {
// 在管道中添加我们自己的接收数据实现方法
channel.pipeline().addLast(new ClientServerHandler());
}
}
2.3、客户端handler处理类
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
import java.text.SimpleDateFormat;
import java.util.*;
/**
*客户端handler处理类
*@author syb
*@date 2023/9/6
*/
public class ClientServerHandler extends SimpleChannelInboundHandler {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 新的客户端连接 循环2秒发一次数据
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Date connectTime = new Date(System.currentTimeMillis());
System.out.println("时间:"+formatter.format(connectTime)+" 服务端连接信息:" +ctx.channel().remoteAddress().toString());
while(true){
try {
//休眠2秒
Thread.sleep(2*1000);
Date time = new Date(System.currentTimeMillis());
String date=formatter.format(time);
System.out.println("发送时间:"+date);
//向服务端发送当前时间
ctx.writeAndFlush(Unpooled.copiedBuffer(date,CharsetUtil.UTF_8));
}catch (Exception exception){
}
}
}
/**
* 断开连接
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Date stopTime = new Date(System.currentTimeMillis());
System.out.println("时间:"+formatter.format(stopTime)+" 断开连接:"+ ctx.channel().remoteAddress().toString());
}
/**
* 发生异常
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Date exceptionTime = new Date(System.currentTimeMillis());
String clientIp=ctx.channel().remoteAddress().toString().replace("/","").split(":")[0];
System.out.println("时间:"+formatter.format(exceptionTime)+clientIp+":客户端异常断开链接"+cause.getMessage());
ctx.channel().close();
}
@Override
public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
Date createTime = new Date(System.currentTimeMillis());
String clientIp=ctx.channel().remoteAddress().toString().replace("/","").split(":")[0];
//读取数据
ByteBuf buf=((ByteBuf)msg);
String clientMsg=buf.toString(CharsetUtil.UTF_8);
System.out.println(formatter.format(createTime)+" IP:"+clientIp+"数据内容:"+clientMsg+"线程名:"+Thread.currentThread ().getName ());
}
}
三、解决方法
1、可以看到以上客户端上传数据速度远远大于服务端处理数据的速度,随着时间的增长,会有很 多的消息积压,此时我们可以用netty的通道队列去处理。可以创建定时任务和普通任务,这里 只创建了普通任务。
/**
* 获取客户端数据
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
Date createTime = new Date(System.currentTimeMillis());
//读取数据
ByteBuf buf=((ByteBuf)msg);
String clientMsg=buf.toString(CharsetUtil.UTF_8);
//交给通道队列中处理,队列内的任务不是异步的,是循环处理的
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
System.out.println(formatter.format(createTime)+"线程名:"+Thread.currentThread ().getName ()+" 连接信息:"+ ctx.channel().remoteAddress().toString()+" 数据内容:"+clientMsg);
try {
//休眠3分钟
Thread.sleep(180*1000);
}catch (Exception e){
e.printStackTrace();
}
}
});
}
2、也可以使用线程池去处理
private static ExecutorService pool = Executors.newCachedThreadPool();
/**
* 获取客户端数据
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
Date createTime = new Date(System.currentTimeMillis());
//读取数据
ByteBuf buf = ((ByteBuf) msg);
String clientMsg = buf.toString(CharsetUtil.UTF_8);
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println(formatter.format(createTime) + "线程名:" + Thread.currentThread().getName() + " 连接信息:" + ctx.channel().remoteAddress().toString() + " 数据内容:" + clientMsg);
try {
//休眠3分钟
Thread.sleep(180 * 1000);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
总结:要解决 Netty 运行一段时间后读不到数据的问题,需要从多个方面进行排查,包括网络、配置、缓冲区、连接状态、超时设置,增加netty通道或者线程池和调整数据处理逻辑等,可以解决该问题。