Linux之网络
- 两个模型
- 应用层协议
- HTTPS
- 传输层协议
- UDP
- TCP
- 可靠性与效率的兼顾
- 面向字节流
- TCP异常情况
- 底层实现
- 网络层协议
- IP
- 网段划分
- 子网划分
- NAT
- 数据链路层协议
- 以太网
- ARP
- 代理服务器
- 内网穿透
- 五种IO
- 多路复用
- Reactor模式
本文旨在讲解tcp-ip协议原理,并不涉及代码部分,具体应用代码请参见我的git仓库,本文默认你已经熟悉网络通信代码编写流程。
两个模型
iso提出的osi7层模型是完美的通信标准,后来所说的tcp/ip模型把上三层合为一个,但暗中遵循前者的三层划分与解耦。
其中,osi的上三层,会话层负责管理会话;表示层:负责序列化和反序列化数据;应用层提供真正的服务,即处理请求。
应用层协议
HTTPS
传输层协议
UDP
udp,用户数据报协议,无连接,不可靠,面向数据报。
无连接和不可靠等讲到tcp就可以感受到了。
由图可知,udp报文采用定长8字节报头区分报头与有效载荷,其中16位UDP长度标识该报文总长度,使得udp报文之间有边界,同时应用层交给udp的数据,udp不做拆分与合并之类的控制而直接打报发送,以报文为基本单位便是udp面向数据报的由来。
当用户层定义的结构体进入传输层前应当需要序列化成为字节流,但双方os都是基于c语言,所以udp报头不需要,但数据需要。
另外,可见udp报文最大长度64KB,有限,需要应用层考虑拆分长报文。
udp不需要发送缓冲区,通常直接交给os付与下层;有接收缓冲区,满了丢弃。
上图是截自内核源码的sk_buff结构体的定义片段,它用于指向一块由内核分配的、用于存放实际网络数据的内存区域,即数据报所在空间。其中,head和end指向分配的整块内存的起始和结束地址,data和tail指向当前协议层的数据报首尾。而next与prev用于管理sk_buff本身。
下面阐述一个应用层数据向下传输流程:当应用层调用send时,数据被复制到内核空间,并创建一个sk_buff结构来管理之,层层向下交付,根据该层所选协议在数据报添加相应报头,即data前移出一个报头位置,并写入报头信息,层层如此。
反之,接收到数据报时,创建sk_buff描述并管理之,到每一层都是去掉该层报头,即取走报头信息,data后移,再向上交付。
TCP
tcp,传输控制协议,面向连接,可靠传输,面向字节流。
tcp报文通过4位首部长度(单位4字节)分离报头与有效载荷,报头范围20~60字节。序号,标识当前发送的数据段,通常填写数据段起始字节编号(对tcp发送缓冲区数据按字节进行编号)。初始序列号由系统通过算法生成,避免旧报文干扰和序列号重用攻击。
确认序号,表示该确认序号之前所有报文数据均已收到,并期望接下来的数据从确认号指定的位置开始。
六个标志位,URG配合16位紧急指针表示中数据中包含了一个需要优先处理、不排队直接发送的紧急数据块,后者指向其位置;ACK置1时表示确认序号字段有效;PSH提示接收方立即将缓冲区中的数据传递给应用层,而不是等待更多数据到达后再一次性交付;RST用于异常关闭连接,比如遇到错误的tcp段或端口未被监听;SYN同步序列编号,配合序列号建立连接,见于三次握手;FIN终止连接,见于四次挥手。
窗口大小,报文发送者的tcp接收缓冲区当前剩余大小。
检验和,检测报文段是否在传输过程中被损坏,确保可靠性,此处略过。
选项,可为空,其中有窗口扩大因子,表示实际窗口为窗口值左移因子位。
可靠性与效率的兼顾
- 序列号与确认应答:首先,每个字节的数据都被赋予一个序列号,报头32位序号会填充为发送数据段中第一个字节的序列号,确保对方按序重组数据。同时,接收方在接收数据(假设其序号100,数据长度100)后会返回一个确认应答报文(其ACK标志位置1,以下简称该报文为ACK),该应答报头中确认序号为接收报文的序号+数据长度+1,即201,表示该确认序号即201之前的数据均已接收,若发送方未在一定时间内收到ACK,则会重传数据。
- 超时重传:发送方发送一个数据包后,会启动一个定时器。如果在这个定时器到期之前没有收到相应的ACK,则认为该数据包丢失或损坏,并进行重传。超时时间通常根据网络条件动态调整,是最短的、应答一定能到达的时间。Linux采取指数退避的重传机制,比如第一次超时500ms就重传,后面超时2500、4500到8*500ms再重传,累计多次超时再异常关闭连接。
- 连接管理之三次握手:client通过connect发起三次握手,请求建立连接。双方os自动完成如下图的三次握手过程,在内核创建结构体描述并管理该连接,server调用accept将从已完成连接队列中提取出已经成功完成了三次握手的新连接。三次握手的前两次不能携带数据,因为握手尚未完成,第三次c->s可以捎带应答,即发送数据时捎带着确认应答。同时,三次握手会同步双方初始序列号,交换双方窗口大小。
问:为什么是三次握手?
答:三次握手以最短的流程验证双方本身通信意愿和网络支持全双工能力,即能收能发。
不能是两次握手,因为如果少了第三次c发给s的ack,无法验证c的全双工能力。另外,如果上一次连接发送给s的旧SYN报文阻塞在网络路由中,而连接断开后该报文送达,那么在两次握手的情况下,s只需要发送syn+ack即可建立连接,而c毫不知情,造成服务器连接队列资源浪费。
其实,三次握手本质上是四次握手,只不过s的syn和ack合并发送,称为捎带应答,这是因为面对客户端连接请求,服务器默认要无脑接受。
- 连接管理之四次挥手:一方比如client想要关闭连接,通过close关闭自己发送数据的方向而接收方向保留,os向对方发送带有FIN的报文,server应答。s在发送完剩余数据后,再发送FIN报文,c给予应答则四次挥手完成。但是此时主动发起四次挥手的一方此处为c,会进入TIME_WAIT状态,即使进程退出,在tcp层也会等待 2MSL(Maximum Segment Lifetime,最大报文生存时间)才能回到CLOSED状态,而此期间该端口仍处于被占用状态,无法被复用即绑定。另外,上面流程图中可见如果服务端不调用close,四次挥手便无法完成,而会进入CLOSE_WAIT状态,浪费fd与连接队列资源。
问:为什么主动断开连接一方要处于TIME_WAIT状态?
答:1.防止旧数据包干扰新连接,通过等待2MSL确保历史上延迟或重复的数据报消失。2.确保最后一个ACK能被s收到。如果最后一个ACK丢失,s会重传FIN,c在TIME_WAIT状态下仍能收到FIN并重发ACK,避免s因收不到ACK而无法关闭连接。
问:如何在TIME_WAIT期间能够复用该端口?
答:使用系统调用int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);其中,level填SOL_SOCKET表示套接字层,optname填SO_REUSEADDR启用端口复用,optval传int带出选项值,optlen即int长度。
- 流量控制与滑动窗口:滑动窗口左边界是上一次接收方发来的确认报文中确认序号处,滑动窗口大小size暂时认为上一次接收方方ACK中窗口大小,右边界则由start+size确定。如此,发送方即可在不造成接收方缓冲区溢出(满了会丢弃新到的报文)的前提下高效传输数据,实现流量控制。
再来看滑动窗口,start是始终向右滑动的,对方发来一个ACK报文,即可根据确认序号更新start,如果1001 ~ 1500丢包,那么发出的两个报文,只会受到两个确认序号为1001的ACK,start不动。若1501 ~ 2000丢包,收到两个确认序号1501的,start指向1501即可。
另外,滑动窗口其实是环形的,循环利用,此处以线性方式看待较为方便。
快重传:当发送方收到三个重复ACK即确认序号相同,就会重传确认序号对应报文,而不用等定时器超时。想象一下,滑动窗口里第一个报文丢失,后面收到3个相同ACK你就会重传第一个报,但是反观超时重传,要等RTO(Retransmission Timeout)几百毫秒级别的时间才能重传,而丢包到快重传可能只用几十毫秒内。在这几百ms里,发送方滑动窗口、应用层数据卡住,连接的吞吐量下降。
当接收方接收缓冲区满了时:接收方ACK窗口大小为0,发送方停止发送新数据,而是进行窗口探测,发送一个只包含1字节数据的小包(通常是对之前发送的数据进行重传),多次探测等待应答;接收方如果有可用空间则会发送窗口更新通知。如果发送方探测达最大次数则终止连接。 - 拥塞控制:通过拥塞窗口+慢启动与拥塞避免应对网络拥塞问题。cwnd(congestion window,拥塞窗口)是当前网络状况能允许批量发送的数据量,从此,滑动窗口大小=min(窗口大小,cwnd)。连接建立或超时重传后,发送方拥塞窗口会从1开始随传输轮次指数增长,指数增长具有前期增速慢,后期增速爆炸的特点,所以称为慢启动,快速而安全地试探出网络通畅情况。指数增长到ssthresh(慢启动阈值)后改为线性增长,称为拥塞避免。如果再次触发超时重传,ssthresh变为此时cwnd/2,称为乘法减小,再重复慢启动+拥塞避免。
通过每个主机tcp层采取拥塞控制策略,网络堵塞时状况会大大改善。 - 延迟应答:接收方通过在不触发超时重传的前提下尽可能晚的发出应答,争取时间使应用层读走tcp缓冲区数据,使得ACK中窗口最大化,增大网络吞吐量。
总结,tcp提高可靠性的机制有检验和,序列号与确认应答,超时重传,三次握手,四次挥手,流量控制,拥塞控制;提高效率的策略有滑动窗口,快重传,捎带应答,延迟应答。
注意,可靠性是中性词,tcp解决丢包、乱序等问题,但也相比udp损失了部分效率。二者应用场景不同,但无优劣之分。
面向字节流
个人理解:tcp层看待应用层交付的数据,并不关心整体性,它可以把应用层给我的一个包拆成100个tcp报文发出,由对方tcp再交给上层。它也可以将应用层给的100个包合并成一个tcp报文发出交予对方应用层。由此产生的诸如粘包问题,是指应用层的包黏在一起,需要应用层自己制定协议,划分边界。注意,tcp报文在tcp层就可以分离出报文数据,交付上层,tcp层不存在粘包问题。
TCP异常情况
进程终止,os会释放fd,发送FIN,连接正常关闭。
机器重启,与上同。
机器断电/断网,此时在线的一方如果发送数据会触发超时重传,就算不发送,也会有TCP自带的保活机制,定期发送“心跳”数据包(即所谓的Keepalive探针,本质是空的ACK报文)或应用层自己实现的保活机制比如ping,来检查对端是否仍然在线。一定时间内无应答则关闭连接。
底层实现
下图为Linux网络栈核心数据结构关系。方框中头部为结构体名称,+表示该结构体中成员
其中,struct sock中含有字段struct {struct sk_buff *head; struct sk_buff *tail;} sk_backlog;
与listen接口第二个参数有关,是内核维护的已完成三次握手但尚未被accept取走的连接请求队列相当于候客区的最大长度。
网络层协议
IP
ip,此处指ipv4,工作于网络层,起路由功能。
版本,IP协议的版本号,在IPv4中该值为4。
首部长度和tcp一致,4字节为单位,最小值填5,加上选项会更长。
服务类型,指定数据包传输的服务优先级等。
总长度,ip数据报报头+数据总长度,单位字节。
标识+标志+片偏移,共同完成分片工作。标识用于重组时标识属于同一原始数据包的所有分片,标志第0位保留、第1位“不分段”标志、第2位“更多分片”标志,片偏移为该分片相对于原始数据包起始位置的偏移量,单位为8字节。
问1:接收方如何判断报文是否被分片?
答1:如果标志第3位置1,则分片;如果“更多分片”为0但片偏移非0,则分片。
问2:接收方怎么保证把片收全?
答2:将所有标识相同的片聚合起来,“更多分片”标志置1且片偏移为0,则第一片收到,根据该片的数据长度片偏移=下一片的片偏移即可找至倒数第二片,最后一片“更多分片”为0。
问3:为什么要分片?
答3:如果ip报文长度超过MTU(Maximum Transmission Unit,最大传输单元,在以太网中为1500字节),会被分片,否则不能在数据链路层传输。另外,分片会增加丢失风险,一旦某个分片丢失或损坏,整个原始数据包就需要重传。所以不推荐分片,为此也有了MSS(Maximum Segment Size,最大段大小),是tcp三次握手时协商取的双方MTU最小值-20-20,以太网中为1460,减的是tcp和ip报头,即一个tcp报文携带数据不应超过1460以避免分片。这个MSS也是滑动窗口的基本单位。
生存时间(TTL, Time to Live),限制数据报可以在网络中经过的路由器数量,每经过一个路由器该值减1,当TTL变为0时数据报将被丢弃。
协议,IP数据报文内封装的上层协议类型,如TCP为6,UDP为17。
头部校验和,保证IP报头的完整性。
网段划分
1981年,网络发展早期,32位的ip地址被粗糙地划分为如下几类地址。A类网络号少,适用于大型网络,可容约1600万主机,B类每个网络可容约65000主机,而C类每个只容254台主机(256-1个网络地址-1个广播地址)。如此固定的划分方法,势必造成网络资源分配不均,比如只有几百台联网设备的小公司被分配了A类地址,它无法完全利用1600万个地址,造成浪费。
子网划分
1985年,引入子网掩码进行子网划分,可以在原有的网络地址基础上进一步灵活精细地划分为多个子网,提供更细粒度的划分方法。
先说明子网掩码,比如,一个C类地址192.168.1.1(二进制形式1100 0000.1010 1000.0000 0001.0000 0001),对应子网掩码为255.255.255.0(1111 1111.1111 1111.1111 1111.0000 0000),前24个1表示ip地址前24位为网络号,后8个0表示地址后8位为主机号。将子网掩码与该地址按位与可得到该IP地址所在的网络地址即192.168.1.0,该ip地址可以表示为192.168.1.1/24,即ip地址/掩码长度,表示前24位为网络号,后8为主机号。注意,一个ip地址的主机号部分全0,如192.168.1.0/24,该ip代表该局域网;主机号全1,该ip为广播地址。
下面介绍一个应用场景,展示子网划分的意义:
假设一家公司有三个部门,分别需要50、100和200个IP地址。申请一个B类地址172.16.0.0/16(支持约65000台主机),通过子网划分为172.16.0.0/26(支持62主机)、172.16.0.128/25(支持126主机)和172.16.1.0/24(支持254主机)分配给三个部门使用,如此只使用了该3个子网的ip,模块化分配子网,便于网络灵活解耦管理。剩下6万多ip被闲置,但仍属于此公司可供未来使用,除非归还否则其他组织无法使用,所以它并没有解决IP不足,但为接下来的NAT技术铺路。
NAT
尽管子网划分优化了ip分配,但32位的ip地址上限是43亿个,终究有用完的那天,一个有前途的解决方案是IPv6,用64位数字标识ip地址。但是,NAT的提出延长了IPv4的寿命,延缓了IPv6的全面部署。
NAT(Network Address Translation,网络地址转换),不同内网(即局域网)中复用私有ip,通过地址转换来共享公网ip以访问公网。用于组建内网的私有ip被规定为10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16,它们不能出现在公网上,所以可以重复被不同内网使用,而公网ip在全球范围内有唯一性,可以访问公网。而大量设备在内网中被路由器动态分配(DHCP)有内网ip用于局域网通信,想获取公网资源则将报文交与路由器,使用公网ip获取资源,再由路由器返回,由此缓解公网ip不足的状况,顺带提升安全性。
前置知识,路由器如何路由?
首先,路由器工作在网络层(现代路由器在应用层),连接不同网络,转发网络之间的IP数据包,所以路由器至少有两套ip,一个LAN口ip即私有ip,一个WAN口ip即在更大的内网或公网中的ip。
路由表形如下图,Destination目标网络或主机地址,0.0.0.0或default表示默认路由;Gateway下一跳路由器的IP地址,0.0.0.0表示不经网关,接口直连;Genmask子网掩码;Iface指定网络接口发出。
路由器路由流程:路由器从某个接口(如以太网口、WAN口)接收到一个IP数据包,然后在路由表中查找与其目标ip匹配的路由条目,即一条一条地将Destination与Genmask做&,与目标ip对比,如果在同一个网络,则通过Iface发往下一跳的路由器Gateway,没找到目标网络则走默认路由,没默认路由则丢弃,并可能发送ICMP“目标不可达”消息回源主机,通知其数据包无法送达。ICMP自行了解。
内网中主机发送数据包之前,如果通过子网掩码发现目标在同一局域网中,则走ARP协议,下面讲;不在,说明目标在外网或不存在,则发给默认网关(通常是路由器)。
内网设备访问公网流程:一个家庭主机进程(192.168.1.10:1234)试图访问www.baidu.com(180.101.49.12),发现不在本地网络,则发给默认网关即路由器A(LAN口ip192.168.1.1,WAN口ip10.1.2.3),A查路由表,如果是首次访问百度,则执行NAT,修改数据包源ip为自己的WAN口ip,记录NAT表(建立ip+port的双向映射,192.168.1.10:1234<–>10.1.2.3:5000,也可采用四元组映射),发给下一跳路由器B(LAN口ip10.1.2.1,WAN口ip203.0.113.1),B查表,执行NAT,修改数据包源ip为B的WAN口ip,记录NAT表(10.1.2.3:5000<–>203.0.113.1:60000),此时终于可以进入互联网,经过骨干网路由,到达百度服务器。返程则通过查NAT表映射并修改目的ip,数据包到B处,目的ip与port会被修改为10.1.2.3与5000,A处同理,最终响应报文交至家庭主机。
当多个设备都想通过NAT经由路由器A向公网访问,而A只有一个WAN口ip,只建立ip映射并不唯一对应,所以NAT还会改变传输层的端口号,称为NAPT(Network Address Port Translation)
所以,当许多家庭和小型办公室网络使用私有IP地址并共享少量公网ip时,ip不足就大为缓解了。
数据链路层协议
前面讲的路由,解决的是子网与子网之间的数据包转发,接下来的工作于数据链路层的以太网技术解决的是局域网内部数据包收发问题。
数据链路层有个子层叫MAC(媒体访问控制)层,以太网帧就是在此层封装而成,以以太网为例,下文MAC帧即以太网帧,MAC地址即硬件地址,相当于公交车标识每一站的地址,而ip地址是标识起点和终点的地址。
以太网
以太网技术是最常用的局域网通信技术,用于子网内通信。
6+6+2=14字节帧头,4字节CRC检验帧尾,定长可分离;类型标识上层协议,ip报文填0800,arp请求/应答填0806,rarp请求/应答填8035,可分用。帧数据不多于MTU,不少于46字节,少了就补充到46字节。局域网是个碰撞域,同时发出去的两个数据包都会损坏。所以,数据帧过大,发生碰撞的概率也大,过小,则发送的帧数过多,碰撞概率也大,加之对效率的考量,得出的帧数据长度区间。
但是,最开始,局域网内主机A只知道主机B的ip地址,并不知道主机B的MAC地址,那么发送数据包时,怎么填充MAC帧头部目的MAC地址?于是有了ARP协议
ARP
ARP(Address Resolution Protocol,地址解析协议),通过主机广播发送ARP请求帧来得知主机B的MAC地址。
上图为ARP请求/应答,主机A发送前还需封装以太网帧头帧尾,由此可见ARP工作于MAC层上方,网络层之下。
A会在硬件类型填1,表示链路层网络为以太网;协议类型填0x0800表示上层协议为IPv4;硬件地址长度对于以太网填6;协议地址长度对于IPv4填4;操作码填1表示ARP请求;发送方MAC与IP填自己的;目的MAC不知道,填全0;目的IP填对方。而以太网帧头目的MAC填全F,表示广播地址。
发出ARP请求后,局域网内所有主机都收到该报文,发现目的IP不为自己则丢弃;B发现目的IP是自己,且是arp请求,就会发送arp响应(操作码填2表示ARP响应)。
A收到响应,通过响应中发送方MAC得知B的MAC地址,可以开始后续通信。同时,A会缓存下该ip与mac的映射直至过期被更新或删除。
以太网交换机,工作于数据链路层,用于局域网内数据转发。其上配有n个端口,不同主机连至不同端口而被划分为n组,划分出n个碰撞域,不同端口之间数据传输不相干扰。它也会维护MAC表,当有帧到来时,建立源MAC与端口映射,后期根据表转发帧只至目的MAC所在端口即可。
代理服务器
代理服务器用来代替本人做转发请求和响应。
正向代理面向客户端,由于对外显示代理服务器的ip,使客户端匿名访问;代理服务器可以提供访问控制和内容过滤,比如校园网可以限制访问某些网站;它也可缓存常用资源;绕过地域限制,即科学上网。
反向代理为服务端提供服务,通过将大量请求合理分配给各个后端服务器,实现负载均衡;可以缓存静态内容比如图片视频,减轻服务器压力;隐藏后端服务器ip。
内网穿透
内网穿透,让内网中设备能被外网访问。主要介绍3种方法。
- 端口转发:手动配置路由器端口转发规则,将外网对于该路由器特定ip+port的请求转发到内网某个设备的特定私有ip+port上。
- 反向代理:在具有公网IP的服务器上部署特定服务比如frp作为中继站,然后配置规则以转发请求给内网主机。原理是,内网主机B先和中继站主动发起长连接(穿透NAT,NAT转化表已缓存),外网的主机A发送给中继站的特定端口的数据流会被frp根据配置文件通过该长连接转发给B,而tcp全双工支持双向通信。适用于访问公司或家庭内网
- 内网打洞:一种P2P(peer-to-peer,点对点)通信技术,让不同内网中的设备直接建立连接。依然需要一台中继服务器,两个不同内网中的主机A和B分别和中继站建立连接,中继站再告知双方对方的出入口路由器特定ip+port,A和B就可以直接通过访问对方出入口路由器ip+port实现直接通信。见于视频通话避开中介服务器转发,降延迟;P2P传输文件;直播,无需平台服务器转发数据流
五种IO
首先,IO=等待+拷贝。其分五种,
- 阻塞式IO,阻塞式等待直至IO条件就绪,再拷贝,如read、write系列。。
- 非阻塞式IO,设置fd为非阻塞模式后,轮询式检测IO条件,不就绪会出错返回,如EAGAIN或EWOULDBLOCK
- 信号驱动式IO,利用信号如SIGIO通知进程IO条件就绪,可设置信号处理方法
- 多路转接/多路复用,一次监听多个文件描述符的状态变化,返回时指示哪些IO就绪,相当于将多个等待过程合并,面对高并发场景颇为高效,如select、poll等函数
- 异步IO,IO全程由os于后台负责,进程只需发起IO操作,而不参与等或拷贝
非阻塞和多路复用代码详见git仓库
多路复用
下面叙述select和poll和epoll之间的实现与优缺。
首先,它们都是让用户把等待的操作交给内核,可以一次等待多个事件,这是多路复用的定义。但是,select使用FD_SETSIZE大小的位图描述fd状态,有上限,通常是1024;poll虽然改用数组描述fd,没有上限,但是在大规模并发连接的情况下效率较低,因为前二者都需要线性O(N)遍历所有fd检查是否有IO就绪,再将fd位图或数组全部拷贝。而epoll使用红黑树来管理所有被监视的文件描述符,增删查O(logN),能高效找出就绪fd并放入就绪队列,一个双向链表,从而允许快速遍历已就绪fd而非所有,只拷贝就绪fd。这是底层结构差异,epoll数据结构优化,拷贝开销降低。
其次,select和poll本质是轮询,需要定期遍历所有fd。而epoll基于事件驱动,只有在IO就绪时才通知唤醒处在该等待队列下的进程,减少cpu浪费。在高并发情况下,前二者性能下降,而epoll依然坚挺。这是底层机制的差异,epoll高效适用于高并发。
epoll工作流程:
- epoll_create在内核创建struct eventpoll,包含红黑树根节点struct rb_root rbr、fd就绪队列struct list_head rdllist和进程等待队列wait_queue_head_t,以及单链表串起struct epitem*。其返回的epfd可找到struct file,file里的private data指向eventpoll。
- epoll_ctl+ADD时,内核创建struct epitem,其通过内含struct rb_node rbn插入至红黑树中。epitem含有struct epoll_filefd ffd,其含struct file *file和int fd,所以它是fd的真正描述体,eventpoll是管理体。内核还会为该fd注册一个回调函数,io就绪触发硬件中断时,会调用回调将其epitem添加至就绪队列尾,并唤醒wait在该fd下的进程队列。
- epoll_wait,内核检查就绪队列,不为空将其复制到用户传入的数组中,为空继续阻塞等待。注意,就算设定了timeout,只要有fd就绪,wait就会立刻返回,除非timeout内无事件就绪才会超时返回。
epoll默认工作于LT(Level Trigger,水平触发)模式,即只要fd是就绪的,epoll就会将其放入就绪队列通知你;ET(Edge Trigger,边沿触发)模式,即只有fd状态变化时比如不可读变可读、新数据到来,epoll才会通知你。ET时,假如用户一次没从缓冲区中读完数据,则epoll不会再通知你去读,所以ET必须配合while循环读取缓冲区而且得是非阻塞,直到EAGAIN到来循环结束,如果是阻塞一旦读完就会阻塞等待。而LT可以一次读取部分数据,因为epoll会因为数据仍在、fd仍就绪而持续提醒上层。
ET一方面倒逼上层一次性取完数据,提高通知效率;一方面增大己方接收窗口和对方滑动窗口大小,提高传输效率。
Reactor模式
采用epoll+非阻塞IO实现事件驱动、多路复用以应对高并发场景,这个模式就称为Reactor(反应堆)模式。
下面提供本人亲测的三套reactor代码,分别是单进程、多进程、多线程版本,思路均为master进程/线程负责管理/分发连接,worker进程/线程负责处理网络IO。
其实,还有一套代码,思路是多进程,父进程负载均衡通过管道通知+子进程继承listensock并处理IO,但是debug中,就不放了。
到这,Linux部分全部结束,后面本人要开始搞项目准备实习了,bye bye