聊一聊网络程序的发展, 有利于了解Netty为什么设计成这样
这里默认各位都知道UDP和TCP协议是什么

理解了实现再谈网络性能_202202更新.pdf https://zh.wikipedia.org/wiki/Berkeley%E5%A5%97%E6%8E%A5%E5%AD%97 The Java Community Process(SM) Program - JSRs: Java Specification Requests - detail JSR# 51 The C10K problem 如何处理10000 TCP连接 - OSCHINA - 中文开源技术交流社区 为什么QQ用的是UDP协议而不是TCP协议?-悬赏问答/综合互动区 - 即时通讯开发者社区! 高性能网络编程(一):单台服务器并发TCP连接数到底可以有多少-网络编程/专项技术区 - 即时通讯开发者社区! 高性能网络编程(二):上一个10年,著名的C10K并发连接问题-网络编程/专项技术区 - 即时通讯开发者社区! 高性能网络编程(五):一文读懂高性能网络编程中的I/O模型-网络编程/专项技术区 - 即时通讯开发者社区! 高性能网络编程(六):一文读懂高性能网络编程中的线程模型-网络编程/专项技术区 - 即时通讯开发者社区! 新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析-网络编程/专项技术区 - 即时通讯开发者社区! 高性能网络编程(七):到底什么是高并发?一文即懂!-网络编程/专项技术区 - 即时通讯开发者社区!

网络IO模型发展的一些时间点

  • 1983年Unix操作系统(于1983发布)的一套应用程序接口
  • 1996年1月23日,Sun Microsystems 宣布发布 Java 1.0
  • 1999年 Dan Kegel 提出了C10K问题(concurrently handling ten thousand connections)
  • 1999年2月10日,腾讯公司正式推出第一版QQ——OICQ
  • 2002年 Java 1.4, JSR 51, 第一版 NIO, https://jcp.org/en/jsr/detail?id=51

    1.BIO时代

    Berkeley套接字

    1983年, Berkeley套接字(也作BSD套接字应用程序接口)刚开始是4.2BSD Unix操作系统的一套应用程序接口
    Berkeley套接字应用程序接口形成了事实上的网络套接字的标准精髓。 大多数其他的编程语言使用与这套用C语言写成的应用程序接口[1] 类似的接口。 这套应用程序接口也被用于Unix域套接字(Unix domain sockets),后者可以在单机上为进程间通讯(IPC)的接口。

    https://zh.wikipedia.org/wiki/Berkeley%E5%A5%97%E6%8E%A5%E5%AD%97#%E5%A5%97%E6%8E%A5%E5%AD%97API%E5%87%BD%E6%95%B0

一.在讲Netty之前, 聊聊一些历史 - 图1

  • Java在类Unix系统上都是通过调用这些系统接口进行网络通信的
  • 其实Berkeley套接字是支持IO复用的网络模型的, 但是Java第一版没有选择IO复用的网络模型

    JavaBIO

    BIO是java1.0就有的IO模型
    一.在讲Netty之前, 聊聊一些历史 - 图2
    基于Linux的Java就是基于Berkeley套接字设计的IO模型, 虽然Berkeley套接字提供了IO多路复用的使用方式, 但是Java1.0并没有使用, 依然使用的是传统的一个线程处理一个连接
    远古时期服务端的网络程序开发, 都是一个线程对应一个连接, 那个年代不仅仅是Java, 其他的编程语言, 网络模型都是一个线程处理一个连接
    这里指的是Java1.4之前

    BIO的好处

  • 那就是编程的时候, 非常符合人类的直觉, 在一个线程中的操作都是线性的, 写出来的代码稳定, 可维护性高

    BIO的问题

  • 在Linux上进程上下文切换一次, 得需要几个微秒, BIO要处理成百上千个线程, 就会浪费大量的CPU时间片去切换上下文, 一旦连接数过大(比如1000个), 性能会急剧下降

2.C10K问题

英文原文: http://www.kegel.com/c10k.html
中文翻译: https://www.oschina.net/translate/c10k

  • 在 1999 年,Dan Kegel 提出了一个针对网络服务器的难题:

