1. 网络
[toc]
1. 网络
1.1 网络协议
OSI体系结构 | TCP/IP体系结构 | 5层协议体系结构 |
---|---|---|
7 应用层 | 应用层(各种协议如TELNET,FTP,SMTP等) | 5 应用层 |
6 表示层 | ^ | ^ |
5 会话层 | ^ | ^ |
4 运输层 | 运输层(TCP/UDP) | 4 运输层 |
3 网络层 | 网际层IP | 3 网络层 |
2 数据链路层 | 网络接口层 | 2 数据链路层 |
1 物理层 | ^ | 1 物理层 |
1.1.1 应用层
应用进程间的交互来完成特定网络应用, 不同网络应用需要不同的应用层协议, 如域名系统DNS, HTTP协议, SMTP协议等. 应用层交互数据单元称为报文.
1.1.2 运输层
两台主机进程之间通信提供的数据传输服务, 应用进程利用该服务传送应用层报文. 多个应用可使用同一运输层服务. 一台主机可同时运行多个进程,因此运输层有复用和分用的功能
主要两种协议:
- 传输控制协议TCP(Transmission Control Protocol): 面向连接的, 可靠的数据传输服务
- 用户数据协议UDP(User Datagram Protocol): 提供无连接,尽最大努力数据传输服务(不保证数据传输可靠性)
类型 | 特点 | 性能 | 应用场景 | 首部字节 |
---|---|---|---|---|
TCP | 面向连接, 传输可靠, 字节流 | 传输效率慢, 所需资源多 | 要求通信数据可靠(文件传输,邮件传输等) | 20-60 |
UDP | 无连接, 传输不可靠, 数据报文段 | 传输效率快, 所需资源少 | 要求通信速度高(如域名转换) | 8个字节(4个字段组成) |
TCP面向连接的服务. 在传送数据之前必须建立连接, 数据传送结束后要释放连接. 不提供广播或多播服务. TCP可靠性体现在TCP在传递数据前, 会有三次握手来建立连接, 而且在数据传递时, 有确认, 窗口, 重传, 阻塞控制机制, 在数据传完后, 还会断开连接用来节约系统资源. 这样难以避免增加了许多开销(如确认, 流量控制, 计时器, 连接管理等) 这样使得协议数据单元的首部增大很多, 还占用很多处理机资源. | ||||
UDP传送数据之前不需要建立连接, 远程主机在收到UDP报文后, 不需要任何确认. |
1.1.3 网络层
通信的2个计算机会经过多个数据链路, 可能还要经过很多通信子网, 网络层任务就是选择合适的网间路由和交换节点, 确保数据即时传送. 发送数据时, 网络层和运输层产生的报文段或用户数据封装成分组和包进行传送.TCP/IP使用IP协议, 因此也叫IP数据报简称数据报.
1.1.4 数据链路层
主机之间数据传输, 总是一段一段的链路上传送, 就需要使用专门的链路层协议. 两相邻节点传送数据, 数据链路层将网络层交下来的IP数据报组装为帧. 在节点链路上传送帧, 每一帧包括数据和必要的控制信息(如同步信息, 地址信息,差错控制等)
接收数据时, 控制信息使接受端知道一个帧从哪个比特开始到哪个比特结束. 接收到一个帧,就提取出数据部分, 上交给网络层. 还能检查接收帧有无差错, 有差错则丢弃这个出错帧. 如果需要改正链路层传输时的差错, 需要采用可靠性传输协议来纠正出现的差错.
1.1.5 物理层
实现相邻节点比特流的透明传送, 尽可能屏蔽具体传输介质和物理设备差异.
1.2 TPC握手与挥手
1.2.1 TCP三次握手
目的是建立可靠的通信信道, 双方确认自己与对方发送和接收是正常的
- 客户端发送带有SYN标志的数据包- 一次握手服务端: 客户端什么都不做, 服务端确认对方发送正常, 自己接收正常
- 服务端发送带有SYN/ACK标志的数据包- 二次握手客户端: 客户端了确认自己发送,接收正常, 对方发送接收正常; 服务端确认了对方发送正常, 自己接收正常
- 客户端发送带有ACK标志的数据包- 三次握手服务端: 客户端确认了自己发送,接收正常, 对方发送接收正常; 服务端确认了自己发送,接收正常, 对方发送,接收正常
第二次握手传回了ACK, 为何要传回SYN(Synchronize Sequence Numbers):
服务端发回ACK是告诉客户端, 接收到的信息确实就是你发送的信号. 表明客户端到服务端通信是正常的
回传SYN是为了建立并确认服务端到客户端的通信.
1.2.2 TCP四次挥手
- 客户端发送FIN, 关闭客户端到服务器数据传送
- 服务收到FIN, 发回一个ACK, 确认序号为收到序号+1, 和SYN一样, 一个FIN占用一个序号
- 服务端关闭与客户端连接, 发送一个FIN给客户端
- 客户端发回ACK报文确认, 并将确认需要设置为收到序号+1
任何一方数据传送结束后发出连接释放通知, 待对方确认后进入半关闭状态. 当另外一方也没有数据再发送时, 则发出连接释放通知, 双方确认后完全关闭TCP连接
为何客户端四次挥手后还要等2MSL时间才释放TCP连接: 考虑丢包问题, 如果第四次挥手丢包. 服务端没收到确认ACK就会重发第三次挥手, 报文来回就是2MSL. 所以需要等待这么长时间确认服务端确实收到了
1.2.3 TCP协议保证可靠性传输
- 应用数据被分割成TCP认为最适合发送的数据库
- TCP给发送的每个包进行编号, 接收方对数据包进行排序, 把有序数据传送给应用层
- 校验和: TCP将保持它的首部和数据的校验和.这时一个端到端的校验和, 目的是检查数据在传输过程中的任何变化. 如果收到的检验和有差错. TCP将丢弃这个报文段和不确认收到此报文段
- TCP的接收端会丢弃重复的数据
- 流量控制: TCP连接的每一方都有固定的大小和缓冲空间, TCP接收端只允许发送端发送接受度胺缓冲区能接纳的数据. 当接收方来不及处理发送方的数据, 能提示发送方降低发送的速率, 防止包丢失. TCP使用流量控制协议是可变大小的滑动窗口协议.
- 阻塞控制: 当网络阻塞时, 减少数据的发送
- ARQ协议: 基本原理就是每发完一个分组就停止发送, 等待对方确认. 在收到确认后再发下一个分组
- 超时重传: TCP发出一个段后, 启动一个定时器, 等待目的端确认收到这个报文. 如果不能及时收到一个确认, 将重发这个报文段
1.2.4 自动重传请求(ARQ, Automatic Repeat-reQuest)
ARQ时OSI模型中数据链路层和传输层的错误纠正协议之一. 使用确认和超时机制, 在不可靠服务基础上实现可靠信息传输. 如果发送方在发送后一段事件内没有收到确认帧, 通常会重新发送. ARQ包括停止等待ARQ协议和连续ARQ协议.
1.2.4.1 停止等待ARQ协议
为了实现可靠传输, 它的基本原理就是每发完一个分组就停止发送, 等待对方确认(回复ACK), 如果一段时间(超时时间后), 没有收到ACK确认, 说明没有发送成功, 需要重新发送, 直到收到确认后再发下一个分组. 如果接收方收到重复分组, 丢弃该分组, 但同时还要发送确认.
优点: 简单; 缺点: 信道利用率低, 等待时间长.
出现情况:
a) 无差错情况: 发送方发送分组,接收方在规定时间内收到, 并且回复确认,发送方再次发送
b) 出现差错情况(超时重传): 超时未收到确认, 就重发前一分组. 因此每个分组需要设置一个超时计时器, 重传时间应比数据在分组传输的平均往返时间更长一些. 如果收到重复分组,就丢弃该分组. 但同时还要发送确认.
c) 确认丢失和确认迟到:
确认丢失: 确认消息在传输过程中丢失, 消息重传. 接收方在接收到消息后丢弃重复分组, 向发送方发送确认消息.
确认迟到: 确认消息在传输过程中迟到. 消息重传, 发送方收到重复确认后, 直接丢弃, 接收方收到重复消息, 也直接丢弃.并向发送方发送确认消息.
1.2.4.2 连续ARQ协议
提高信道利用率. 发送方维持一个发送窗口, 凡位于发送窗口内的分组可以持续发送出去, 无需等待对方确认. 接收方一般采用累加确认, 对按序到达的最后一个分组发送确认, 表明这个到这个分组为止的所有分组都已经正确收到.
优点: 信道利用率高, 容易实现, 即时确认丢失, 也不必重传.
缺点: 不能向发送方反应出接收方已经正确收到的所有分组. 比如中间某条分组缺失, 接收方只ACK异常分组之前的分组. 发送方无法直到异常消息之后的分组. 只能把后面的消息全部重传一次, 也叫Go-Back-N(回退N), 表示需要退回重传已发送过的N个消息
1.2.4 滑动窗口和流量控制
滑动窗口实现流量控制
流量控制时为了控制发送方发送速率, 保证接收方来得及接收
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小, 从而影响方的发送速率. 将窗口字段设置为0, 则发送方不能发送数据.
流量控制往往是点对点通信量的控制. 是端到端的问题. 所要作的就是抑制发送端发送数据的速率, 以便使接收端来得及接收.
1.2.4 阻塞控制
某个时间, 若对网络中某一资源的需求超过了该资源所能提供的可用部分, 网络性能变坏, 就叫阻塞. 阻塞控制就是为了防止过多的数据注入到网络中, 使网络中的路由器或链路不致过载. 阻塞控制所要做的前提就是网络能够承受现有的网络负荷. 阻塞控制是一个全局性的过程, 涉及到所有主机,所有路由器,以及降低网络传输性能有关的所有因素.
为了阻塞控制, TCP要维持一个阻塞窗口(cwnd)的状态变量. 阻塞窗口大小取决于网络的阻塞程度, 并动态变化. 发送方让自己的发送窗口取阻塞窗口和接收方接收窗口中较小的一个.
TCP阻塞控制采用4种算法, 在网络层可以使路由器采用适当的分组丢弃策略(如主动队列AQM), 以减少网络阻塞的发生
- 满开始: 慢开始算法思路是当主机开始发送数据时, 如果立即把大量数据注入网络, 可能引起网络阻塞,因为还不知道网络情况. 较好的方法是先探测下, 有小到大逐渐增大发送窗口(从小到大逐渐增大阻塞窗口数值). cwnd初始为1, 每经过一个传播轮次, cwnd加倍
- 拥塞避免: 让拥塞窗口cwnd缓慢增大, 每经过一个往返时间RTT就把发送方的cwnd加1
- 快重传和快恢复(fast retransimit and recovery, FRR): 拥塞控制算法, 能快速恢复丢失的数据.没有FRR, 数据包丢失, TCP使用定时器来要求传输暂停,暂停时间内, 没有新的或复制的数据包被发送. 有了FRR接收机接收到一个不按顺序的数据段, 会立即给发送方一个重复确认. 如果发送方接收到3个重复确认, 它会假定确认件指出的数据段丢失, 并立即重传这些丢失的数据段. 有了FRR就不会因为重传时要求的暂停被耽误了.
1.2.5 TCP粘包/拆包
粘包.拆包原因:
- 应用程序写入的数据大于套接字缓冲区大小, 将发生拆包
- 应用程序写入数据小于套接字缓冲器大小, 网卡将应用多次写入的数据发送到网络上, 将发生粘包
- 进行MSS(最大报文长度)大小的TCP分段, 当TCP报文长度-TCP头长度>MSS时发生拆包
- 接收方法不及时读取套接字缓冲区数据, 将发生粘包
解决办法:
- 发送端每个数据包添加包首部, 首部应该包含数据包长度. 这样接受端在接收数据时, 通过读取包首部的长度字段, 直到每个数据包的实际长度
- 发送端将每个数据包封装为固定长度(不够则通过补0填充). 接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来
- 在数据包之间设置边界, 如添加特殊符号, 这样接收端通过这个边界就可以将不同数据包拆分开
1.2.6 建立连接后故障
TCP由保活计时器. 服务端每收到一次客户端请求都会复位计时器(超时时间默认2消息). 如果2小时还未收到客户端任何数据, 服务端就会发送一个探测报文. 以后每隔75s发送一次. 若联系10次仁没有反应, 就认为客户端故障. 连接关闭.
1.3 HTTP协议
输入URL到看到这个页面整个过程发生了什么
- DNS解析
定位地址对应的服务器IP地址. 递归查询过程
a) 先检查本地hosts文件是否有这个网址, 如果有先调用这个ip地址映射, 完成域名解析
b) 没有映射, 查找本地DNS解析器缓存, 是否有这个网址映射, 如果有直接返回, 完成域名解析
c) 如果都没有, 查找TCP/IP参数种设置的首选DNS服务器(本地DNS服务器),去此服务器查询,如果要查询域名包含在本地配置资源种, 则返回解析结果给客户机, 完成域名解析
d) 如果还没查到,根据本地DNS服务器设置(是否设置转发器)进行查询. 如果未用转发模式, 本地DNS就把请求发至13台根DNS, 根DNS收到请求会判断这个域名谁来授权管理, 并发回一个负责该顶级域名服务器的一个IP. 本地DNS收到IP后, 会联系该域名服务器. 域名服务器收到请求, 如果自己无法解析, 会找到一个管理域的下一级DNS服务器地址给本地DNS服务器, 本地DNS服务器收到这个地址后, 就会找到域名对应域服务器.重复上面动作, 直到找到域名主机(如www.sina.com.cn, 先到根服务器, 根服务器转发给cn域名服务器, cn域名服务器转发给.com.cn域名服务器, 再转发给.sina.com.cn域名服务器, 再转发给www.sina.com.cn域名服务器, 最后找到该域名IP)
e) 如果是转发模式, DNS服务器会把请求转发至上一级DNS服务器.上一级服务器进行解析, 上一级不能机械, 或找根DNS, 或转请求到上上级. 以此循环.
f) 不管是转发还是请求根服务器, 最后都把结果返回给本地DNS服务器, 此DNS服务器再返回给客户机
g) 客户端到本地DNS服务器属于递归查询, 而DNS服务器之间交换查询是迭代查询
优化:
- DNS缓存
- 浏览器DNS缓存 chrome://dns/
- 系统缓存 /etc/hosts
- DNS负载均衡
DNS可以根据机器负载量, 机器离用户地理位置距离等,给用户一个合适的机器IP. 叫做DNS负载均衡(DNS重定向). CDN(Content Delivery Network)就是利用DNS负载均衡. 返回一个根用户最接近的点的IP地址给用户
- TCP连接
打开TCP端口, 请求目标服务器 - 发送HTTP请求
请求过程就是构建HTTP请求报文, 通过TCP协议中发送到服务器指定端口, 包括请求行, 请求报头, 请求正文
把HTTP报文包裹再TCP报文种发送. 存在信息泄漏风险
在进入TCP报文前对HTTP进行一次加密(HTTPS协议就是HTTP+SSL/TLS)
HTTPS传输数据之前客户端和服务器进行一次TLS/SSL握手.握手过程种确定双方加密传输数据的密码信息. TLS/SSL使用非对称加密, 对称加密以及hash等.
请求行: Method Request-URL HTTP-Version CRLF(请求方式, 请求地址, http版本号, 换行符)
请求报头: 向服务器传递请求附加信息和客户端自身信息(如请求头等)
请求正文: POST, PUT请求的数据就存在请求正文
4. 服务器处理请求返回HTTP报文
服务器从固定端口接收到TCP报文, 多TCP连接进行处理, 解析HTTP请求. 按照报文格式进一步封装未HTTP Request对象, 供上层使用
Http响应报文包含3个部分: 状态码, 响应报头, 响应报文
状态码:响应的类别
响应报头: 响应头信息
响应报文: 文本, 文件, json内容信息
- 浏览器解析渲染页面
接收到html, css, js后.
- 首先解析html未DOM树
- 然后解析CSS文件构建渲染树. 等渲染树构建完成后, 浏览器开始布局渲染树将其绘制到屏幕
- JS由JS解析引擎完成, 单线程允许
- 解析过程中请求外部资源.异步重复过程. 当遇到JS文件, 会挂起渲染过程.等待JS文件加载完毕还要等待解析完毕, 才会继续html渲染过程.js代码执行前必须保证css文件已下载并加载完毕
- 连接结束
1.3.1. HTTP 1.0与HTTP 1.1区别
- 长连接: 1.0默认未短连接, 每次请求重新建立一次连接. 1.1 默认使用长连接. 1.1持续连接由非流水线方式: 客户收到前一个响应才能发送下一个请求. 流水线方式: 收到响应报文之前就能接着发送新的请求报文.
- 错误状态响应码: 1.1新增24个错误状态响应码
- 缓存处理: 1.1引入了entity tag, if-unmodified-since等更多可供选择缓存头来来控制缓存策略
- 带宽优化和网络连接使用: 1.1支持断点续传
1.3.2 URI与URL
- URI(Uniform Resource Identifier)统一资源标志符, 唯一标识一个资源
- URL(Uniform Resource Locator)统一资源定位符, 提供该资源路径, 是一种具体URI, 既可以标识一个资源,又可以指明如何locate资源
1.3.3 http与https
- 端口: http为"http://", 默认80端口, https为"https://", 默认443端口
- 安全性和资源消耗: http运行在TCP上, 传输明文. https运行在SSL/TLS上的http协议,内容加密. http安全性没有https高, https比http耗费更多的服务器资源.
1.3.3 http2
- http1中浏览器限制同域名请求数量(chrome下一般为6个). 当请求很多资源时, 由于队列头阻塞当浏览器达到最大请求后, 剩余资源阻塞需等待当前6个请求完成后才能发起请求. http1.1的"管线化pipelining"只能串行化, 一个响应必须完全返回, 下一个请求才会开始传输
- http2引入多路复用技术. 可只通过一个TCP连接就可传输所有请求数据. 多路复用可绕开浏览器限制同一域名下请求数量问题. 进而提高网页性能. 多路复用利用"分帧"数据流, 将http协议分解为"互不依赖"的帧(为每个帧标序发送, 接收时按序重组)进而可以乱序发送, 避免队首阻塞问题,
- http2不再以文件方式传输,采用"二进制分帧层", 对头部进行压缩, 支持"流控"
1.3.4 http3
http3 基于UDP, 减少RTT(往返时延)(TCP需要三次握手, TLS握手)
2. Netty
2.1 优点
NIO缺点: 类库和API复杂, 业务代码过于耦合, 需要了解很多多线程知识, 熟悉网络编程, 面对断连重连, 保丢失, 粘包等, 处理复杂NIO存在BUG
Netty优点:
- 框架设计优雅, 统一API, 底层模型随意切换适应不同的网络协议要求(阻塞非阻塞)
- 提供很多标准的协议, 安全, 编码解码的支持, 解决TCP粘包/拆包问题
- 真正的无连接数据包套接字支持
- 解决很多NIO不易用的问题, 有更高的吞吐量, 更低的延迟, 更低的资源消耗和更少的内存复制
- 很多开源框架使用
- 底层核心有零拷贝buffer(Zero-Copy-Capable Buffer), 统一API, 标准可扩展的事件模型
- 传输支持: 管道通信, HTTP隧道, TCP和UDP
- 协议支持: 基于原始文本,二进制协议, 解压缩, 大文件传输,流媒体传输,protobuf编解码, 安全认证, http和websocket等
- 安全性有完整的SSL/TLS以及StartTLS支持
执行流程:
- 创建ServerBootStrap
- 设置绑定Reactor线程池: EventLoopGroup, EventLoop就是注册到本线程的Selector上的Channel
- 设置并绑定服务端channel
- 创建处理网络事件的ChannelPipeline和handler, 网络事件以流的形式在其中流转, handler完成多数的功能定制: 编解码, SSL安全认证
- 绑定并启动监听端口
- 当轮询到准备就绪的channel后, 有Reactor线程: NioEventLoop执行pipline中方法, 最终调度并执行ChannelHandler
2.1 select, poll, epoll机制和区别
- 单进程打开文件描述符(fd文件句柄)不一致
select: 有最大连接数限制数位1024, 单个进程所能打开的最大FD_ZETSIZE定义
poll: 本质与select没区别, 但没有最大连接数限制, 基于链表存储
epoll: 连接有限制,但是很大. 1G内存可以打开10万左右连接
- 监听socket方式不一致
select: 轮询方式,一个个socket检查, 发现socket活跃才进行处理. 当线性socket增多时, 轮询速度将变得很慢, 造成性能下降问题
poll: 稍微优化, 只是修改文件描述符, 但监听socket还是轮询
epoll: 实现是根据每个fd上callback函数来实现, 只有活跃socket才会主动调用callback, 通知epoll来处理这个socket.
- 内存空间拷贝方式(消息传递方式)
select: 内核将消息传递给用户态, 需要将数据从内核态拷贝到用户态, 这个过程非常耗时
poll: 与select一致
epoll: 内核和用户空间共享一块内存.因此内核态数据和用户态数据共享
2.1 核心组件
2.1.1 ByteBuf
网络通信通过字节流传输. ByteBuf就是Netty的字节容器.内部为一个字节数组. 可看作NIO的ByteBuffer的封装抽象(ByteBuffer过于复杂繁琐,所有netty自己实现了一个)
2.1.2 Bootstrap 和 ServerBootstrap(启动引导类)
Bootstrap: 客户端启动引导类/辅助类, 通常使用connect()连接到远程主机端口, 作为TCP协议通信中的客户端. 也可以通过bind()绑定本地的一个端口, 作为UDP协议通信中的一端. 只需要配置一个线程组EventLoopGroup
ServerBootstrap: 服务端启动引导/辅助类, 通过bind()绑定本地的一个端口,等待客户端连接. 需要配置两个线程组EventLoopGroup,一个用于接收连接,一个用于具体的 IO 处理.
2.1.3 Channel (网络操作抽象类)
对网络操作的抽象类,通过Channel进行IO操作
2.1.4 EventLoop(事件循环)
主要作用实际就是责监听网络事件并调用事件处理器进行相关 I/O 操作(读写)的处理
与Channel与关系: EventLoop负责处理注册到其上的Channel的IO操作, 两者配合进行IO操作
与EventLookGroup关系: EventLookGroup包含多个EventLoop(每个EventLoop通常内部包含一个线程). 并且EventLook上处理IO事件都有专有的Thread上处理, 即EventLoop与Thread属于1:1关系.保证线程安全
默认NioEventLoopGroup线程池大小:
- 如果指定了线程池大小, 则根据指定值创建
- 未指定, 则读取系统变量io.netty.eventLoopThreads
- 如果系统变量未设置, 则读取CPU可用核心数*2
- 如果上面结果小于1, 则返回1
2.1.4 ChannelHandler(消息处理器)和ChannelPipeline(ChannelHandler对象链表)
ChannelHandler是消息的具体处理器,要负责处理客户端/服务端接收和发送的数据.
当Channel创建时, 被自动分派一个专属的ChannelPipeline. ChannelPipeline为ChannelHandler的链.
ChannelPipeline 通过addLast()添加一个或多个ChannelHandler(一个数据或者事件可被多个Handler处理). 一个ChannelHandler处理完成后交给下一个ChannelHandler.
当ChannelHandler被添加到ChannelPipeline, 将得到一个ChannelHandlerContext, 代表ChannelHandler与ChannelPipeline之间的绑定.ChannelPipeline通过ChannelHandlerContext间接管理ChannelHandler
2.1.4 ChannelFuture(操作执行结果)
所有IO操作都是异步的, 可以通过channel()获取关联的Channel
2.2 线程模型
2.2.1 单Reactor单线程模型
一个线程(Reactor线程)创建select选择器, 将Accept事件注册到select选择器, 轮询是否有"准备就绪"事件. 当请求为连接事件, 交给acceptor处理(绑定handler); 当为读写事件时, 交给读handler/写handler处理(业务处理). 这个事情都在一个线程中处理
缺点:
- 一个线程处理请求, 对多核资源机器来说浪费
- 当读写任务线程负载过高, 处理速度会下降, 事件堆积, 严重会超时, 导致客户端重新发送请求, 性能越来越差
- 单线程可靠性问题
2.2.2 单Reactor多线程模型
一个线程(Reactor线程)创建select选择器, 将Accept事件注册到select选择器, 轮询是否有"准备就绪"事件. 当请求为连接事件, 交给acceptor处理(绑定handler); 当有读写事件时, 交给读/写线程池. 由对应Handler线程池处理
缺点: Reactor线程承担所有事件, 如监听响应. 高并发下单线程存在性能问题
2.2.3 多Reactor多线程模型
将原Reactor线程拆分为mainReactor和subReactor. mainReactor只处理连接事件(一个线程来处理就好). subReactor维护自己的selector, 基于mainReactor注册的socketChannel多路分离IO读写事件, 只处理读写事件(一般与CPU数量相等), 对于业务处理功能, 交给handler线程池处理
2.3 零拷贝
2.3.1 传统拷贝
传统发送数据, 如File.read(bytes)
,Socket.send(bytes)
需要四次数据拷贝和四次上下文切换:
- 数据从磁盘读取到内存read buffer
- 数据从内核缓冲区拷贝到用户缓冲区
- 数据从内核缓冲区到内核socket buffer
- 数据从内核socket buffer拷贝到网卡接口(硬件)缓冲区
2.3.2 零拷贝
上面的2, 3没有必要, 直接通过FileChannel.transferTo
方法, 避免2次多余拷贝(需底层操作系统支持)
- 调用transferTo. 数据从文件由DMA引擎拷贝到内核read buffer
- 接着由DMA拷贝内核read buffer将数据拷贝到网卡接口(硬件)缓冲区. 2次操作不需要CPU操作, 就达到了零拷贝
2.3.3 Netty零拷贝
- ByteBuf: Netty接收发送都是通过ByteBuf, 使用的堆外内存(DirectMemory)直接进行socket读写(传统堆内存socket读写, JVM需要将堆内存buffer拷贝一份到直接内存然后再写入sockect, 多了一次缓冲区拷贝). 堆外内存则直接可以通过DMA发送到网卡接口
- Composite Buffers: 传统ByteBuffer两数据合并, 需要创建新数组,并将数据拷贝到新数组. ByteBuf则可以避免, 使用CompositeByteBuf没有真的将多个ByteBuf组合, 而是保留引用, 避免数据拷贝, 实现零拷贝
- transferTo: 使用了FileChannel.transferTo方法, 依赖操作系统实现零拷贝.
2.4 内存池
内存池分为堆内存和非堆内存(directMemory, 堆外内存). 内存分派核心算法类似.
ByteBuf承载数据, ByteBuf内存分配由PooledByteBufAllocator来执行.最终内存分配会委托给PoolArena, 堆内存实现为HeapArena.
高并发下, 多线程竞争加锁会影响内存分配效率. 为了缓解线程竞争, 运行使用者创建多个分配器(PoolArena)来分离线程竞争, 提高内存分配效率. 可通过PooledByteBufAllocator构造器中的nHeapArena参数来设置PoolArena数量或者直接取框架中默认值(通过CPU核心数, JVM最大可用内存, 默认内存块PoolChunk大小等来决定)
- 如果配置系统变量io.netty.allocator.numHeapArenas. 则使用系统变量值
- 如果未配置. 计算CPU核心数*2和JVM最大可用内存/默认内存块PoolChunk大小/2/3, 取其中一个较小的值最为PoolArena的个数
当确定了PoolArena个数后, 会创建一个PooArena数组, 数组中所有poolArena都会用来执行内存分配. 当线程申请内存分配时, 线程会从数组中挑选一个被占用次数最小的执行内存分派
还使用FastThreaLocalThread来优化ThreadLocal性能:
- 默认ThreadLocal使用ThreadLocalMap存储线程局部变量, 需要计算hashCode定位线程局部变量所在Entry索引.
- FastThreaLocalThread使用FastThreadLocal代替. FastThreaLocalThread使用一个数组维护线程变量, 每个FastThreadLocal维护一个index(代表局部变量再数组中的位置, 从而无需计算hashcode)
2.4 对象池
通过重用对象, 来避免频繁创建对象, 销毁对象带来的损耗.
// todo: