2.2高性能网络设计专栏-网络原理
- posix api
- reactor数组大小1048576的优化
Posix API与网络协议栈
Linux底层都是由Posix API实现的。
- 服务器API:
socket(), bind(), listen(), accept(), recv(), send(), close()
- 客户端API:
socket(), bind()//可选, connect()//udp, send(), recv(), close()
- epoll API:
epoll_create(), epoll_ctl(), epoll_wait(), fcntl()
API的过程:
- 建立连接
- 传输数据
- 断开连接
socket()
socket中文可翻译为插座
- 插座:<插, 座>
- 网络io: <fd, tcp control block>
socket在实现的时候有两个功能 (M: socket是如何实现的?)
a.fd的分配(int 类型)
使用bitmap算法进行回收分配
bitmap:用一个bit表示,某一位置是否已经使用(0没有/1被占用), 比如下面有 1个Byte,共有8个bit,然后从前往后找,找到第一个值为0的位,把该位给分配出去。

b.tcb的分配;
tcp control block的分配, alloc()
bind()
bind(fd, ) (M: bind的过程是什么?)
-
把对应的ip地址,port端口绑定到socket对应的tcb。所谓的绑定就是set的过程,通过fd找到对应的tcb,再把相应的ip、port设置进去。
-
客户端/服务器都可以有bind
客户端如果没有bind,connect会为其随机的绑定一个port(1024-65535), 如果有bind,可以指定port端口,使用于对端口分配较严格的场景。(比如IM通信为例,当请求第三方,比如云主机对端口分配较为严格)
listen()
a. tcp->status = TCP_STATUS_LISTEN;
b. tcb->syn
tcb->accept_queue
每个tcp包都有这个头:

建立连接:TCP的三次握手
对照着tcp状态迁移图学习:


- 客户端先发起请求, 向服务端发送tcp包,进入SYN_SENT状态:[SYN=1,seqnum=1234],一般seqnum为随机值
- 服务端解析这个tcp包,发现有[SYN, seqnum],确认是客户端发送三次握手请求后,向客户端返回[ACK=1, acknum=1235(seqnum+1), SYN=1, seqnum=5647(随机值)], 进入SYN_RCVD状态
- 客户端发送数据 [ACK=1, acknum=5648], 进入ESTABLISHED, 服务端接受到ACK,进入到ESTABLISHED
三次握手主要是确定双方发送数据从什么时候开始,这里的seqnum、acknum的作用是保证不重复、不乱序。
第一个过程表示客户端向服务端发送从seqnum(1234)开始,下一次发从acknum(1235)开始;
第二个过程表示服务端向客户端发送从seqnum(5647)开始,下一次发从acknum(5648)开始;
(1)M: 三次握手的过程,发生在哪些函数里面?

半连接队列 syn队列
全连接队列 accept队列
- a. tcp连接的生命周期,从什么时候开始?
从client第一次连接开始,server端listen()到syn包,协议栈会为其分配对应的tcp连接。
- b. 第三次握手数据包,如何从半连接队列查找匹配的节点?
通过五元组 [源ip,源port,目的ip, 目的port, 协议类型],从而查找到匹配的节点
- c. 如何防护SYN泛宏,DDos攻击(攻击者模拟客户端多次向服务端发送第一次请求,真正需要服务的顾客无法正常访问,可能导致服务器崩溃)
(2)M:listen的第二个参数是什么?
从上世纪70年代,listen已经历了多个版本的迭代,listen(fd, backlog), 第二个参数backlog共有三个版本
1)syn队列长度
在旧版本中(70年代)listen(fd, backlog), 通过backlog参数来设置半连接syn队列的长度,从而限制连接无限增长。
优点:避免SYN泛宏
缺点:鸡肋
2)syn + accept队列总长度,未分配fd的tcb的数量
优于 1)
3)accept队列长度
加快建立连接的速率,更符合网络的需求
accept()
1)分配fd
2)fd --> tcb
关于IO有没有数据?
当一个I/O事件(例如,新的数据到达可读,或者套接字可写)发生时,epoll会根据其配置的触发模式来通知应用程序
(3) 如果双方同时connect,发起三次握手呢?
在客户端调用 connect 函数之前,其实我们还可以通过 bind 函数将客户端绑定一个端口,那么如果此时我们再在另一个客户端通过 connect 函数连接该客户端,这时就是 p2p 连接.
fd = socket();
localaddr, remoteaddr;
bind(8000); //optional
connect();
p2p: 这是一种Tcp的点对点连接,没有所谓的客户端与服务端的概念,这种方式是一种去中心化的连接方式,中间不通过网络、Server,直接进行通信,传播速度更快和方便。

