网络编程 — Reactor模型与Netty入门

前言

前面我们介绍了网络的基础知识,socket网络编程和IO模型,这一小节我们来入门Netty。对于很多程序员来说,Netty是非常遥远的。很多程序员认为平时的开发过程中基本不会使用到Netty,如果你也是这么认为的那就大错特错了。因为我们虽然不会直接写Netty的代码,但是我们每天都在间接使用Netty,大到我们的RPC框架dubbo、大数据的Hadoop,小到Redis的工具包,几乎所有实现网络通信的框架都是基于Netty实现的。所以学习Netty掌握Netty不论是对于我们来说都是开发网络程序,还是排查网络问题,亦或是框架设计思想上的提升都有莫大的帮助。这一小节我们先从最基本的Netty的结构模型开始,先建立一个Netty的基本的认知,随后我们编写Netty的HelloWorld加深对Netty的一些模块点的理解。我们发车啦~

为什么是Netty

Netty是目前最流行的由JBOSS提供的一个Java开源NIO框架,Netty提供异步的、事件驱动的网络应用程序框架和工具,用来快速开发高性能、高可靠的网络服务器和客户端程序。

Netty的优点

Netty提供了相对十分简单易用的API,非常适合网络编程。Netty是完全基于NIO实现的,Netty提供了所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。Netty无疑是NIO的老大,它的健壮性、可定制性、可拓展性和强大的功能性能在同类框架中都是首屈一指的。Netty的优点可以总结如下:

  1. API使用简单,开发门槛低。

  2. 功能强大,预置了多种编解码功能,支持多种主流协议。

  3. 定制功能强大,可以通过ChannelHandler对通信框架进行灵活地扩展。

  4. 性能高,通过与其他业绩主流NIO框架对比,Netty的综合性能最优。

  5. 成熟、稳定,Netty修复了已经发现的JDK NIO bug,业务开发人员不需要在开发过程中因为NIO的BUG而烦恼。

  6. 社区活跃,版本迭代周期短,发现的bug可以被及时修复,同时,更多的新功能会加入。

  7. 经历了大规模的商业应用,质量得到验证。

高性能的协议服务器的特性:
高吞吐、低延迟、低开销、零拷贝、可扩容、松耦合(网络逻辑和业务逻辑分离)、使用方便、可维护性好。

Netty的兼容特性:

  • JDK兼容性:

    • Netty 3.x :JDK5
    • Netty 4.x :JDK6
    • Netty 5.x:已经废弃
  • 协议兼容性:

    • 兼容大部分通用协议
    • 支持自定义协议
  • 基于Netty可实现构建实现众多服务类型,包括但不仅限于:

    • HTTP Server
    • HTTPS Server
    • WebSocket Server
    • TCP Server
    • UDP Server

不尽如人意的NIO

我们都知道Java有自己实现的nio,同时Netty也是基于Java nio实现的。有的同学可能还是会说,我看socket就挺好,简单原生,我就是喜欢用自己去封装,不用Netty行不行呢?当然是可以的,但是我还是建议你使用Netty进行网络开发,具体的原因那还得从Java nio自身说起。在JDK1.8的NIO中依然存在一些问题,这些问题在编写NIO程序时候需要格外注意⚠️:

  • NIO跨平台的兼容性问题。NIO是底层API,它的实现依赖于操作系统针对IO操作的APIs,所以Java能在所有操作系统中实现统一的接口,并用一致的行为来操作IO是很伟大的。使用NIO经常发现代码在Linux上正常运行,但在Windows上就会出现问题。所以编写程序,特别是NIO程序,特别是NIO,需要在程序支持的所有操作系统上进行功能测试,否则你可能会碰到一些莫名其妙的问题。NIO2看起来很理想,但是NIO2只支持jdk1.7+,若你的程序在Java1.6上运行,则无法使用NIO2。另外,Java7的NIO2中没有提供DatagramSocket的支持,所以NIO2只支持TCP程序,不支持UDP程序。
  • NIO对缓冲区的聚合和分散操作可能会导致内存泄漏。很多Channel的实现支持Gather和Scatter。这个功能允许从多个ByteBuffer中读入或写入,这样做可以有更好的性能。操作系统底层知道如何处理这些写入/读出,并且能以最有效的方式处理。如果要分割的数据在多个不同的ByteBuffer中,使用Gather/Scatter是比较好的方式。

scatter/gather指的在多个缓冲区上实现一个简单的I/O操作,比如从通道中读取数据到多个缓冲区,或从多个缓冲区写入数据到通道;

  1. scatter(分散):指的是从通道中读取数据分散到多个缓存区Buffer的过程,该过程会将每个缓存区填满,直至通道中无数据或缓冲区没有空间。
  2. gatter(聚集):指的是将多个缓冲区Buffer聚集起来写入到通道的过程,该过程类似于将多个缓冲区的内容连接起来写入通道。
  • Squashing the famous epoll bug(压碎著名的epoll bug),Linux-like OSs的选择器使用的是epoll-bug也可能会导致无效的状态选择和100%的CPU利用率。要解决epoll-bug的唯一方法是回收旧的选择器,将先前注册的通道实例转移到新创建的选择器上。NIO对epoll问题的解决方案是有限制的,Netty提供了更好的解决方案。

Netty 模型介绍

Netty的线程模型也不是一上来现在的主从Reactor模型,也有一个思考进化的过程。在Doug Lee编写的《Scalable IO in Java》中有很好的表述,说明了整个线程模型的变化过程。学习Netty的线程模型非常推荐去看看英文版本,不论是对加深对Netty线程的理解,还是对逐步的结构设计都有很大的启发作用。

在介绍之前我们先需要介绍几个基本的概念:

  • Channel,通道,Java NIO中的基础概念,代表一个打开的连接,可以执行读取/写入IO操作。Netty对Channel的所有IO操作都是非阻塞的。

  • ChannelFuture,Java的Future接口,只能查询操作的完成情况,或者阻塞当前线程等待操作完成。Netty封装一个ChannelFuture接口,我们可以将回调方法传给ChannelFutrue,在操作执行完成时自动执行。

  • Event & HandlerNetty基于事件驱动,事件和处理器可以关联到入站和出战数据流。

  • Encoder & Decoder,处理网络IO时,需要进行序列化与反序列化,转换Java对象与字节流。对入站数据进行解码,基类是ByteToMessageDecoder。对出战数据进行编码,基类是MessageToByteEncoder

  • ChannelPipeline,数据处理管道就是事件处理器链。有顺序,同一Channel的出战处理器和入站处理器在同一列表中。

基础模型抽象

对于网络服务都有一个通用的处理结构即readdecodeprocessServiceencodesend。当然他们内部的逻辑并不一样,例如对应不同的请求数据类型会有不一样的编解码形式,不同的业务逻辑也会有不一样的处理方式。但是他们处理流程的结构都是相通的。

正因为这第一层抽象,我们可以得到一个经典抽象结构,即一个客户端连接多个Service,一个Service对接多个Handler,并且每个Handler都有自己的线程。

这个结构就对应我们前面介绍的更多的连接中的多线程的解决方案。即每个连接都由一个handler快速处理。但是这样的结构也存在明显的弊端,就是扩展性不够。不能随着外部资源的提升增加性能,随着连接的增加Server连接越来越多的服务,整个负载受到Server连接数量的限制。对于Netty整个发展的过程我们可以有以下一些拓展思考。

以下这些内容都是从Scalable in Java里面摘取出来的,是Reactor模型设计上的一些思考,我觉得对于一些结构上的设计非常具有启发意义,值得反复品。其中有一句话非常指的思考🤔。

Divide-and-conquer is usually the best approach for achieving any scalability goal

--

分治思想通常是处理的可拓展性问题的最好方法。

对于拓展性我们可以从下面几个方面思考

  • 在增加负载的情况下,性能缓慢的降级(连接更多的客户端)。
  • 可以随着硬件资源的提升,不断提升整体性能(CPU、内存、硬盘和带宽等)。
  • 还有一些可用性和性能指标
    • 低延迟。
    • 能在最高负载下稳定运行。
    • 可以调节性能指标。
  • 分治思想

这里谈到了分治思想,前面Doug Lee 也提到了分治思想是处理扩展性问题的最好解决办法,那我们一起看看分治思想又是怎么在IO中体现的吧。Java NIO 中的分治思想:

  • 将大任务切分成多个小任务,并且每个小任务都是非阻塞的。
  • 事件驱动,当任务达到就可以被处理,通常用一个IO事件触发条件。
  • 基本的Java NIO机制支持。
    • 非阻塞的读和写。
    • 分派任务和IO事件进行关联。
  • 无穷无尽的拓展性
    • 可以依据任务驱动设计模式设计更多的处理器。