是时候让网络服务器去同时应对 10000 个客户端了,你觉得呢?毕竟网络已经变得很普及了。
那个年代, 网络编程的模型都是BIO的模型
一.在讲Netty之前, 聊聊一些历史 - 图3
但实际上, CPU在这么多线程上, 并不是一直都在处理读写操作, 更多的是在切换上下文, 和等待, 这他妈不是妥妥的浪费么,
这些上下文切换, 就跟很多大公司的制度一样, 纯纯的浪费人力物力
一.在讲Netty之前, 聊聊一些历史 - 图4
站在Java的角度去看BIO, 浪费了哪些资源:

  1. 大量线程处于休眠状态
  2. 线程多了, 自然上下文切换就多了
  3. 内存的浪费: 每个线程的调用栈都分配内存(64KB~1MB)

如此的浪费让机器很难满足C10K的需求

  • 那么要让一个服务器支持更多的并发连接,其实是完全可以达到的, 想办法把无用的浪费干掉就OK了家人们

    3.单台服务器并发TCP连接数到底可以有多少

    上面讨论了BIO的先天问题, 导致并发数上不去, 那么我们排除掉所有限制, 看看一台服务器理论上能达到多少并发

    3.1 服务器理论最大并发数

    (排除文件描述符限制, 排除内存消耗, 排除进程/线程切换消耗, 排除其他所有资源限制, 只层协议层面看)
    发送方IP:port — 接收方IP:port
    假设这里接收方IP和PORT固定不变
    IP 地址是⼀个 32 位的整数,所以发送方 IP 最⼤有 2 的 32 次⽅这么多个。 端⼝是⼀个 16 位的整
    数,所以端⼝的数量就是 2 的 16 次⽅。 2 的 32 次⽅(ip数)× 2的 16 次⽅(port数)⼤约等于两百多万亿。

  • 排列组合, 可以理解为无限

    3.2 内存开销

    https://mp.weixin.qq.com/s/Lkyj42NtvqEj63DoCY5btQ

  1. 文件句柄: 一个连接要占据一个文件句柄 FD

每一条TCP链接都需要file,socket等内核对象; 等于一条空TCP连接最起码得消耗3.3KB左右的内存空间

如果连接要接收数据就需要接收缓冲区了

  1. 接收缓冲区: 接收缓存区大小是可以配置的,通过sysctl命令就可以查看。比如这里是4K

    1. $ sysctl -a | grep rmem
    2. net.ipv4.tcp_rmem = 4096 87380 8388608
    3. net.core.rmem_default = 212992
    4. net.core.rmem_max = 8388608

    “其中在tcp_rmem”中的第一个值是为你们的TCP连接所需分配的最少字节数。该值默认是4K,最大的话8MB之多。也就是说你们有数据发送的时候我需要至少为对应的socket再分配4K内存,甚至可能更大。”

  2. 发送数据缓冲区: TCP分配发送缓存区的大小受参数net.ipv4.tcp_wmem配置影响。这里是4K

    1. $ sysctl -a | grep wmem
    2. net.ipv4.tcp_wmem = 4096 65536 8388608
    3. net.core.wmem_default = 212992
    4. net.core.wmem_max = 8388608

    “在net.ipv4.tcp_wmem”中的第一个值是发送缓存区的最小值,默认也是4K。当然了如果数据很大的话,该缓存区实际分配的也会比默认值大。”

  • 如果是空TCP连接, 那么100w个连接只需要3.5G左右的内存空间
  • 如果是活动的TCP连接, 那么也就是一个TCP连接大约需要12K的内存空间

3.3 CPU开销

  • 这是BIO的工作流程

一.在讲Netty之前, 聊聊一些历史 - 图5

  • 这是I/O 复用模型的工作流程

一.在讲Netty之前, 聊聊一些历史 - 图6
单纯看TCP协议下的网络包处理中,CPU的开销

  1. 系统态CPU开销: 当⽤户进程调⽤ socket、connect、recvfrom 等等函数的时候,都会将⽤户进程陷⼊到内核态; 这些是必须的
  2. 硬中断, 软中断: 内核收到包以后会在硬中断、软中断上下⽂中进⾏内核相关⼯作的处理; 这些是必须的
  3. 进程上下文切换

也就是说, 后面对网络模型的改进, 实际上就是想办法消除进程(线程)上下文切换对CPU的消耗

