Netty的基本理解
我们知道NIO以及多路复用的出现解决了BIO连接的阻塞和获取信息的阻塞问题。
那Netty的出现又是为了解决什么样的痛点问题呢?NIO的API极其繁杂,需要熟练运用Selector、 ServerSocketChannel、 SocketChannel、ByteBuffer等,导致开发难度也不小。而Netty是基于JDK的NIO之上,进行了封装,大大降低了开发的难度,让我们可以就基于handler来进行开发。
作为异步高性能的通讯框架,拥有高性能,高吞吐,低延迟,长连接,资源消耗低,最小化不必要的内存复制等优点,并提供了TCP/UDP和HTTP的协议栈,它的应用极其之广,Dubbo的RPC框架,Rocketmq,很多游戏的客户端与服务器之间的连接,等等,都是基于Netty来作为节点间通信的。
Netty线程模型
看懂这个图首先简单回顾一下多路复用模型。多路复用通过使用非阻塞I/O和选择器(Selectors),允许一个线程监视多个连接I/O事件,并提供多线程甚至是线程池来轮询(Select/Poll)或者监听(Epoll)数据的读取和写入。
接着再来欣赏一下这张图(图是网上找的,非原创),是不是清晰了很多。
先理解一下理论,在后续会提供Netty应用的代码详解,里面都有对应到。
-
BossGroup就是专门负责接受客户端连接,WorkerGroup专门负责网络的读写操作,
-
它们都是属于NioEventLoopGroup,是一种事件循环的线程池,在这个线程池中,会有NioEventLoop(相当于worker thread),它会包含一个selector,用于监听并作出回应。
-
Boss线程里面会处理accept事件(与Client建立连接),并注册将NIOSocketChannel到worker线程的selector上
-
Worker线程会轮询注册到selector上的所有NIOSocketChannel的读写事件,即业务。
-
最后橙色的就是pipeline(管道),上面维护了很多handler,每一个handler都是具体的业务逻辑,也是我们去进行具体开发的地方。handler又分为入栈和出栈,也存在着一定的执行顺序。
看了那么多的理论,是不是有点迫不及待去用它了。下面来看看具体的应用代码吧~
Netty的入门应用
Netty的基础框架
其实server端和client端的写法都是基本固定的,具体可操作的就是加入到pipeline的handler,如编解码的handler(会比较得影响性能),以及实现具体业务逻辑的handler,它通过责任链的模式保证了handler执行的顺序。
Server启动代码
public class NettyServer {
public static void main(String[] args) {
//两个线程池。boss负责Accept事件,即处理连接请求
NioEventLoopGroup boss = new NioEventLoopGroup();
//worker负责轮询Read, Write 事件,即真正的业务逻辑处理
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
//server端的启动对象
ServerBootstrap serverBootstrap = new ServerBootstrap();
//server使用NioServerSocketChannel作为server的通道实现,比较常见。
//还可以使用EpollServerSocketChannel,但仅限与linux操作系统。
serverBootstrap.channel(NioServerSocketChannel.class);
//和两个线程池绑定
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//首先要考虑编解码以及TCP的粘包,半包问题
ch.pipeline().addLast(new ProcotolFrameDecoder());
ch.pipeline().addLast(new MessageCodecSharable());
//业务代码
ch.pipeline().addLast(new HelloWorldHandler());
}
});
//绑定端口
Channel channel = serverBootstrap.bind(8080).sync().channel();
//对通道的关闭进行监听,是异步操作。sync方法会等待通道关闭处理完成
channel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
其实除了业务代码那一块,其他的都基本一样的,初始化灰常简单。
Client启动代码
public class NettyClient {
public static void main(String[] args) {
//事件循环组
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//一样,首先要考虑编解码以及TCP的粘包,半包问题
ch.pipeline().addLast(new ProcotolFrameDecoder());
ch.pipeline().addLast(new MessageCodecSharable());
//业务处理的handler
ch.pipeline().addLast(new PingHandler());
}
});
//启动client去连接server服务器
Channel channel = bootstrap.connect("localhost", 8080).sync().channel();
channel.closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
部分充分的代码就没有注释了,和server也一样,主要是在handler的处理。
handler的常见用法
首选需要清醒的一点是,handler们都被安置在pipeline里面。pipeline是一个双向链表,分入栈handler(会从链表的head往后传递到最后一个入站的handler)和出栈handler(从链表的tail往前传递到最前一个出栈的handler),两种类型的handler互不干扰。
这里列举了最最最常用的Handler,仅限于快速入门,需要更高级用法的请参考官网:Netty API Reference (4.1.109.Final)
下图是简单入栈的类图关系
当然别忘记了,子类也可以重写父类的方法,此外,通过writeAndFlush(Message)也可以快速将消息发送出去:
Handler 类 | 方法名 | 方法介绍 |
ChannelHandlerAdapter | handlerAdded | 当Handler被添加到Pipeline时调用。 |
handlerRemoved | 当Handler从Pipeline中移除时调用。 | |
exceptionCaught | 在处理过程中发生异常时调用。 | |
ChannelInboundHandlerAdapter | channelRead | 每当从客户端接收到新数据时调用。数据的类型通常是ByteBuf。 |
channelActive | 当Channel处于活跃状态(已连接到它的远程节点)时调用。 | |
channelInactive | 当Channel不再连接其远程节点时调用。 | |
channelReadComplete | 当Channel上的一次数据读取完毕时调用。 | |
SimpleChannelInboundHandler<Message> | channelRead0 | 该方法与channelRead类似,但它只处理特定类型的Message。 |
下图是简单出栈的类图关系:
出栈的看上去会更简单,没那么复杂的项目很少依赖于出栈handler,一般都在入栈handler里面通过write()或者writeAndFlush()就直接输出了。
ChannelOutboundHandlerAdapter | write | 被请求将消息写入到客户端时调用。 |
flush | 请求将之前写入到Channel中的消息全部写入到网络Socket中。 | |
bind | 当请求将Channel绑定到本地地址时调用。 | |
connect | 当请求将Channel连接到远程节点时调用。 | |
disconnect | 当请求将Channel从其远程节点断开连接时调用。 | |
close | 当请求关闭Channel时调用。 |
还有一些工具类的handler,它们具备协助我们业务代码的能力,也是需要认识一下滴~
Handler类 | 方法 | 描述 |
ByteToMessageDecoder | decode | 解析接收到的ByteBuf数据,转换成一个或多个Java对象。 |
decodeLast | 在Channel的最后一次调用decode方法时,处理剩余数据。 | |
MessageToByteEncoder | encode | 接受一个消息并将其编码为ByteBuf,然后写入到网络中。 |
IdleStateHandler | 构造方法: IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) | 该类不提供特定的方法供重写,但它会在检测到读、写或读写空闲时触发userEventTriggered事件,如 IdleState.WRITER_IDLE 或 IdleState.READER_IDLE。 捕获该类事件再进行处理,如心跳机制。 |
心跳机制如下:
Client
//先写一个心跳的handler
public class HeartbeatHandler extends ChannelInboundHandlerAdapter {
// 心跳消息内容,也可以用具体的Message对象来代替
private static final String HEARTBEAT_SEQUENCE = "HEARTBEAT";
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
//写空闲了,就发送心跳包
if (event.state() == IdleState.WRITER_IDLE) {
// 发送心跳消息
System.out.println("Sending heartbeat to server...");
ctx.writeAndFlush(HEARTBEAT_SEQUENCE);
}
} else {
super.userEventTriggered(ctx, evt);
}
}
}
//在client端添加
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(0, 3, 0, TimeUnit.SECONDS));
pipeline.addLast(new HeartbeatHandler());
Server
//server端再开一个IdleStateHandler
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(6, 0, 0, TimeUnit.SECONDS));
pipeline.addLast(new HeartbeatFailHandler());
如果没有收到来自client的心跳或者其他的信息,会再触发这里的读空闲事件(IdleState.READER_IDLE)。在HeartbeatFailHandler里面监听这类事件再根据业务逻辑进行处理就好了(关闭or重连)。
Netty的入门教学就先说这么多,后面可能会再做一节源码分析~