一、Netty简介
Netty是由JBOSS提供的一个java开源框架,现为 Github上的独立项目。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
也就是说,Netty 是一个基于NIO的客户、服务器端的编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。
“快速”和“简单”并不用产生维护性或性能上的问题。Netty 是一个吸收了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,并经过相当精心设计的项目。最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性
1.1 Netty的特点
- 设计优雅
适用于各种传输类型的统一API - 阻塞和非阻塞Socket
基于灵活且可扩展的事件模型,可以清晰地分离关注点
高度可定制的线程模型 - 单线程,一个或多个线程池
真正的无连接数据报套接字支持(自3.1起) - 使用方便
详细记录的Javadoc,用户指南和示例
没有其他依赖项,JDK 5(Netty 3.x)或6(Netty 4.x)就足够了 - 高性能
吞吐量更高,延迟更低
减少资源消耗
最小化不必要的内存复制 - 安全
完整的SSL / TLS和StartTLS支持 - 社区活跃,不断更新
社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入
1.2 Netty常见使用场景
- 互联网行业
在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高新能的通信框架,往往作为基础通信组件被这些RPC框架使用。
典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。 - 游戏行业
无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。Netty作为高性能的基础通信组件,它本身提供了TCP/UDP和HTTP协议栈。
非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过Netty进行高性能的通信 - 大数据领域
经典的Hadoop的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行跨界点通信,它的Netty Service基于Netty框架二次封装实现
二、Netty高性能设计
2.1 Netty线程模型
Netty主要基于主从Reactors多线程模型(参考:【Linux】IO的线程模型)(如下图)做了一定的修改,其中主从Reactor多线程模型有多个Reactor:MainReactor和SubReactor:
- MainReactor负责客户端的连接请求,并将请求转交给SubReactor
- SubReactor负责相应通道的IO读写请求
- 非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进行处理
这里引用Doug Lee大神的Reactor介绍:Scalable IO in Java里面关于主从Reactor多线程模型的图
特别说明的是:
虽然Netty的线程模型基于主从Reactor多线程,借用了MainReactor和SubReactor的结构,但是实际实现上,SubReactor和Worker线程在同一个线程池中:
1 EventLoopGroup bossGroup = new NioEventLoopGroup(); 2 EventLoopGroup workerGroup = new NioEventLoopGroup(); 3 ServerBootstrap server = new ServerBootstrap(); 4 server.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
上面代码中的bossGroup 和workerGroup是Bootstrap构造方法中传入的两个对象,这两个group均是线程池
- bossGroup线程池则只是在bind某个端口后,获得其中一个线程作为MainReactor,专门处理端口的accept事件,每个端口对应一个boss线程
- workerGroup线程池会被各个SubReactor和worker线程充分利用
2.2 异步处理
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
Netty中的I/O操作是异步的,包括bind、write、connect等操作会简单的返回一个ChannelFuture,调用者并不能立刻获得结果,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。
当future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作,常见有如下:
- 通过isDone方法来判断当前操作是否完成
- 通过isSuccess方法来判断已完成的当前操作是否成功
- 通过getCause方法来获取已完成的当前操作失败的原因
- 通过isCancelled方法来判断已完成的当前操作是否被取消
- 通过addListener方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果future对象已完成,则理解通知指定的监听器
例如下面的代码中绑定端口是异步操作,当绑定操作处理完,将会调用相应的监听器处理逻辑
1 serverBootstrap.bind(port).addListener(future -> { 2 if (future.isSuccess()) { 3 System.out.println(new Date() + ": 端口[" + port + "]绑定成功!"); 4 } else { 5 System.err.println("端口[" + port + "]绑定失败!"); 6 } 7 });
相比传统阻塞I/O,执行I/O操作后线程会被阻塞住, 直到操作完成;异步处理的好处是不会造成线程阻塞,线程在I/O操作期间可以执行别的程序,在高并发情形下会更稳定和更高的吞吐量。
三、Netty架构设计
前面介绍完Netty相关一些理论介绍,下面从功能特性、模块组件、运作过程来介绍Netty的架构设计
3.1 功能特性
- 传输服务
支持BIO和NIO - 容器集成
支持OSGI、JBossMC、Spring、Guice容器 - 协议支持
HTTP、Protobuf、二进制、文本、WebSocket等一系列常见协议都支持。
还支持通过实行编码解码逻辑来实现自定义协议 - Core核心
可扩展事件模型、通用通信API、支持零拷贝的ByteBuf缓冲对象
3.2 模块组件
Bootstrap、ServerBootstrap
Bootstrap意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。
Future、ChannelFuture
正如前面介绍,在Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。
Channel
Netty网络通信的组件,能够用于执行网络I/O操作。
Channel为用户提供:
- 当前网络连接的通道的状态(例如是否打开?是否已连接?)
- 网络连接的配置参数 (例如接收缓冲区大小)
- 提供异步的网络I/O操作(如建立连接,读写,绑定端口),异步调用意味着任何I/O调用都将立即返回,并且不保证在调用结束时所请求的I/O操作已完成。调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以I/O操作成功、失败或取消时回调通知调用方。
- 支持关联I/O操作与对应的处理程序
不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,下面是一些常用的 Channel 类型
- NioSocketChannel,异步的客户端 TCP Socket 连接
- NioServerSocketChannel,异步的服务器端 TCP Socket 连接
- NioDatagramChannel,异步的 UDP 连接
- NioSctpChannel,异步的客户端 Sctp 连接
- NioSctpServerChannel,异步的 Sctp 服务器端连接
这些通道涵盖了 UDP 和 TCP网络 IO以及文件 IO.
Selector
Netty基于Selector对象实现I/O多路复用,通过 Selector, 一个线程可以监听多个连接的Channel事件, 当向一个Selector中注册Channel 后,Selector 内部的机制就可以自动不断地查询(select) 这些注册的Channel是否有已就绪的I/O事件(例如可读, 可写, 网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。
NioEventLoop
NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:
- I/O任务
即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法触发。 - 非IO任务
添加到taskQueue中的任务,如register0、bind0等任务,由runAllTasks方法触发。
两种任务的执行时间比由变量ioRatio控制,默认为50,则表示允许非IO任务执行的时间与IO任务的执行时间相等。
NioEventLoopGroup
NioEventLoopGroup,主要管理eventLoop的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应于一个线程。
ChannelHandler
ChannelHandler是一个接口,处理I/O事件或拦截I/O操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。
ChannelHandler本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:
- ChannelInboundHandler用于处理入站I/O事件
- ChannelOutboundHandler用于处理出站I/O操作
或者使用以下适配器类:
- ChannelInboundHandlerAdapter用于处理入站I/O事件
- ChannelOutboundHandlerAdapter用于处理出站I/O操作
- ChannelDuplexHandler用于处理入站和出站事件
ChannelHandlerContext
保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象
ChannelPipline
保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作。 ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互。
下图引用Netty的Javadoc4.1中ChannelPipline的说明,描述了ChannelPipeline中ChannelHandler通常如何处理I/O事件。 I/O事件由ChannelInboundHandler或ChannelOutboundHandler处理,并通过调用ChannelHandlerContext中定义的事件传播方法(例如ChannelHandlerContext.fireChannelRead(Object)和ChannelOutboundInvoker.write(Object))转发到其最近的处理程序。
1 I/O Request
2 via Channel or
3 ChannelHandlerContext
4 |
5 +---------------------------------------------------+---------------+
6 | ChannelPipeline | |
7 | \|/ |
8 | +---------------------+ +-----------+----------+ |
9 | | Inbound Handler N | | Outbound Handler 1 | |
10 | +----------+----------+ +-----------+----------+ |
11 | /|\ | |
12 | | \|/ |
13 | +----------+----------+ +-----------+----------+ |
14 | | Inbound Handler N-1 | | Outbound Handler 2 | |
15 | +----------+----------+ +-----------+----------+ |
16 | /|\ . |
17 | . . |
18 | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
19 | [ method call] [method call] |
20 | . . |
21 | . \|/ |
22 | +----------+----------+ +-----------+----------+ |
23 | | Inbound Handler 2 | | Outbound Handler M-1 | |
24 | +----------+----------+ +-----------+----------+ |
25 | /|\ | |
26 | | \|/ |
27 | +----------+----------+ +-----------+----------+ |
28 | | Inbound Handler 1 | | Outbound Handler M | |
29 | +----------+----------+ +-----------+----------+ |
30 | /|\ | |
31 +---------------+-----------------------------------+---------------+
32 | \|/
33 +---------------+-----------------------------------+---------------+
34 | | | |
35 | [ Socket.read() ] [ Socket.write() ] |
36 | |
37 | Netty Internal I/O Threads (Transport Implementation) |
38 +-------------------------------------------------------------------+
入站事件由自下而上方向的入站处理程序处理,如图左侧所示。 入站Handler处理程序通常处理由图底部的I/O线程生成的入站数据。 通常通过实际输入操作(例如SocketChannel.read(ByteBuffer))从远程读取入站数据。
出站事件由上下方向处理,如图右侧所示。 出站Handler处理程序通常会生成或转换出站传输,例如write请求。 I/O线程通常执行实际的输出操作,例如SocketChannel.write(ByteBuffer)。
在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应, 它们的组成关系如下:
一个 Channel 包含了一个 ChannelPipeline, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表, 并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。入站事件和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler互不干扰。
四、工作原理架构
初始化并启动Netty服务端过程如下:
1 public static void main(String[] args) {
2 // 创建mainReactor
3 NioEventLoopGroup boosGroup = new NioEventLoopGroup();
4 // 创建工作线程组
5 NioEventLoopGroup workerGroup = new NioEventLoopGroup();
6
7 final ServerBootstrap serverBootstrap = new ServerBootstrap();
8 serverBootstrap
9 // 组装NioEventLoopGroup
10 .group(boosGroup, workerGroup)
11 // 设置channel类型为NIO类型
12 .channel(NioServerSocketChannel.class)
13 // 设置连接配置参数
14 .option(ChannelOption.SO_BACKLOG, 1024)
15 .childOption(ChannelOption.SO_KEEPALIVE, true)
16 .childOption(ChannelOption.TCP_NODELAY, true)
17 // 配置入站、出站事件handler
18 .childHandler(new ChannelInitializer<NioSocketChannel>() {
19 @Override
20 protected void initChannel(NioSocketChannel ch) {
21 // 配置入站、出站事件channel
22 ch.pipeline().addLast(...);
23 ch.pipeline().addLast(...);
24 }
25 });
26
27 // 绑定端口
28 int port = 8080;
29 serverBootstrap.bind(port).addListener(future -> {
30 if (future.isSuccess()) {
31 System.out.println(new Date() + ": 端口[" + port + "]绑定成功!");
32 } else {
33 System.err.println("端口[" + port + "]绑定失败!");
34 }
35 });
36 }
- 基本过程如下:
- 1 初始化创建2个NioEventLoopGroup,其中boosGroup用于Accetpt连接建立事件并分发请求, workerGroup用于处理I/O读写事件和业务逻辑
- 2 基于ServerBootstrap(服务端启动引导类),配置EventLoopGroup、Channel类型,连接参数、配置入站、出站事件handler
- 3 绑定端口,开始工作
结合上面的介绍的Netty Reactor模型,介绍服务端Netty的工作架构图:
server端包含1个Boss NioEventLoopGroup和1个Worker NioEventLoopGroup,NioEventLoopGroup相当于1个事件循环组,这个组里包含多个事件循环NioEventLoop,每个NioEventLoop包含1个selector和1个事件循环线程。
每个Boss NioEventLoop循环执行的任务包含3步:
- 1 轮询accept事件
- 2 处理accept I/O事件,与Client建立连接,生成NioSocketChannel,并将NioSocketChannel注册到某个Worker NioEventLoop的Selector上
- 3 处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用eventloop.execute或schedule执行的任务,或者其它线程提交到该eventloop的任务
每个Worker NioEventLoop循环执行的任务包含3步:
- 1 轮询read、write事件
- 2 处理I/O事件,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理
- 3 处理任务队列中的任务,runAllTasks
其中任务队列中的task有3种典型使用场景
- 1 用户程序自定义的普通任务
1 ctx.channel().eventLoop().execute(new Runnable() {
2 @Override
3 public void run() {
4 //...
5 }
6 });
-
2 非当前reactor线程调用channel的各种方法
例如在推送系统的业务线程里面,根据用户的标识,找到对应的channel引用,然后调用write类方法向该用户推送消息,就会进入到这种场景。最终的write会提交到任务队列中后被异步消费。 -
3 用户自定义定时任务
1 ctx.channel().eventLoop().schedule(new Runnable() {
2 @Override
3 public void run() {
4
5 }
6 }, 60, TimeUnit.SECONDS);
一、服务端Netty的工作架构
二、Netty服务端
代码如下:
1、NettyServer.java 服务端主类
1 public class NettyServer {
2
3 public static void main(String[] args) throws InterruptedException {
4
5 // 创建EventLoopGroup
6 NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
7 NioEventLoopGroup workerGroup = new NioEventLoopGroup();
8
9 try {
10 // 创建ServerBootstrap
11 ServerBootstrap bootstrap = new ServerBootstrap();
12
13 // 配置bootstrap
14 bootstrap.group(bossGroup, workerGroup)
15 // 指定使用的 NIO传输Channel
16 .channel(NioServerSocketChannel.class)
17 // 指定端口,设置套接字地址
18 .localAddress(new InetSocketAddress(9000))
19 // 添加处理器
20 .childHandler(new ChannelInitializer<SocketChannel>() {
21 @Override
22 protected void initChannel(SocketChannel ch) throws Exception {
23 // 初始化channel时,添加处理器
24 ch.pipeline().addLast(new NettyServerHandler());
25 }
26 });
27
28 // 绑定端口
29 // sync():调用sync()方法阻塞等待知道绑定完成
30 ChannelFuture channelFuture = bootstrap.bind().sync();
31 // 获取Channel的CloseFuture,并阻塞知道它完成
32 channelFuture.channel().closeFuture().sync();
33 } finally {
34 // 关闭释放资源
35 bossGroup.shutdownGracefully().sync();
36 workerGroup.shutdownGracefully().sync();
37 }
38
39 }
40 }
2、NettyServerHandler.java 服务端处理类
1 public class NettyServerHandler extends ChannelInboundHandlerAdapter {
2
3 // 对于每个传入的消息都要调用
4 @Override
5 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
6 // 记录已接收的消息
7 ByteBuf byteBuf = (ByteBuf) msg;
8 System.out.println("Server received: " + byteBuf.toString(CharsetUtil.UTF_8));
9 }
10
11 // 通知 ChannelInboundHandlerAdapter 最后一次 channelRead()的调用
12 // 是当前批量读取中的最后一条消息
13 @Override
14 public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
15 // 将未决消息冲刷到远程节点,并且关闭该Channel;
16 ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
17 .addListener(ChannelFutureListener.CLOSE);
18 System.out.println("读取完成");
19 }
20
21 // 在读取操作期间,有异常抛出时调用
22 @Override
23 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
24 // 打印异常
25 cause.printStackTrace();
26 // 关闭Channel
27 ctx.channel();
28 }
29 }
三、Netty客户端
代码如下:
1、NettyClient.java 客户端主类
1 public class NettyClient {
2 public static void main(String[] args) throws InterruptedException {
3 // 事件循环组
4 NioEventLoopGroup group = new NioEventLoopGroup();
5 try {
6 // 启动对象
7 Bootstrap bootstrap = new Bootstrap();
8 // 配置
9 bootstrap.group(group)
10 .channel(NioSocketChannel.class)
11 .remoteAddress(new InetSocketAddress("127.0.0.1",9000))
12 .handler(new ChannelInitializer<SocketChannel>() {
13 @Override
14 protected void initChannel(SocketChannel ch) throws Exception {
15 ch.pipeline().addLast(new NettyClientHandler());
16 }
17 });
18
19 // 连接远程地址,阻塞至连接完成
20 ChannelFuture channelFuture = bootstrap.connect().sync();
21 // 阻塞直到channel关闭
22 channelFuture.channel().closeFuture().sync();
23 } finally {
24 // 关闭释放资源
25 group.shutdownGracefully().sync();
26 }
27 }
28 }
2、NettyClientHandler.java 客户端处理类
1 public class NettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
2
3 // 当从服务器接收到一条消息时被调用
4 @Override
5 protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
6 // 记录已接收的消息
7 System.out.println("Client received: " + msg.toString(CharsetUtil.UTF_8));
8 }
9
10 // 在到服务器的连接已建立之后将被调用
11 @Override
12 public void channelActive(ChannelHandlerContext ctx) throws Exception {
13 // 当被通知Channel是活跃的时侯,发送一条消息
14 ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
15 }
16
17 // 在处理过程中引发异常时调用
18 @Override
19 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
20 // 打印异常
21 cause.printStackTrace();
22 // 关闭Channel
23 ctx.channel();
24 }
25 }