水平触发与边沿触发
水平触发(Level Triggered, LT):检测有无数据,可触发多次,可分多次读完
这是epoll的默认工作模式。在这种模式下,只要文件描述符上存在可用的I/O条件(例如,读缓冲区中仍有数据可读,或者写缓冲区仍有空间可写),epoll就会持续地报告该事件。即使应用程序没有一次性处理完所有数据,只要条件仍然满足,epoll会反复触发该事件,直到所有数据都被处理完毕,或者写缓冲区被填满。
边沿触发(Edge Triggered, ET):检测数据从无到有(过程),只触发一次,且一次性读完.(相当于一个while 1,直到读完退出)
在这种模式下,epoll只会在文件描述符上的I/O条件发生变化时(即从“不可用”变为“可用”的瞬间)通知一次事件。一旦事件被通知,即使文件描述符上仍然存在可用的I/O条件,epoll也不会再次触发该事件,直到下一次I/O条件发生新的“边沿”变化。应用程序必须在一次事件通知中尽可能多地处理所有可用的数据或完成所有可写的操作,否则剩余的数据或未完成的操作将不会再次触发事件通知,可能导致数据滞留或饿死。
所以 accept适合用 水平触发(LT),有数据就触发,有多个连接,每次处理一个节点。
那 M: 如果用 边沿触发(ET), accept该如何写, 从而可以处理多个连接?
因为ET只触发一次,为了能处理多个连接,可以使用一个while循环
while(1){
if(-1 == accept()) break;
}
Poisx API

只讨论传输层 tcp协议是描述的两个机器kernel协议栈互相通信
代码实现层面则是posix API和kernel协议栈的交互逻辑
tcp通信涉及到的拥塞控制和滑动窗口
拥塞控制是一组算法和机制,用于检测和响应网络中的拥塞情况,防止过多的数据注入网络。
主要算法:
慢启动(Slow Start):在TCP连接建立之初,窗口大小指数增长,直到达到一个阈值(ssthresh),然后转入拥塞避免阶段。
拥塞避免(Congestion Avoidance):在达到慢启动阈值后,窗口大小线性增长。

快速重传(Fast Retransmit):在检测到丢包时,立即重传丢失的数据包,而不是等待重传定时器超时。
快速恢复(Fast Recovery):在快速重传后,适当调整窗口大小,以快速恢复传输。
在传输阶段,涉及到几个概念:
1.慢启动:慢启动是 TCP 拥塞控制的初始阶段,用于在连接建立后逐步增加发送速率,避免一开始就向网络发送大量数据导致拥塞。尽管名为 “慢启动”,但实际速率增长是指数级的,只是起点较低,因此称为 “慢”。
2.拥塞控制:拥塞控制是网络协议通过调整发送方的数据包发送速率,使网络负载处于合理范围,避免拥塞发生或缓解已发生拥塞的机制
3.滑动窗口:滑动窗口是 TCP 实现流量控制和拥塞控制的基础机制,它通过在发送方和接收方维护一个 “窗口” 范围,动态限制发送方未确认数据的最大量,从而实现高效的双向数据传输。
4.延迟确认:当接收方收到数据后,不立即发送 ACK(确认数据),而是等待一小段时间(通常 200ms 以内),若这段时间内有其他数据需要发送给对方(如应用层响应数据),则将 ACK 与数据报文合并发送;若没有,则超时后单独发送 ACK。
5.超时重传:在超过一定时间内发送方没有回复,默认这个包重传。
close()
1.将fd回收
2.发送一个fin包
断开连接:四次挥手
主动方和被动方

第一次握手:客户端发送FIN报文,表示 “我已完成数据发送,请求关闭我的发送通道”,发送后,客户端进入 FIN_WAIT_1 状态,等待服务器的确认。
第二次握手:服务器回复ACK报文,告知客户端 “我已收到你的关闭请求”,发送后,服务器进入 CLOSE_WAIT 状态,此时客户端到服务器的发送通道已关闭,但服务器仍可向客户端发送数据。
客户端收到 ACK 后,客户端进入 FIN_WAIT_2 状态,等待服务器的 FIN 报文。
第三次握手:服务器向客户端发送FIN报文,表示 “我也完成数据发送,请求关闭我的发送通道”,发送后,服务器进入 LAST_ACK 状态,等待客户端的最终确认。
第四次握手:客户端回复ACK报文,发送后,客户端进入 TIME_WAIT 状态,确保服务器收到 ACK),最终进入 CLOSED 状态。服务器收到 ACK 后,立即进入 CLOSED 状态,释放所有资源。

可能遇到的问题:
1)fin_wait_1 ack没有收到,先收到fin
ESTABLISHED -> FIN_WAIT_1 -> TIME_WAIT
2) 双方同时调用close
双方都是发送FIN,进入FIN_WAIT_1, 接收ACK, 进入CLOSING, 接收ACK, 进入TIME_WAIT
(如果服务器出现大量TIME_WAIT,可能是出现了双方同时调用close的情况)

浙公网安备 33010602011771号