java程序员要想升级高级工程师或者成为架构师,绕不开Netty的学习,就算你不做IM即时通信,也不是网络编程的工作岗位,仅仅只是CRUD程序员,当你想要了解一下Dubbo、kafka、rabbitMQ、ES、zookeeper、nginx等等的底层原理或者是源码时,你会发现他们在底层实现上都用了Netty。
那么什么是Netty呢:Netty是一个高性能、异步事件驱动的NIO框架,提供了对TCP、UDP和文件传输的支持,核心功能是让客户端和服务端两者之间进行通信交流。简单说,是对TCP/UDP编程进行了简化和封装,提供了更容易使用的网络编程接口,但凡是用java开发的跟网络IO相关的中间件,都少不了Netty的影子。
那么如何来学习Netty呢,下面是我整理的Netty学习路径,如下图:
- 学习Netty首先要了解基本的网络原理,这里不详细介绍各网络原理和协议分层了,这里需要单独讲一下Socket,它是一组接口,是应用层和传输层之间的接口,封装了TCP/UDP,更方便我们进行开发,用于在网络上实现进程之间的通信。
- 有Socket实现进程间的网络通信后,其实这里只是简单的能通信,但是要如何处理进程间的数据读写,还要了解一下BIO、NIO、IO多路复用、AIO等。
- 其中IO多路复用的底层是基于操作系统的函数来实现的,比如Linux的内核函数select、poll、epoll函数,epoll是能力最强的IO多路复用函数模型。Windows是不支持epoll的,但我们的服务器一般都是Linux,所以这里我们默认看Linux操作系统。
- 有了Selector多路复用器,就可以实现简单的IO多路复用了,也就是单线程模型。但我们为了提高IO读写性能,设计了一整套Reactor多路复用开发模型,其中分别有单Reactor单线程模型、单Reactor多线程模型、主从Reactor多线程模型,通过增加线程池的方式提高IO读写性能。
- Netty就是基于主从Reactor多线程模型实现的,再加上它的零拷贝、对象池技术,使Netty成为了一个高性能网络通信框架,广泛应用于各大互联网平台框架,如远程调用RPC、消息中间件MQ、ES、Nginx等。
在 Java 的 IO 体系中,类将近有 80 个,基本位于java.io包下,初步看起来感觉非常复杂,但是经过一番梳理之后,你会发现还是有规律可循的。
从传输数据的格式角度看,可以大致分为两组:基于字节操作的 I/O 接口:InputStream 和 OutputStream
基于字符操作的 I/O 接口:Reader 和 Writer从传输数据的方式角度看,也可以大致分为两组:
基于磁盘操作的 I/O 接口:File
基于网络操作的 I/O 接口:Socket
Socket:这篇里面要说的Socket,在java中Socket类其实并不在java.io包下,是在java.net包下。Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,是应用层和传输层之间的接口,用于在网络上实现进程之间的通信。Socket是指两个不同计算机之间的通信链路,包括IP地址和端口号。所以其实它并不在TCP五层模型中,算是TCP/UDP的封装,实在要算也应该算是传输层。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
通俗一点来理解,在网络中,通过IP地址找到网关,通过Mac地址找到子网内的机器,通过TCP/UDP协议帮我们建立端口-端口之间的通信,而一台机器中一个端口将值对应一个应用程序。socket封装了IP+端口,如果在客户端和服务器端都有一个socket封装了对方的ip和端口,那这两个socket就能相互传递信息,就如同我们每家安装的电话主机一样。
比较典型的基于 Socket 通信的应用程序场景,如下图:


简单看一个例子,了解一下Socket工作流程:
//这是客户端代码
public static void main(String[] args) throws IOException {
//通过IP和端口与服务端建立连接
Socket socket =new Socket("127.0.0.1",8080);
//将字符流转化成字节流,并输出
BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String str="Hello,我是客户端!";
bufferedWriter.write(str);
bufferedWriter.flush();
bufferedWriter.close();
}
//这是服务器端代码
public static void main(String[] args) throws Exception {
//初始化服务端socket并且绑定 8080 端口
ServerSocket serverSocket = new ServerSocket(8080);
//循环监听所有连接的客户端请求
while (true){
try {
//等待客户端的连接,会阻塞在accep
Socket socket = serverSocket.accept();
//将字节流转化成字符流,读取客户端输入的内容
//这个过程也是阻塞的,如果有连接了但是不传输内容,线程依然会阻塞在这里
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream()));
//读取一行数据
String str = bufferedReader.readLine();
//输出打印
System.out.println("服务端收到客户端发送的信息:" + str);
} catch (Exception e) {
}
}
}
我们先启动服务器端,注意,服务器端的serverSocket.accept()其实是一直阻塞等待的,我们再运行客户端,客户端发来一个连接请求,服务器端接收到后就会打印出“服务端收到客户端发送的信息:Hello,我是客户端!”这个数据,处理完这一次之后,再继续循环等待新的通信数据。
在服务端S和客户端C建立好Socket连接后,假设C向S发送了一条报文信息,服务器端操作系统收到之后,应用程序需要去操作系统中读取Socket里的报文信息(recvfrom命令),然后操作系统需要将报文数据从内核空间复制到用户空间,给到应用程序去处理。
- 如果这个过程中我们应用程序一直等到有数据发过来我们才处理,不做别的事情,就叫做阻塞IO,就好比我们一直坐在沙发上等一个电话,不干别的事。
- 如果我们边干别的事,边看电话有没有来电,这个过程我们不闲着,那么就叫非阻塞IO。
- 如果我们一直坐在沙发上等,但是我们同时盯着好几个电话,那就叫IO多路复用。
- 如果我们先干别的事,让电话来的时候会响铃提示我们,响了我们再来接电话,就叫做信号驱动IO模型。
- 如果我们再给电话一个功能叫免提,或者蓝牙耳机,不用拿着听筒接了,那么我们甚至可以边干别的事,别接听电话,这就叫异步IO。
上面这几种情况就对应我们接下来要讲的五种IO模型。
参考:《大白话详解5种网络IO模型》
参考《BIO、NIO、selector、Netty代码Demo示例》
下面主要讲五种IO模型在做read()方法调用时是怎么实现阻塞、非阻塞、io复用等的底层原理:假如我们要用套接字读取数据,此时我们必然会调用read方法,此时这个read方法就会触发操作系统内核的一次recvfrom系统调用,那么是否阻塞就取决于我们的IO模型怎么处理recvfrom命令。
blocking I/O的缩写,同步并阻塞(传统阻塞型)从下图可以看到,不管有无数据报到来,进程(线程)是阻塞于recvfrom系统调用的。
non-blocking I/O的缩写,同步非阻塞,当内核中的数据报还没准备好,此时recvfrom系统调用立即返回一个EWOULDBLOCK错误,即不会将用户进程(线程)置于阻塞状态。当内核中的数据报已经准备好时,此时recvfrom系统调用,用户进程(线程)还是会阻塞,直到内核中的数据报已经拷贝到了用户空间,此时用户进程(线程)才会被唤醒来处理接收的数据报。
与上面NIO不同的是recvfrom操作换成了select,区别是一个线程的select操作可以选择多个文件描述符,而recvfrom每次只能选一个。用一个用户线程就能监听不同channel的OP_CONNECT,OP_ACCEPT,OP_READ和OP_WRITE这些就绪事件,然后根据某个就绪事件拿到相应的channel来做对应的操作。另一个区别是select是阻塞的,虽然它能同时监听多个连接多个文件描述符。
文件描述符(fd):分别对应Java NIO中的OP_CONNECT,OP_ACCEPT,OP_READ和OP_WRITE就绪事件,下面详细介绍IO多路复用时会用到。
信号驱动IO模型在等待数据报期间是不会阻塞的,即用户进程(线程)发送一个sigaction系统调用后,此时立刻返回,并不会阻塞,然后用户进程(线程)继续执行;当数据报准备好时,此时内核就为该进程(线程)产生一个SIGIO信号,此时该进程(线程)就发生一次recvfrom系统调用将数据报从内核复制到用户空间,注意,这个阶段是阻塞的。
Asynchronous I/O的缩写,异步非阻塞,异步IO模型也很好理解,即用户进程(线程)在等待数据报和数据报从内核拷贝到用户空间这两阶段都是非阻塞的,即用户进程(线程)发生一次系统调用后,立即返回,然后该用户进程(线程)继续往下执行。当内核把接收到数据报并把数据报拷贝到了用户空间后,此时再通知用户进程(线程)来处理用户空间的数据报。也就是说,这一些列IO操作都交给了内核去处理了,用户进程无须同步阻塞,因此是异步非阻塞的。
总结一下:
从下图可以看出,除了异步IO,其它四种IO的第二步调用recvfrom其实都是阻塞的,第一步通过主动检查或者被动通知的方式实现了非阻塞的能力。
IO多路复用(IO Multiplexing)指单个进程/线程可以同时处理多个I/O请求。在unix系统中有三个内核函数select、poll、epoll,对应三种方式处理连接。
从下图可以看到,在调用select时,第3步中需要将我们所有监听的文件描述符(fd)传入内核空间中,遍历fd,如果有客户端client传入数到FDBuffer中,就会检测到某个fd就绪了,获取到就绪的fd去给服务应用程序处理。
缺点是fd_set数组有1024个限制,而且每次遍历获取就绪的fd,时间复杂度都是O(n)
poll与select处理流程类似,只是修改了存储文件描述符的fd_set从数组改成了链表,这样就没有了1024的限制。
针对select和poll的缺点,epoll做了几个优化,首先存放监控的文件描述符的fd_set改成了红黑树,然后就绪的文件描述符fd_set改成了双向链表,这样读取就绪文件描述符的时间复杂度就变成了O(1)。
client在向服务器传输数据后,就会把准备就绪的数据写入双向链表中,当有事件就绪的时候,epoll_wait只需要去检测就绪的链表中是否有数据就可以了。
执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据,所以当一个socket上有数据到了,内核再把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
另一个优化是通过内存映射(mmap),不需要从用户空间频繁拷贝数据到内核空间。
如下图,我们从jdk源码中一路追本溯源,可以看到Selector是如何一步步最终调用到底层等内核函数epoll的几个核心方法:epoll_create()、epoll_ctl()、epoll_wait()。
如下图,传统IO模型,一个连接就会分配一个线程处理请求,这样连接数多了之后,就需要分配大量的线程去处理,每个线程不仅需要分配大量的内存空间,而且线程之间的上下文切换也会极大的消耗cpu资源。如果一个连接一直没有请求,那么这个线程也将一直空置,对资源是极大的浪费。
在java1.4之后,java提供了NIO的相关API,帮助我们可以避免上面的问题。NIO编程的本质是以事件驱动来处理我们的网络事件,Reactor就是基于这套API提供的一套IO模型。
Reactor模型思想:
分而治之 + 事件驱动(优点:模块化、高性能————把大拆小,减少阻塞时间)
分而治之:一个连接里完整的网络处理过程一般分为accept、read、decode、process、encode、send(write)这几步。Reactor模式将每个步骤映射为一个Task,服务端线程执行的最小逻辑单元不再是一次完整的网络请求,而是Task,且采用非阻塞方式执行。
事件驱动:相应的Task(accept、read、write)对应特定网络事件。当Task准备就绪时,Reactor收到对应的网络事件通知,并将Task分发给绑定了对应网络事件的Acceptor和Handler执行。
Reactor三大组件:
Reactor:事件分派器,将I/O事件分派给对应的Handler和Acceptor。
Acceptor:多路复用器,处理客户端新连接,创建handler。
Handler:事件处理器,有读写请求过来时,进handler处理。
一个请求进来后,如果是连接请求,会交给accecptor创建一个对应的handler。如果是非连接请求,如读写请求,则会有一个分发器dispatch交给这个链接对应的handler来处理。
单线程模型无法充分利用我们的多核cpu的优势,同时也会因为处理速度慢导致事件堆积。相比单线程模型,单reactor多线程模型增加了线程池,
一个reactor接收所有线程的请求和响应,也会存在性能问题,所以衍生出第三种主从reactor多线程模型。mainReactor只处理acceptor(即连接请求)相关的请求,其它的handler处理的请求都交给subReactor去处理。
Netty是一款卓越的java框架,提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。它用较简单的抽象,隐藏 Java网络编程底层实现的复杂性。

- Core 核心层:
Core 核心层是 Netty 最精华的内容,它提供了底层网络通信的通用抽象和实现,包括可扩展的事件模型、通用的通信 API、支持零拷贝的 ByteBuf 等。- Protocol Support 协议支持层:
协议支持层基本上覆盖了主流协议的编解码实现,如 HTTP、SSL、Protobuf、压缩、大文件传输、WebSocket、文本、二进制等主流协议,此外 Netty 还支持自定义应用层协议。Netty 丰富的协议支持降低了用户的开发成本,基于 Netty 我们可以快速开发 HTTP、WebSocket 等服务。- Transport Service 传输服务层:
传输服务层提供了网络传输能力的定义和实现方法。它支持 Socket、HTTP 隧道、虚拟机管道等传输方式。Netty 对 TCP、UDP 等数据传输做了抽象和封装,用户可以更聚焦在业务逻辑实现上,而不必关系底层数据传输的细节。
Netty 的逻辑处理架构为典型网络分层架构设计,共分为网络通信层、事件调度层、服务编排层,每一层各司其职。

- Netty 抽象出两组线程池BossGroup和WorkerGroup,BossGroup专门负责接收客户端的连接, WorkerGroup专门负责网络的读写
- BossGroup和WorkerGroup类型都是NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环线程组, 这个组中含有多个事件循环线程 , 每一个事件循环线程是NioEventLoop
- 每个NioEventLoop都有一个selector , 用于监听注册在其上的socketChannel的网络通讯
- 每个Boss NioEventLoop线程内部循环执行的步骤有 3 步处理accept事件 , 与client 建立连接 , 生成 NioSocketChannel将NioSocketChannel注册到某个worker NIOEventLoop上的selector处理任务队列的任务 , 即runAllTasks
- 每个worker NIOEventLoop线程循环执行的步骤轮询注册到自己selector上的所有NioSocketChannel 的read, write事件处理 I/O 事件, 即read , write 事件, 在对应NioSocketChannel 处理业务runAllTasks处理任务队列TaskQueue的任务 ,一些耗时的业务处理一般可以放入TaskQueue中慢慢处理,这样不影响数据在 pipeline 中的流动处理
- 每个worker NIOEventLoop处理NioSocketChannel业务时,会使用 pipeline (管道),管道中维护了很多 handler处理器用来处理 channel 中的数据
| 分类 | Netty特性 |
|---|---|
| 设计 | 多协议支持:Netty提供了丰富的协议支持,包括常用的网络协议(如HTTP、WebSocket、TCP和UDP)以及自定义协议。具备灵活的编解码器和处理器,简化了协议的实现和交互。针对多种传输类型的统一接口,支持阻塞和非阻塞简单但更强大的线程模型真正的无连接的数据报套接字支持链接逻辑支持复用可扩展和灵活:Netty的架构和组件设计具有高度的可扩展性和灵活性。它提供了一组可重用的组件,可以根据应用需求进行定制和扩展。 |
| 易于使用 | 完善的JavaDoc,用户指南和样例简洁简单 |
| 性能 | 高性能:Netty采用了一系列优化策略,如零拷贝技术、内存池和可定制的线程模型等,以提供出色的性能和吞吐量。能处理高负载和大规模并发 比核心 Java API 更好的吞吐量,较低的延时Netty能够高效地处理并发连接和大量的并发请求。资源消耗更少,这个得益于共享池和重用减少内存拷贝 |
| 健壮性 | 消除由于慢,快,或重载连接产生的 OutOfMemoryError消除经常发现,在 NIO 在高速网络中的应用中的不公平的读/写比 |
| 安全性 | 完整的 SSL / TLS 和 StartTLS 的支持可运行在受限的环境例如 Applet 或 OSGI |
| 社区驱动 |
Bootstrap和ServerBootstrap:当需要连接客户端或者服务器绑定指定端口时需要使用Bootstrap,ServerBootstrap有两种类型,一种是用于客户端的Bootstrap,一种是用于服务端 的ServerBootstrap。
Channel:相当于socket,与另一端进行通信的通道,具备bind、connect、read、write等IO操作的能力。
EventLoop:事件循环,负责处理Channel的IO事件,一个EventLoopGroup包含多个EventLoop,一个EventLoop可被分配至多个Channel,一个Channel只能注册于一个EventLoop,一个EventLoop只能与一个Thread绑定。
ChannelFuture:channel IO事件的异步操作结果。
ChannelHandler:包含IO事件具体的业务逻辑。
ChannelPipeline:ChannelHandler的管道容器。
组件关系梳理:
- 服务端启动初始化时有 Boss EventLoopGroup 和 Worker EventLoopGroup 两个组件,其中 Boss 负责监听网络连接事件。当有新的网络连接事件到达时,则将 Channel 注册到 Worker EventLoopGroup。
- Worker EventLoopGroup 会被分配一个 EventLoop 负责处理该 Channel 的读写事件。每个 EventLoop 都是单线程的,通过 Selector 进行事件循环。
- 当客户端发起 I/O 读写事件时,服务端 EventLoop 会进行数据的读取,然后通过 Pipeline 触发各种监听器进行数据的加工处理。
- 客户端数据会被传递到 ChannelPipeline 的第一个 ChannelInboundHandler 中,数据处理完成后,将加工完成的数据传递给下一个 ChannelInboundHandler。
- 当数据写回客户端时,会将处理结果在 ChannelPipeline 的 ChannelOutboundHandler 中传播,最后到达客户端。
从宏观来讲,Netty的高性能主要在于:Reactor模式、Zero Copy(零拷贝)和对象池
通过设置不同的启动参数,Netty可以同时支持单Reactor单线程模型、单Reactor多线程模型和主从Reactor多线层模型。
Netty 通过 Reactor 模型基于多路复用器接收并处理用户请求,内部实现了两个线程池, boss 线程池和 worker 线程池,其中 boss 线程池的线程负责处理请求的 accept 事件,当接收 到 accept 事件的请求时,把对应的 socket 封装到一个 NioSocketChannel 中,并交给 worker 线程池,其中 work 线程池负责请求的 read 和 write 事件,由对应的 Handler 处理。
1. Netty线程模型–Reactor 模型:
2、Zero Copy
Zero Copy技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域,这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。举例来说,如果要读取一个文件并通过网络发送它,传统方式下每个读/写周期都需要复制两次数据和切换两次上下文,而数据的复制都需要依靠CPU。通过零复制技术完成相同的操作,上下文切换减少到两次,并且不需要CPU复制数据。3、对象池
对象池模式(The Object Pool Pattern)是单例模式的一个变种,对象池模式管理一个可代替对象的集合,组件从池中借出对象,用它来完成一些任务并当任务完成时归还该对象。Netty中的Recycler,该类是个容器,基于ThreadLocal实现的的轻量级对象池,内部主要是一个Stack结构。当需要使用一个实例时,就弹出,当使用完毕时,就清空后入栈。
视频学习Netty:【【面试精华版】整整600集,秋招一周如何逼自己快速通关Java面试?】视频文档参考百度网盘
参考:《BIO、NIO、selector、Netty代码Demo示例》
参考:《Netty 中的零拷贝机制》
参考:《Netty核心概念、架构及用法》
参考:《大白话详解5种网络IO模型》