IO模型及Netty线程模型

Netty框架详解
Netty是基于NIO的异步网络通信框架,用于快速搭建高性能网络应用。支持统一API,高性能,安全特性,广泛应用于互联网、游戏、大数据等领域。

Netty简介


1. 简介

版本:Netty4.X
Netty是基于NIO的异步网络通信框架
能快速的搭建高性能易扩展的网络应用程序(服务器/客户端)

2. 特征

设计

  • 适用于各种传输类型的统一API-阻塞和非阻塞套接字
  • 基于灵活且可扩展的事件模型,可将关注点明确分离
  • 高度可定制的线程模型-单线程,一个或多个线程池
  • 真正的无连接数据报套接字支持(从3.1开始)

性能

  • 更高的吞吐量,更低的延迟
  • 减少资源消耗
  • 减少不必要的内存复制

安全

  • Complete SSL/TLS and StartTLS support
  • 完整的SSL / TLS和StartTLS支持

而如果对这些特点进行细化,则可以得出:

  1. 基于事件机制(Pipeline - Handler)达成关注点分离(消息编解码,协议编解码,业务处理)
  2. 可定制的线程处理模型,单线程,多线程池等
  3. 屏蔽NIO本身的bug
  4. 性能上的优化
  5. 相较于NIO接口功能更丰富
  6. 对外提供统一的接口,底层支持BIO与NIO两种方式自由切换

3. 核心模块

  • Extensible Event Model:可扩展的事件模型
  • Universal Communication API : 通用的通讯API
  • Zero-Copy-Capable Rich Byte Buffer : 零拷贝的字节缓冲区
  • Transport Services

Socket&Datagrame:TPC、UDP传输实现
HTTP Tunnel:http传输协议实现
In-VM pipe:内部JVM传输实现

  • Protocol Support

http/SSL/Google
压缩、大文件传输协议、实时流传输协议


二. 应用领域


a、互联网行业

阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。除了 Dubbo 之外,淘宝的消息中间件RocketMQ 的消息生产者和消息消费者之间,也采用 Netty 进行高性能、异步通信。除了阿里系和淘宝系之外,很多其它的大型互联网公司或者电商内部也已经大量使用 Netty 构建高性能、分布式的网络服务器

b、游戏行业

无论是手游服务端、还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈,非常方便定制和开发私有协议栈。账号登陆服务器、地图服务器之间可以方便的通过 Netty 进行高性能的通信

c、大数据领域

经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨节点通信,它的 Netty Service 基于 Netty 框架二次封装实现。大数据计算往往采用多个计算节点和一个/N个汇总节点进行分布式部署,各节点之间存在海量的数据交换。由于 Netty 的综合性能是目前各个成熟 NIO 框架中最高的,因此,往往会被选中用作大数据各节点间的通信。

e、通信行业

Netty 的异步高性能、高可靠性和高成熟度的优点,使它在通信行业得到了大量的应用。


三. IO模型

a. BIO—阻塞I/O

在这里插入图片描述

  • 一个线程负责连接,多线程则为每一个接入开启一个线程
  • 一个请求一个应答
  • 请求之后应答之前客户端会一直等待(阻塞)

b. NIO—同步非阻塞I/O

在这里插入图片描述

  • 一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上
  • 多路复用器轮询到连接有I/O请求就进行处理

NIO的三大核心部分:

  • Channel(通道):和IO中的Stream是差不多一个等级的,只不过Stream是单向的,而Channel是双向的,既可以用来读操作,也可以用来写操作
  • Buffer(缓冲区):实际上是一个容器,一个连续数组,Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer
  • Selector(复用器):将Channel注册到Selector上,Channel必须处于非阻塞模式下(channel.configureBlocking(false))

NIO和传统的IO之间最大的一个区别就是,IO是面向流的,NIO是面向缓冲区的。java IO面向流意味着每次从流中读一个或多个字节,知道读取所有字节,它们没有被缓存在任何地方。

c. AIO—异步非阻塞

NIO2,异步IO模型,底层完全使用异步回调的方式来实现,但是由于AIO这项技术在Linux操作系统上还不太成熟,所以通常也不会说太多关于这方面的内容

Linux I/O模型

在Linux中,将I/O模型分为五种类型:

  • 阻塞式I/O模型
  • 非阻塞式I/O模型
  • I/O复用模型
  • 信号驱动式 I/O 模型
  • 异步 I/O模型

Linux中的 I/O 流程概括来说可以分为两步:

  1. 等待数据准备好(waiting for the to be ready)
  2. 从内核向进程复制数据(copying the data from the kernel to the process)

在这里插入图片描述

从上面的流程可以看出,首先是内核空间把数据从硬件(磁盘)中读到内核空间的缓冲区中,进行这一步操作时,数据处于内核态,通过磁盘驱动器,把数据从磁盘读到内核缓冲区来。下一步是把数据从内核缓冲区拷贝到用户空间的缓冲区,数据由内核态变为用户态。

知识普及:Linux中的程序大致运行在两个空间,自己的程序运行在用户空间,用户空间的权限有限,相对的另外一个空间是内核空间,比如驱动程序或者一些核心的系统调用都是在内核空间完成的。我们的I/O操作当在一个用户空间做系统调用的时候,数据会自动从用户态变到内核态来进行操作,完成了以后再从内核空间拷贝到用户空间。这种状态的切换是为了保证安全。比如在我们32位的操作系统中,大部分是4G的内存,一般前面一个G主要是内核态使用,后面三个G是用户态使用。

阻塞式 I/O 模型

当用户进程调用recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据一开始还没有到达。比如,还没有收到一个完整的数据包,这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态。所以,blocking IO的特点是在IO执行的两个阶段都被block了。这就是阻塞式IO模型。明显可见效率不会高。(小明从家里面先到演唱会现场问售票业务员买票,但是票还没出来,三天以后才出来,小明直接打了个地铺睡在举办商售票大厅,一直等票出来,然后买票。)

非阻塞式 I/O 模型

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么他并不会block用户进程,而是立刻返回一个error。从用户进程角度讲,他发起一个read操作后,并不需要等待,而是马上就得到一个结果,用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作,一旦kernel中的数据准备好了,并且又再次收到用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,nonblocking IO的特点时用户进程需要不断的主动询问kernel数据好了没有。但是,从内核空间拷贝到用户空间,让数据从内核态变为用户态,这个过程在非阻塞式 I/O 模型中,也是阻塞的!所以效率也不会太高。(小明从家里面先到演唱会现场问售票业务员买票,但是票还没出来,然后小明走了,办理其他事情去了,然后过了2个小时,又去举办商售票大厅买票来了,如果票还没有出来,小明又先去办其他事情了,重复上面的操作,直到有票可以买。)

I/O 复用模型

复用的意思是,系统一次去查看多个io的进度,看哪一个有了结果,对于有结果的,就开始执行下面从内核空间拷贝到用户空间的操作,这个模型和上面非阻塞式 I/O 模型的区别是,非阻塞式 I/O 模型一次只看一个结果好了没有,而IO复用模型一次可以查看多个,一次监控一批系统调用好了没有,这是复用模型的特点。在一定程度上,能提高一些效率。(小明想买票看演唱会,都直接给黄牛(selector/epoll)打电话了,说帮我留意买个票,票买了通知我,我自己去取(当我接到黄牛的电话时,我需要花费整个路成的时间去读这个数据,买拿这个票),那么票没出来之前,小明完全可以做自己的事情。)

信号驱动式 I/O 模型

在Linux系统中,有一种信号机制,也就是说调用方可以注册一个信号,当系统调用完成之后,可以通知这个信号,那注册信号的人就会知道这个请求已经完成了,这种信号机制应用在IO当中就是信号驱动式IO。这样就不是查看结果是否好了,而是被通知的一种方式。不过通知后,从内核态到用户态这个过程还是阻塞的(小明想买票看演唱会,给举办商售票业务员说,给给你留个电话,有票了请你给我打个电话通知一下(是看人家操作系统提不提供这种功能,Linux提供,windows没有这种机制),我自己再来买票(小明完全可以做自己的事情,但是票还是需要小明自己去拿的)

异步 I/O 模型

用户进程发起read操作之后,立刻就可以开始去做其它的事,而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。(小明想买票看演唱会,给举办商售票业务员说(异步非阻塞i/o)打电话了,给你留个地址,有票了请通知快递员,把这张票送到这个地址来,当小明听到敲门声,看见快递员,就知道票好了,而且指导票好了的时候,票已经到他手上了,票不用小明自己去取(应用不用自己再去read数据了))


四. Netty线程模型


Reactor线程模型

在这里插入图片描述

  • Reactor模式,通过一个或多个输入同时传递给服务处理器模式(基于事件驱动)
  • 服务器程序处理传入的多个请求并将他们同步分派到相应的处理线程,因此Reactor模式也叫Dispatcher模式
  • Reactor模式使用IO复用监听事件

Reactor单线程模型

在这里插入图片描述
流程:

  1. 其中客户端发送请求至服务器,Reactor响应其IO事件。
  2. 如果是建立连接的请求,则分发至acceptor,由其接收连接,然后再将其注册至分发器
  3. 如果是读请求,则分发至Handler,由其读出请求内容,然后对内容进行解码,然后处理运算,再对响应编码,最后发送响应。
  4. 再整个过程中都是使用单线程,无论是Reactor线程和后续的Handler处理都只使用了一个线程。

Reactor单线程模型:

Reactor单线程模型仅使用一个线程来处理所有的事情,包括客户端的连接和服务器的连接,以及所有连接产生的读写时间,这种模型需要使用同步非阻塞I/O,使得每一个操作都不会发生阻塞,Handler为具体的处理事件的处理器,而Acceptor为连接的接收者,作为服务端接收来自客户端的链接请求。这样的线程模型理论上可以仅仅使用一个线程就完成所有的事件处理,显得线程的利用率非常高,而且因为只有一个线程在工作,所有不会产生多线程环境下会发生的各种多线程之间的并发问题。架构简单明了,线程模型的简单性决定了线程管理工作的简单性。但是这样的线程模型存在很多不足

缺点:

1、仅利用一个线程来处理事件,对于目前普遍多核处理器来说太过浪费资源
2、一个线程同时处理N个连接,管理起来较为复杂,而且性能也无法得到保证,这是以线程管理的简洁换取来的事件管理的复杂性,而且是在性能无 法得到保证的前提下换取的,在大流量的应用场景下根本没有实用性
3、根据第二条,当处理的这个线程负载过重之后,处理速度会变慢,会有大量的事件堆积,甚至超时,而超时的情况下,客户端往往会重新发送请求,这样的情况下,这个单线程的模型就会成为整个系统的瓶颈
4、单线程模型的一个致命缺钱就是可靠性问题,因为仅有一个线程在工作,如果这个线程出错了无法正常执行任务了,那么整个系统就会停止响应,也就是系统会因为这个单线程模型而变得不可用,这在绝大部分场景(所有)下是不允许出现的

Reactor多线程模型:

在这里插入图片描述
流程:

  • Selector对象通过select监控客户端请求事件,收到事件后,通过Dispatch进行分发
  • 如果建立连接请求,则由Acceptor通过accept处理连接请求,然后创建一个Handler处理连接完成后的各种事件
  • 如果不是连接请求,则由reactor分发调用连接对应的Handler来处理
  • Handler只负责响应事件,不做具体的业务处理,通过read读取数据后,会分发给后面的worker线程池的某个线程处理业务
  • worker线程池会分配独立的线程完成真正的业务,并将结果返回给Handler。
  • handler收到响应后,通过send将结果返回给client

Reactor多线程模型:

多线程模型下,接收连接和处理请求作为两部分分离了,而Acceptor使用单独的线程来接收请求,做好准备后就交给事件处理的handler来处理,而handler使用了一个线程池来实现,这个线程池可以使用Executor框架实现的线程池来实现,所以,一个连接会交给一个handler线程来处理其上面所有的事件,需要注意的是:一个连接只会由一个线程来处理,而多个连接可能会由一个handler线程来处理,关键在于一个连接上的所有事件都只会由一个线程来处理,这样的好处就是消除了不必要的并发同步的麻烦

缺点:

多线程模型下任然只有一个线程来处理客户端的连接请求,那如果这个线程挂了,那整个系统任然会变为不可用,而且,因为仅仅由一个线程来负责客户端的连接请求,如果连接之后要做一些验证之类复杂耗时操作再提交给handler线程来处理的话,就会出现性能问题。

Reactor主从线程模型

在这里插入图片描述
流程:

  • Reactor主线程MainReactor对象通过select监听连接事件,收到事件后,通过Acceptor处理连接事件
  • 当Acceptor处理连接事件后,MainReactor将连接分配给SubReaactor
  • SubReactor将连接加入连接队列进行监听,并创建Handler进行各种事件处理
  • 当有新事件发生时,subReactor就会调用对应的Handler处理
  • handler通过read读取数据,分发给后面的worker线程处理
  • worker线程池分配独立的线程worker线程进行业务处理,并返回结果
  • handler收到响应结果后,再通过send将结果返回给client
  • Reactor主线程可以对应多个Rreactor子线程,即MainReactor可以对应多个SubReactor
### Netty 线程模型概述 Netty 是一种基于 NIO 的高性能网络框架,它的线程模型设计非常精巧,能够高效处理大量并发连接。Netty 使用了 Reactor 模式来实现事件驱动机制,其中核心组件之一就是 `EventLoop` 其对应的线程池。 #### 1. **Reactor 模型** Netty 基于经典的 Reactor 设计模式运行[^1]。在这种模式下,I/O 操作由专门的线程负责监听并分发到业务逻辑处理器中执行。具体来说,Netty 将 I/O 多路复用器(Selector)封装到了 EventLoop 中,使得开发者可以专注于编写业务逻辑而无需关心底层复杂的 I/O 实现细节。 #### 2. **EventLoop 组件** 在 Netty 中,`EventLoop` 负责管理一组 Channel 并轮询这些 Channel 上发生的事件。每一个 `Channel` 都绑定到唯一的 `EventLoop` 实例上,在整个生命周期内不会改变这种绑定关系[^2]。这意味着对于同一个客户端请求而言,所有的读写操作都会被分配给相同的线程去完成,从而避免了跨线程共享状态所带来的同步开销。 #### 3. **线程分工** 通常情况下,Netty 的线程分为两类:Boss Group Worker Group。 - **Boss Group**: 主要职责在于接受新的连接请求并将新建立好的 Socket 分配至下一个阶段——Worker Group 进行进一步处理; - **Worker Group**: 则承担起实际的数据收发任务以及调用用户定义的各种回调方法来响应不同的网络行为(比如接收到消息后的解码编码过程)[^3]。 当一个新的 TCP 客户端尝试接入服务器时,流程如下所示: ```plaintext Client -> BossGroup (accepts connection) -> WorkerGroup (handles IO operations) ``` 每组都维护着自己的 NioEventLoop 或 EpollEventLoop 对象集合,并通过它们内部所包含的选择键集不断查询是否有就绪的任务待处理;一旦发现某个信道上有可利用资源,则立即触发相应的方法链路直至最终达成目标功能为止。 #### 4. **性能优化策略** 为了提高系统的吞吐量降低延迟时间,可以通过调整以下几个方面来进行针对性改进: - **增加线程数**:适当增大 boss/worker groups size 参数可以让更多 CPU 参与进来共同协作完成繁重的工作负载; - **亲缘调度**:如果硬件支持 NUMA 架构的话,考虑让特定范围内的 channel 总是由固定几个临近物理位置上的 core 来服务可能会带来额外好处因为减少了内存访问跨越节点带来的成本; - **零拷贝技术应用**:充分利用操作系统层面提供的 sendfile() 函数或者 mmap 映射文件等方式减少不必要的数据搬移动作次数进而提升效率. 以下是创建自定义配置的一个简单例子展示如何设置多线程环境下的 netty server 启动参数: ```java import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; public class NettyServer { public static void main(String[] args) throws Exception { int port = 8080; // 创建两个线程组分别用于接收新连接(boss group)服务已存在的连接(worker group). EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class); // 设置其他选项... ChannelFuture f = b.bind(port).sync(); System.out.println("Server started and listen on " + f.channel().localAddress()); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值