4.QQ是怎么处理网络连接的问题?

为什么QQ用的是UDP协议而不是TCP协议?-悬赏问答/综合互动区 - 即时通讯开发者社区!

  • QQ是1999年出现的即时通讯软件, QQ的一个服务器需要支持大量的客户端连接

那个年代基于TCP协议的网络模型, 都是一个线程对应一个连接, 别说腾讯了, 全世界基于TCP的网络应用都无法处理大量的TCP连接
于是腾讯选择了UDP协议, 自己模拟了三次握手之类的操作, 封装了一下, 相当于模拟了TCP, 这样就可以绕开Linux提供的标准的函数, 自己决定如何触发监听网络事件

虽然之后Linux出现了epoll, 解决了腾讯以前遇到的内核接口无法很好支持大量TCP连接问题, 但是由于腾讯自己搞的基于UDP的那套很成熟很稳定了,于是就没有改回使用TCP了
现在再做类似的东西就不需要使用udp了

5.IO模型

高性能网络编程(五):一文读懂高性能网络编程中的I/O模型-网络编程/专项技术区 - 即时通讯开发者社区!

一.在讲Netty之前, 聊聊一些历史 - 图7
Linux提供了几个网络编程的接口, 可以通过这些接口实现IO复用的网络模型

select

  • 每次调用select函数都会把所有需要关注的文件描述符都传进去
  • select 入参是个数组, select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件
  • select 是水平触发, 只要有IO数据可读, 就会触发通知

    poll

  • 每次调用poll函数都会把所有需要关注的文件描述符都传进去

  • poll 入参是个链表
  • poll 是水平触发, 只要有IO数据可读, 就会触发通知

    epoll

  • epoll每次调用只会向多路复用器传入新需要关注的文件描述符

  • epoll开辟了一个内存空间, 专门保存需要监听的文件描述符(红黑树)
  • epoll是边缘触发, 只有新的IO活动到来, 才会触发通知; 即使通道中有数据也不会再触发

    IO复用模型

    https://zh.wikipedia.org/wiki/Epoll

在 I/O 复用模型中,会用到 select 或 poll 函数或 epoll 函数,这三个函数也会使进程阻塞,但是和阻塞 I/O 有所不同。
这三个函数可以同时阻塞多个 I/O 操作,而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。

  • BIO一个线程阻塞的时候, 只能监听一个连接的一个事件
  • IO多路复用的模式, 一个线程阻塞的时候, 可以监听一堆连接的各种事件

此图是epoll的工作流程图
一.在讲Netty之前, 聊聊一些历史 - 图8

6.JavaNIO

Channel

  • java.nio.channels.Channel

Java把所有IO连接抽象成了一个Channel对象, 包括文件IO,网络IO等等

  • java.nio.channels.SocketChannel

为了消除不同操作系统之间的差异 , SocketChannel是专门处理网络IO的抽象, 可以认为它表示一个TCP连接;

事件

  • SelectionKey.OP_ACCEPT : 接收连接进行事件,表示服务器监听到了客户连接,那么服务器可以与客户端建立TCP连接
  • SelectionKey.OP_CONNECT : 连接就绪事件,表示客户与服务器的连接已经建立成功
  • SelectionKey.OP_READ : 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
  • SelectionKey.OP_WRITE : 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)

这里的通道, 其实也是缓冲区的抽象

7.Netty

http://www.52im.net/thread-2043-1-1.html

既然Java有了NIO标准的编程库为什么还要用Netty

老生常谈的问题

JavaNIO的问题

  1. NIO 的类库和 API 繁杂,使用麻烦:你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
  2. 需要具备其他的额外技能做铺垫:例如熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的 NIO 程序。
  3. 可靠性能力补齐,开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大。
  4. JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。官方声称在 JDK 1.6 版本的 update 18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 Bug 发生概率降低了一些而已,它并没有被根本解决。 Java NIO中的 epoll bug分析与复现_speakguoer的博客-CSDN博客

    Netty如何解决JavaNIO的问题

  • Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题。
  1. 设计优雅:
  1. 使用方便:
  • 详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就足够了。
  1. 高性能、吞吐量更高:
  1. 安全:
  1. 社区活跃、不断更新: