第 1 部分 楔子
作为一个技术人,应该时刻保持对未知世界的饥饿感和好奇心。
对于一个优秀的开源框架的源码,我们都会怀着崇敬的心情去阅读。 对于如何阅读源码,简单总结如下:
第一步是学习如何使用该框架。 至少我们可以独立跑起入门级项目,无障碍调试。
第二步,在阅读框架源码之前,我们首先查看官网或者网上资料,了解框架的整体架构设计、核心组件以及一些相关概念。 心胸中的山河,了解数据的大体流程。 另外,要充分规划,做好知识点预热。 比如:Netty相关的NIO、IO复用模型、Reactor线程模型、阻塞IO、非阻塞IO、同步调用、异步调用等,这是我们需要提前掌握的基础。 否则理解起来会很混乱。
第三步,我们对比第二步的框架设计。 我们分析框架源码的包结构,了解框架中有哪些包以及负责哪些模块。 对于框架的一些核心组件类,我们最好先了解一下。 类继承图,这样我们在阅读源码的过程中,就不会莫名其妙的找不到方法真正的调用类了。
第四步,开始阅读部分核心源码。 优秀的框架通常是大的、全面的、功能齐全的。 如果你一一阅读,你就会迷失其中。 所以我们需要先抓住第二步的核心组件,阅读关键对象,理解它们的领域概念和设计思想,理清脉络。 然后在逐步的依赖调用中,慢慢延伸、扩展,最终波及到整个框架。 在阅读过程中,记得将复杂的调用部分画出数据流程图,以方便理解。 这一部分我们也熟悉了整个数据流程。 在这一部分中,我们研究设计思想和概念。
第五步,精雕细琢,查漏补缺。 框架中的某些模块可能是相对独立的。 例如 Netty 中的编解码器。 还有一些地方属于框架的基础核心代码,比如:粘包拆包、对象池、高性能无锁队列、FastThreadLocal、零拷贝、Futrue&Promise等,我们需要阅读和理解word通过言语。 这部分我们需要吸收的是编程的方法和思维。
最后但并非最不重要的一点是,这是我们最容易忽视的一点。 尽可能自动模仿并实现一个简单版本的框架。 验证您自己的学习和理解。
以上只是个人的一些看法,欢迎大家交流学习。 下面开始正文。
第 2 部分 Netty 的过去和现在
什么是 Netty
Netty是一个高性能、异步风暴驱动的NIO框架。 它提供对 TCP、UDP 和文件传输的支持。 作为一个异步NIO框架,Netty的所有IO操作都是异步且非阻塞的。 通过Future-Listener机制,用户可以方便地主动或通过通知机制获取IO操作结果。
Netty作为目前最流行的NIO框架,已经广泛应用于互联网领域、大数据分布式估计领域、游戏行业、通信行业等,业界一些知名的开源组件也是基于Netty的NIO框架。
目前Netty官方推荐的版本是4.X版本。 由于5.X版本引入了ForkJoin线程池,代码复杂,性能利润一般,所以暂时视为废弃版本。
Netty的优点
1、API简单易用,开发门槛低;
2.功能强大,预设多种编解码功能,支持多种主流合约;
3、定制能力强手游辅助源码实例,可通过ChannelHandler灵活扩展通信框架;
4、高性能。 与业界其他主流NIO框架相比,Netty综合性能最好;
5、成熟稳定,Netty已经修补了所有已经发现的JDK NIO bug,业务开发者无需再担心NIO bug;
6.社区活跃,版本迭代周期短,发现的Bug可以及时修复,并会增加更多新功能;
7、经历过大规模商业应用测试,品质得到验证。 已在互联网、大数据、网络游戏、企业应用、电信软件等多个行业成功商用,证明其早已能够充分满足不同行业的商业应用。
Netty的应用场景
1、构建各种高性能、低信噪比的Java中间件,如MQ、分布式服务框架、ESB消息总线等。Netty主要作为基础通信框架,提供高性能、低信噪比的通信服务; 典型应用包括:阿里分布式服务框架Dubbo、淘宝的消息中间件RocketMQ等。
2、开发公共或私有协议栈的基础通信框架,例如基于Netty构建异步高性能的WebSocket协议栈;
3、针对各个领域的应用,如大数据、游戏等,采用Netty作为高性能通信框架,对内部模块进行数据分发、传输、聚合,实现模块间的高性能通信。 Avro是经典的Hadoop高性能通信和序列化组件,其RPC框架默认使用Netty进行跨界点通信。 其Netty Service是基于Netty框架的二次封装实现的。
Netty和Mina的比较
1.社区活动。
Netty 的社区活跃度远低于 Mina。 Netty还有JBoss作为强有力的背书。 基于这个考虑,Netty绝对是首选。
2.任务调度精细化。
Netty4.x的任务队列被细化为3个队列。 执行优先级:taskQueue>scheduledTaskQueue>tailTasksQueue。 这样,任务执行的公平性就更好了。
Part 3 源码预热知识点
核心理念
阻塞调用和非阻塞调用的区别
它们之间的差异表明了调用者的状态。
阻塞调用——直到调用结果返回之前,当前线程会被挂起,调用线程只有得到结果后才能返回。 呼叫者仍在等待,没有执行任何其他操作。
同步处理和异步处理的区别
它们之间的区别在于被调用者的状态。
同步处理——被调用者在返回给调用者之前获得最终结果。
异步处理——被调用者先返回响应,然后估计调用结果,计算出最终结果后通知并返回给调用者
举个形象的栗子:
老李喜欢喝水,也喜欢烧水。
1、老李把水壶放在火上,等水烧开。 (同步阻塞)
老李觉得自己有点傻。
2、老李把水壶放在火上,去书房看电视,时不时去卧室看看水烧开没有。 (同步非阻塞)老李还是觉得自己有点傻。 于是我买了一个可以吹笛子的水壶,水烧开的时候会发出声音。
3、老李把水壶放在火上,等水烧开。 (异步阻塞)
4、老李把水壶放在火上,就去书房看电视了。 在水壶响之前他不再看水壶,当水壶响时他就去拿水壶。 (异步非阻塞)
五种常见的网络IO模型
1. 块I/O
在阻塞I/O模型中,从调用recvfrom到返回数据报期间,应用程序处于阻塞状态。 当recvfrom成功返回后,应用进程开始处理数据报。
比喻:一个人钓鱼,没有鱼上钩,他就坐在河边等待。
优点:程序简单,进程/线程在阻塞等待数据时被挂起,基本不占用CPU资源。
缺点:每个连接需要由独立的进程/线程处理。 当并发请求数量较大时,为了维护程序,内存和线程切换的成本是比较大的。 该模型在实际生产中很少使用。
2.非阻塞I/O
在非阻塞I/O模型中,应用程序将套接字设置为非阻塞,告诉内核当请求的I/O操作难以完成时不要让进程进入睡眠状态。
而是返回错误,应用程序根据I/O操作函数继续判断数据是否已经准备好。 如果没有,继续寻址,直到数据准备好。
比喻:钓鱼时玩手机,过一会儿看看有没有鱼上钩,有的话赶紧拉竿。
优点:不会阻塞内核等待数据的过程,每次发起的I/O请求都可以立即返回,无需阻塞等待,实时性能较好。
缺点:轮询会不断询问内核,会占用大量CPU时间,系统资源利用率低,所以通常Web服务器不使用这些I/O模型。
3. 多路复用NIO(IO Multiplexing)、事件驱动IO(Event Drive IO)
select/epoll的优点不是可以更快地处理单个连接,而是可以处理更多的连接
其次手游辅助源码实例,该模型混合了风暴检测和事件响应。 一旦风暴响应的执行体庞大,对整个模型来说将是灾难性的。
幸运的是,有许多高效的风暴驱动库可以屏蔽上述困难。 常见的storm驱动库有libevent库和作为libevent替代品的libev库。 这些库会根据操作系统的特点选择最合适的风暴检测套接字,并添加信号等技术来支持异步响应,这使得该库成为构建风暴驱动模型的最佳选择。
在I/O复用模型中,会用到Select或Poll函数或者Epoll函数(Linux 2.6以后内核支持)。 这两个函数也会阻塞进程,但与阻塞I/O不同。
这两个函数可以同时阻塞多个I/O操作,可以同时检查多个读操作和多个写操作的I/O函数,直到有数据可读或有数据时才真正调用I/O可写。 Ø 操作功能。
打个比方:放一堆鱼竿,还在河边守着一堆渔具,没鱼上钩就玩手机。
优点:基于阻塞对象,可以同时等待多个描述符,而不是使用多线程(每个文件描述符一个线程),可以大大节省系统资源。
缺点:当连接数较少时,效率比多线程+阻塞I/O模型低,并且延迟可能较大,因为单个连接处理需要2次系统调用,耗时会减少。 Nginx 等高性能互联网反向代理服务器成功的关键是受益于 Epoll。
下图展示了目前复用的几种重要实现方式。
选择、轮询、epoll、kqueue。
4. 信号驱动模型
在信号驱动I/O模型中,应用程序使用socket接口进行信号驱动I/O,并安装信号处理函数,进程继续运行而不会阻塞。
当数据准备好后,进程会收到SIGIO信号,可以在信号处理函数中调用I/O操作函数来处理数据。
打个比方:钓鱼竿上系着一个铃铛。 当铃声一响,你就知道鱼已经上钩了,你就可以专心玩手机了。
优点:线程在等待数据时不会被阻塞,可以提高资源利用率。
缺点:大量IO操作时,可能会因信号队列溢出而导致信号I/O无法得到通知。
信号驱动的 I/O 虽然对于处理 UDP 套接字很有用,但这些信号意味着数据报的到达或异步错误的返回。
但对于 TCP 来说,信号驱动的 I/O 方式几乎没有什么用处,因为引起这些通知的条件有很多,对每一个进行判断都会消耗大量的资源,失去了与 TCP 相比的优势。以前的方法。
5. 异步IO模型
根据 POSIX 规范的定义,应用程序告诉内核开始一项操作,并让内核在整个操作(包括将数据从内核复制到应用程序的缓冲区)完成时通知应用程序。
该模型与信号驱动模型的主要区别在于,信号驱动I/O是内核通知应用程序何时开始I/O操作,而异步I/O模型是内核通知应用程序何时开始I/O操作。 I/O 操作完成。
优点:异步 I/O 可以充分利用 DMA 功能,允许 I/O 操作与估计重叠。
缺点:要实现真正的异步I/O,操作系统需要做很多工作。 目前,真正的异步I/O是在Windows下通过IOCP来实现的。
Linux系统下,Linux 2.6推出,目前还没有建立AIO,所以在Linux下实现高并发网络编程时,IO复用模型是主要模式。
5种IO模型对比:
从上图我们可以看出,越往前,阻塞越少,理论上效率最优。
五种I/O模型中,前四种是同步I/O,因为真正的I/O操作(recvfrom)会阻塞进程/线程,只有异步I/O模型才兼容异步I/O由 POSIX 定义。 匹配。
常见的线程模型
1.传统同步阻塞模型
特征:
1)使用阻塞I/O模型获取输入数据;
2)每个连接都需要一个独立的线程来完成数据输入、业务处理、数据返回的完整操作。 单连接。
有一个问题:
1)当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大;
2)连接完成后,如果当前线程暂时没有数据可读取,则线程会阻塞在Read操作上,造成线程资源的浪费。
2. 反应器模型
针对传统阻塞I/O服务模型的两个缺点,比较常见的解决方案如下:
1)基于I/O复用模型:多个连接共享一个阻塞对象,应用程序只需等待一个阻塞对象,而不需要阻塞等待所有连接。 当连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始业务处理;
2)基于线程池复用线程资源:不再需要为每个连接创建一个线程,连接完成后的业务处理任务分配给线程处理,一个线程可以处理的业务多个连接。
反应堆模式是指通过一个或多个输入同时传递到服务处理器的服务请求的风暴驱动处理模式。 服务器程序处理传入的多通道请求,并将它们同步分派到与请求对应的处理线程。 Reactor模式也称为Dispatcher模式。
也就是说I/O更加复用和统一来拦截风暴,并在收到风暴后进行分发(Dispatch到某个进程),这是编译高性能网络服务器的必备技术之一。
Reactor 模式有 2 个关键组件:
1)Reactor:Reactor运行在一个单独的线程中,负责监听和调度storms,调度到合适的处理程序来响应IO事件。 它充当公司的电话接线员,接听客户的电话并将线路转接到适当的联系人;
2) 处理程序:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之聊天的公司中的实际腐败官员。 Reactor 通过调度适当的处理程序来响应 I/O 事件,这些处理程序执行非阻塞操作。
根据Reactor的数量和处理资源池线程的数量,有3种典型的实现方式:
1)单Reactor单线程;
2)单Reactor多线程;
3)主从Reactor多线程。
单Reactor单线程模型
其中,Select是上述I/O复用模型引入的标准网络编程API,可以实现应用程序通过阻塞对象窃听多个连接请求,其他方案的示意图类似。
计划说明:
1)Reactor对象通过Select监听客户端请求风暴,收到风暴后通过Dispatch进行分发;
2)如果是连接请求风暴,则Acceptor通过Accept处理连接请求,连接完成后创建Handler对象来处理后续的业务处理;
3)如果不是要建立连接风暴,Reactor会调度并调用该连接对应的Handler进行响应;
4)Handler会完成Read→业务处理→Send的完整业务流程。
优点:模型简单,不存在多线程、进程通信、竞争等问题,全部在一个线程中完成。
缺点:性能问题,只有一个线程,无法充分利用多核CPU的性能。 当Handler处理某个连接上的业务时,整个流程无法处理其他连接干扰,很容易造成性能困难。
可靠性问题,线程意外跑掉,或者进入死循环,都会导致整个系统通信模块不可用,无法接收和处理外部消息,导致节点故障。
使用场景:客户端数量有限,业务处理速度很快,比如Redis,业务处理时间复杂度为O(1)。
单Reactor多线程模型
计划说明:
1)Reactor对象通过Select监听客户端请求风暴,收到风暴后通过Dispatch进行分发;
2)如果是构建连接请求风暴,Acceptor会通过Accept处理连接请求,然后创建Handler对象来处理连接并完成后续风暴;
3)如果不是要建立连接风暴,Reactor会调度并调用该连接对应的Handler进行响应;
4)Handler只负责响应风暴,不做具体的业务处理。 通过Read读取数据后,会分发到附近的Worker线程池进行业务处理;
5)Worker线程池会分配独立的线程来完成真正的业务处理,如何将响应结果发送给Handler进行处理;
6)Handler收到响应结果后,通过Send将响应结果返回给Client。
优点:可以充分利用多核CPU的处理能力。
缺点:多线程数据共享和访问比较复杂; Reactor承担了所有的风暴窃听和响应,运行在单线程中,在高并发场景下很容易成为性能困境。 Reactor运行在单线程中,在高并发场景下很容易成为性能困境。 Reactor 可以在多个线程中运行。
Active Reactor多线程模型
在单Reactor多线程模型中,Reactor运行在单线程中,在高并发场景下很容易成为性能困境。 您可以使 Reactor 在多线程中运行。
计划说明:
1)Reactor主线程MainReactor对象通过Select监听并构建连接风暴,通过Acceptor接收风暴,并对连接风暴进行处理和构建;
2)Acceptor处理完连接风暴的构建后,MainReactor将Reactor子线程的连接分配给SubReactor处理;
3)SubReactor将连接添加到连接队列中用于监听,并创建Handler来处理各种连接干扰;
4)当新的风暴发生时,SubReactor会调用该连接对应的Handler进行响应;
5)Handler通过Read读取到数据后,会分发到附近的Worker线程池进行业务处理;
6)Worker线程池会分配独立的线程来完成真正的业务处理,如何将响应结果发送给Handler进行处理;
7)Handler收到响应结果后,通过Send将响应结果返回给Client。
优点:父线程和子线程数据交互简单,职责明确。 父线程只需要接收新的连接,子线程完成后续的业务处理。
父线程和子线程之间的数据交互很简单。 Reactor主线程只需要将新的连接传递给子线程,子线程不需要返回数据。
该模型在很多项目中得到广泛应用,包括Nginx主从Reactor多进程模型、Memcached主从多线程、Netty主从多线程模型支持。
Reactor模式具有以下优点:
1)响应快,不必被单次同步时间阻塞,虽然Reactor本身仍然是同步的;
2)编程比较简单,可以最大程度避免复杂的多线程和同步问题,避免多线程/进程切换开销;
3)可扩展性,通过减少Reactor实例的数量,可以方便地充分利用CPU资源;
4)可重复使用性。 Reactor模型本身与具体的风暴处理逻辑无关,具有较高的复用性。
3.前摄器模型
在Reactor模式下,Reactor等待风暴或适用或操作状态发生(例如文件描述符可以读写,或者Socket可以读写)。
然后将这个storm传递给预先注册的Handler(事件处理函数或者回调函数),前者会做实际的读写操作。
所有的读写操作都需要应用程序同步,因此Reactor是一种非阻塞同步网络模型。
如果将I/O操作改为异步,即交给操作系统来进一步提高性能。 这就是异步网络模型Proactor。
Proactor 与异步 I/O 相关。 详细方案如下:
1)Proactor Initiator创建Proactor和Handler对象,并通过AsyOptProcessor(异步操作处理器)将Proactor和Handler都注册到内核;
2)AsyOptProcessor处理注册请求并处理I/O操作;
3)AsyOptProcessor完成I/O操作后通知Proactor;
4)Proactor根据不同的风暴类型,反弹不同的Handler进行业务处理;
5)Handler完成业务处理。
你可以看到Proactor和Reactor之间的区别:
1)当风暴发生时,Reactor通知预先注册的风暴(读写均在应用线程中处理);
2)Proactor在风暴发生时基于异步I/O(由内核完成)完成读写操作,I/O操作完成后回落到应用程序的处理器进行业务处理。
理论上来说,Proactor比Reactor效率更高,并且异步I/O可以充分利用DMA(Direct Memory Access,直接内存访问)的优势。
但Proactor有以下缺点:
1)编程复杂性,由于异步操作过程的初始化和事件完成在时间和空间上是相互分离的,因此异步应用程序的开发比较复杂。 由于反向流控制,应用程序也可能变得越来越无法调试;
2)内存使用,在读或写操作的时间段内必须保留缓冲区,这可能会造成连续的不确定性,并且每个并发操作都需要独立的缓存。 与Reactor模式相比,Socket在读取或写入之前就已经计划好,不需要打开缓存;
3)在操作系统的支持下,真正的异步I/O在Windows下是通过IOCP实现的,但是在Linux系统下,Linux 2.6引入了,异步I/O目前还不完善。
因此Linux下高并发网络编程的实现都是基于Reactor模型的。
JDK中NIO核心组件介绍
缓冲
NIO中使用的缓冲区并不是一个简单的字节字段,而是一个封装的Buffer类。 通过它提供的API,我们可以灵活的操作数据。 NIO提供了多种Buffer类型,如ByteBuffer、CharBuffer、IntBuffer等,区别在于读写Buffer时的单位宽度不同(读写是以对应类型的变量为单位进行的)。
Buffer中有三个非常重要的变量,是理解Buffer工作机制的关键,分别是
容量(总容量)