事件驱动模型

上面我们提到了事件驱动。事件驱动就是基于事件触发拉起程序的执行。例如Java里面的AWT即Java桌面窗口程序,如果你写过JWT或者说是.net桌面应用程序,那肯定知道如果想要这些程序正常的执行一定将点击事件和一个处理器绑定,当用户点击某个button就会触发执行特定Handler。我们的web前端交互也是这样的,只不过这些操作前端帮我们处理了,然后前端直接触发对应的接口逻辑。整个过程如下图展示:

事件驱动相较于其他的一些方式有更高的效率。通常来说一段逻辑的执行由某个事件触发。这意味着更少的资源消耗,并不需要对于每个客户端都用一个线程处理,线程资源可以池化公用同时也会由更少的上下文切换和更少的锁。如果基于这样的设计就需要添加一个dispatcher,使用dispatcher将事件分派到对应的handler,但是dispatcher可能成为性能的瓶颈,因为每个连接都需要通过这里分派到对应的处理器处理,这个部分还需要开放事件和handler的绑定,可以在项目启动时或程序运行期间进行事件和handler的绑定。

Reactor单线程模型

我们的模型并没有直接采用这种模式而是使用了响应(Reactor)模式,也就是我们经常的提到的Netty的Reactor模型。Reactor和我们的响应式还是很相似的,首先是事件驱动,有一个或者多个并发输入源,有一个Service Handler和多个EventHandlers。这个Service Handler会同步的将输入的请求多路复用并分发给相应的EventHandler。

上面这个版本是Reactor的单线程版本,一个客户端连接上acceptor后,然后分派给对应的Handler处理执行。但是一个连接连接上acceptor之后,所有的I/O操作都在一个线程上完成,此时线程职责包括:接收新连接请求、读写操作等。一个客户端连接上后,在处理完成send之前,acceptor都处于阻塞状态,这极大的影响了Reactor的整体性能。依据这个逻辑我们Netty构建出来的Reactor模型如下:

Netty对于这种模式的编码实现:

1
2
3
EventLoopGroup eventGroup = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);

Reactor多线程模型

Reactor多线程模型和Reactor单线程模型的区别就在于处理连接的是一个线程,而处理具体业务逻辑是另外一组线程,这样的好处就是,一个连接acceptor同时可以处理多个连接事件

Reactor多线程模型在绝大多数场景下都能满足性能需求。但是这个模型中,只有一个线程负责监听和处理所有的客户端连接。如果遇到连接风暴,一个线程是很有可能扛不住的。同时通过我们前面介绍过的TCP三次握手,一个连接的背后会有多次的握手交互,所以一个线程的Reactor的压力还是很大的。Netty的Reactor多线程模型如下:

Netty对于Reactor多线程版本的实现:

1
2
3
EventLoopGroup eventGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);

在连接了很多的客户端的情况下,一个acceptor线程可能会成为整个系统的瓶颈。为了解决这个潜在的性能问题,又有了下面的第三种Reactor模型。

Reactor主从多线程模式

Reactor主从多线程模式的特点是,服务端处理客户端的连接不再是一个单独的线程,而是一个独立的线程池。acceptor接收到客户端TCP连接请求并处理完成后(这里的处理包括握手、认证等操作),一旦链路建立成功,就将链路注册到后端的 subReactor线程池I/O线程上,由I/O线程负责后续的操作。一下就是这个模型的示意图。

到此,我们的线程模型演变到最终的主从Reactor模式,分为两个Reactor,第一个Reactor处理连接有关的操作,而第二个Reactor负责处理业务有关的操作。当连接很多客户端时候,整个连接压力被均匀的分摊到mainReactor的多个线程上,再配合subReactor将业务处理分发到各个worker线程上,使得整个模型在比较高的负载下依旧有很高的性能表现。并且是可以通过提升机器性能来提升整体服务的性能,可根据实际需要来配置合适的性能范围。按照这个模型建立的Netty模型如下:

Netty对上面主从的Reactor模式的实现如下:

1
2
3
4
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);

快速开始

在梳理过前面的模型之后,我们来写一个简单的Demo,快速上手Netty。这次我们写一个客户端和服务端互相说Hello的程序,代码相较于Socke程序会有有点长,要耐心看完~。使用的Netty版本如下:

1
2
3
4
5
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.51.Final</version>
</dependency>

服务端的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* Created by Daiwei on 2021/9/21
*/
public class NettyServer {

public static void main(String[] args) {
int port = 8801;
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 512)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new HelloHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("服务启动完成, 监听 127.0.0.1:" + port);
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}

/**
* helloHandler 处理器
*/
class HelloHandler extends ChannelInboundHandlerAdapter {

/**
* 从channel中读数据。执行一些业务操作,或者加入一些hook触发后续业务。
* 同时可以通过channel发送数据
* @param ctx 上下文
* @param msg 从channel中读到的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("message["+ byteBuf.toString(StandardCharsets.UTF_8) +"] received!hello there and " +
"message is from [" + ctx.channel().remoteAddress().toString() + "]");
ByteBuf toClientMsg = Unpooled.copiedBuffer( "hello!".getBytes(StandardCharsets.UTF_8));
ctx.channel().writeAndFlush(toClientMsg);
}


/**
* 在处理过程中捕捉到了异常执行这个方法,输出异常,关闭资源
* @param ctx 上下文
* @param cause 异常信息
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
super.exceptionCaught(ctx, cause);
}
}

客户端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* Created by Daiwei on 2021/9/21
*/
public class NettyClient {

public static void main(String[] args) {
String host = "127.0.0.1";
int port = 8801;
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ClientHelloHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}

/**
* 客户端的ChannelHandler
*/
class ClientHelloHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("receive message [" + buf.toString(StandardCharsets.UTF_8) + "] from server!");
ctx.close();
}

/**
* 当有channel连接上发送消息
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("hello server!".getBytes(StandardCharsets.UTF_8)));
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
super.exceptionCaught(ctx, cause);
}
}

执行结果如下:

1
2
3
4
5
6
Server端输出:
服务启动完成, 监听 127.0.0.1:8801
message[hello server!] received!and message is from [/127.0.0.1:55370]
---
Client端输出:
receive message [hello!] from server!

上面这个例子是最基础的一个入门的demo,但是相比于Socket还是复杂了不少😂。先启动服务端启动并监听8801端口,服务端启动后输出日志信息“服务启动完成, 监听端口 127.0.0.1:8801”。随后我们启动客户端,客户端连接127.0.0.1:8081,随后连接成功后,客户端的Netty回调channelActive这个方法,然后客户端发送一个“hello server”消息。然后服务端接收到消息并将信息输出到console,然后服务端向客户端发送消息“hello”。客户端收到消息之后,将消息输入到console并关闭channel断开连接。

组件和运行流程介绍

对于接触过Netty的同学,上面的例子会感觉非常简单,没有接触过Netty的小伙伴来说,这个Demo接触下来还是有一些难度的,尤其一些陌生的类。这些陌生的类都是Netty的核心组件,接下来我们来介绍这些组件的基本功能和Netty的运行流程。

基本组件

其中我们的核心组件包括EventLoopBootstrap&ServerBootstrapChannelChannelHandlerChannelPipelineChannelFuture等。这些核心组件支撑起了Netty基本的主从Reactor结构。

  • EventLoopEventLoop用于处理Channel的I/O操作。一个单一的EventLoop通常会处理多个Channel事件。一个EventLoopGroup可以含有多于一个EventLoop和提供了一种迭代用于检索清单中的下一个。

  • Bootstrap&ServerBootstrapNetty应用程序通过设置bootstrap引导类来完成,该类提供了一个用于应用网络配置的容器。Bootstrap服务端的是ServerBootstrap,客户端是Bootstrap

  • Channel:Netty中的接口 Channel 定义了与 socket 丰富的交互的操作集:bindcloseconfigconnectisActiveisOpenisWritablereadwrite等等。

  • ChannelHandlerChannelHandler 支持非常多的协议,并且提供用于数据处理的容器,ChannelHandler中的方法由特定的事件触发,常用的一个接口是ChannelInboundHandler,该类型主要方法入站读数据(socket读事件)。

  • ChannelPipelineChannelPipeline提供了一个容器给ChannelHandler链并提供了一个API调用,用于管理沿着链入站和出站事件的流动。每个Channel都有自己的ChannelPipeline,当Channel创建时自动创建的。下图即可说明两者的关系。

  • ChannelFutureNetty 所有的I/O操作都是异步的。因为一个操作可能无法立即返回,我们需要一种方法在以后获取他的结果,出于这个目的,Netty提供了接口ChannelFutureaddListener方法。

Netty是一个非阻塞、事件驱动的网络框架。Netty实际上是使用Threads(多线程)处理I/O事件的,对于熟悉多线程编程可能需要关注代码同步。但是这样并不好,同步会严重影响到程序的性能,线程间的同步操作也容易产生bug。Netty的设计保证了处理事件不会又同步,因为处理某个Channel事件是被添加到一个EventLoop中的,以后该Channel事件都是由该EventLoop的线程处理,并且每个线程之间资源是隔离的,也就是说Netty不需要进行线程间的同步资源竞争操作。EventLoop在EventLoopGroup中,他们的关系可以理解为线程和线程池的关系。

运行流程分析

我们梳理完核心组件之后,在看前面的代码就清晰很多了,我们上面列举的核心组件也基本在代码中都有使用。他们通过ServerBootstrap联系在一起,通过Group()方法将两个EventLoopGroup实例(NioEventLoopGroup)组成主从Reactor,然后使用option()方法配置可调参数,使用channel()函数指定监听的SocketChannel类型。随后使用childChannel(new ChannelInitializer<>{...}) 构造出后续的PipelinechannelHandler等。默认情况下每有一个连接创建,这个部分都会创建一次。最后将整个服务绑定在某个监听端口,然后返回一个ChannelFuture对象。这个服务端就算是启动完毕。因为EventLoopGroup本质上是个线程池,所以最后要优雅的关闭回收资源即shutdwonGracefully(),以下这张图是Server端的Netty主从Reactor的工作示意图。

ServerSocketChannel,SocketChannel之间的关系和ServerSocket与Socket之间的关系类似。

整个工作描述如下:

  1. Netty抽象出两组线程池,BossGroup 专门负责接收客户端的连接,workerGroup 专门负责网络的读写。他们的类型都是NioEventLoopGroup。
  2. NioEventLoopGroup相当于一个事件循环组,这个组中包含了很多个事件循环,每个事件循环是NioEventLoop。
  3. NioEventLoop标识一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket网络通信
  4. NioEventLoopGroup可以有多个线程,即包含多个NioEventLoop。
  5. 每个BossNioEventLoop循环的步骤分为3步:
    1. 轮询accept事件
    2. 处理accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某个worker NioEventLoop上的selector。
    3. 处理任务队列的任务,即runAllTasks。
  6. 每个WorkerNioEventLoop循环执行的步骤:
    1. 轮询read/write事件。
    2. 处理I/O事件,即read/write事件,在对应NioSocketChannel处理。
    3. 处理任务队列的任务,即runAllTasks。
  7. 每个worker NIOEventLoop处理业务会使用pipeline,pipeline中包含了channel,即通过pipeline可以获取到对应的通道,管道中维护了很多的处理器。

总结

这一小节我们先抛出了一个疑问?为什么是Netty,随后我们梳理了Netty的优点和Java NIO 的缺点,总结得出好用强大且拓展性高的Netty是我们首选的网络编程工具这一结论。随后我们介绍了Netty的模型的演进,这一部分我们一起顺着Doug Lee在《Scalable IO in Java》中的观点,把Netty的模型梳理了一边。一开始我们是一个简单的多个客户端连接一个Server,然后Server调用一个包含读取发送,编解码和业务处理的调用链。很明显这个结构不具备高可用性和高拓展性。其实我们的网络程序和Java AWT程序有异曲同工之处,他们都可以是一个事件驱动。基于事件驱动整体可以获得更高的效率,更少的上下文切换,但是编码会相较变得稍复杂一些。Reactor的单线程版本出现了,这个版本当一个acceptor监听到服务后,dispatcher再将任务分派给对应的处理流程进行处理。这个模型存在一个问题就是处理过程从始至终都是单线程的,在这个连接未完成处理之前,acceptor始终无法处理下一个连接。正因为如此有了Reactor的多线程版本,多线程版本和单线程版本的最大区别就是单线程同时只能处理一个连接,而处理事件的是一个线程池,一个acceptor可以同时处理多个连接。但是面对海量连接,一个acceptor还是会存在性能问题。因此我们将一个Reactor切分开来,主Reactor负责连接,从Reactor负责业务处理,并且他们都是使用线程池进行处理,使得整体的吞吐能力大幅提升。这也就是我们Netty参照的主从Reactor模式。最后我们编写了一个Netty的HelloWorld,随后我们介绍了其中的基础组件和Netty整体的工作流程。这个部分我们没有深入源码细讲,因为会有专门的源码分析篇进行解读分析。加油,未来可期!

学习资料