Posix API 与网络协议栈
TCP协议的特点
- 面向连接
- 点对点:连接的只有两个端点
- 可靠传输:无差错、不丢失、不重复,有序
- 全双工通信:双向通信,两端设有发送缓存和接收缓存
- 面向字节流
tcp 相关的 Posix API
- 服务端:
socket - bind - listen - accept - recv - send - close
- 客户端:
socket - connect - send - recv - close
查看网络相关状态命令:netstat -nat
1、TCP 报文段
tcp 首部
tcp 报文段分为 tcp 首部和 tcp 数据两部分
- 源端口和目的端口
- 序号 seq:本报文段发送的数据的第一个字节的序号。tcp面向字节流,为数据流中的每一个字节编上一个序号
- 确认号 ack:期望收到对方的下一个报文段的数据的第一个字节的序号。确认号为n,表示序号 n-1 前的所有数据都已经正确收到。序号 + 有效荷载 = 确认号
- 数据偏移:首部长度,单位4B
- 窗口:允许对方发送的数据量,单位1B
- 校验和:首部 + 数据
2、连接管理
每个 tcp 连接都有三个阶段:连接建立、数据传输和连接释放。
2.1、连接建立
2.1.1、posix API
socket()
:为文件系统分配fd,配置tcb(tcp control block)
。bind()
:为tcp
绑定本地的ip
和端口。客户端bind(opt)
,不绑定则随机端口。listen()
: 把tcb
状态置位 listen,参数backlog
指的是全连接队列的长度。connet()
: 向服务端发送SYN
报文,开始协议栈的三次握手,并等待三次握手的返回结果。accept()
:阻塞,直至全连接队列非空。此时,从全连接队列中取一个tcb
结点并为其分配1个 socket。
2.1.2、三次握手
tcp 建立三次握手的时候,内核做了哪些事情
服务端接收第一次握手,回复第二次握手的同时,把未建立完的 tcp 连接放入半连接队列里面。
第三次握手成功后,服务端收到 ACK
报文,从半连接队列中取出 tcb 结点放入全连接队列,发出通知,accept()
收到信号后从全连接队列后取出1个 tcb 结点,并为其分配1个 socket 。
因此三次握手发生在客户端调用connect()
后,服务端调用listen()
后accept()
执行前。
tcp 三次握手
2.1.3、常见面试题
问题1、三次握手的原因
防止旧的重复连接引起连接混乱的问题(为什么不是两次握手)
- 服务端收到已断开连接的
SYN
报文,服务器陷入忙等状态, - 客户端未收到
ACK
报文,而服务器端已处于ESTABLISHED
状态,等待客户端发送数据,造成资源浪费
为什么不是四次握手?没必要,浪费资源。
问题2、DDos | SYN flood 攻击原理
- 半连接队列:当客户端发送的
SYN
被服务端接收后,该连接加入syn
队列,状态SYN_RCVD
- 全连接队列:当客户端返回的
ACK
被服务端接收后,该连接加入accept
队列
DDos | SYN flood 攻击原理:攻击方伪造 IP 发送大量的SYN
报文到服务端,服务端不断重发 SYN
和 ACK
报文却无法得到响应,大量的连接处于SYN_RCVD
状态。当半连接队列中的半连接数量足够多时,无法处理正常的连接请求,资源耗尽。
ddos | syn flood 攻击
解决:
- 减少
SYN
+ACK
重试次数,避免大量的超时重发; - 利用
SYN Cookie
技术,在服务端接收到SYN
后不立即分配连接资源,而是根据这个SYN
计算出一个Cookie,连同第二次握手回复给客户端,在客户端回复ACK
的时候带上这个Cookie
值,服务端验证 Cookie 合法之后才分配连接资源。
问题3、双方同时发送 SYN 报文
- 发完
SYN
,两者的状态都变为SYN-SENT
- 在各自收到对方的
SYN
后,两者状态都变为SYN-REVD
- 回复对应的
ACK + SYN
,这个报文在对方接收之后,两者状态一起变为ESTABLISHED
同时发送 syn
问题4、已经建立连接的一方突然断开
TCP 的KeepAlive
保持服务端与客户端的连接,一方不定期发送心跳探活包,另一方回复 ACK
。若另一方断开连接,无法响应,返回RST
,则释放当前连接。
区分 http 的keep-alive
:短时间内连接可以复用。
2.2、数据传输
2.2.1、posix API
recv()
从fd
对应的内核态tcb
的readbuffer
的数据拷贝到用户态send()
将用户态的数据拷贝到fd
对应的内核态tcb
的sendbuffer
中
recv send
2.2.2、tcp 分包与粘包问题
原因:tcp面向字节流,没有边界,不能通过send
返回值判断。解决这一问题有两种方案:
- 应用层协议首部增加数据报的长度
pktlen
read(tcphdr, 2);
read(tcphdr->length);
while (cout < tcphdr->length) {
size = read(tcphdr->length - count);
count += size;
} - 为每个包加上分隔符,如
'/r/n'
read(buffer, 1024);
buffer[idx] = '/r/n';
pktlen = &[idx + 2];
2.3、连接释放
2.3.1、posix API
close()
: 将FIN
报文放入sendbuffer
中,回收fd
2.3.2、四次握手
tcp 四次握手
2.3.3、常见面试题
问题1、服务端出现大量close_wait
状态
原因:客户端主动关闭连接(recv返回0)
,服务端忙于读写,未及时关闭连接close()
解决:业务数据处理与网络层处理异步分离
问题2、双方同时关闭连接
双方变化状态是一致的,即双方发送了FIN包后,收到了对方的FIN包,进入 CLOSING
状态。
closing 状态
- 双方发送
FIN
报文后,进入FIN_WAIT_1
状态 - 接收到对方的
FIN
报文后,进入CLOSING
状态,并向对方发送ACK
- 接收到对方的
ACK
报文后,进入TIME-WAIT
状态,等待2MSL后,关闭连接
同时发送 fin
问题3、主动断开为什么会有TIME_WAIT
状态
避免由于最后一次握手时ACK
包的丢失造成问题,原因如下:
- 1、保证四次握手顺利完成
- 假设第四次挥手的
ACK
丢失,此时主动断开方认为对端没有收到FIN
报文,该连接没有正常断开,重发第三次挥手的FIN
报文。 - 2、保证旧的报文在网络中消失
连接的socket
五元组信息相同,可看作是同一个连接。若已经失效的报文请求出现在本次连接中,则造成数据蹿链。
问题4、TIME_WAIT
状态为什么是2MSL
MSL
:Maximum Segment Lifetime
,报文最大生存时间
确保对端没有收到第四次ACK
报文时重传的FIN
报文可以到达主动断开方。(ACK
+超时重传FIN
)
问题5:四次握手的原因
TCP是全双工的连接,需要把两个方向上的数据传输都断开
问题6:服务器端能否主动断开连接?
可以,但是主动断开连接的一方会进入TIME_WAIT
状态,该状态会持续 2MSL
的,造成服务器端的资源浪费。可以设置服务器的网络地址为可重用,可解决该问题。
2.4、tcp 状态转移图
tcp 状态转移图
3、可靠传输机制
- 序号:保证数据有序提交给应用层
- 确认:累计确认 + 延迟 ACK
- 重传
- 超时:计时器到期未收到确认则重传对应的报文
- 冗余确认:当收到失序报文时向发送端发送冗余ACK
4、流量控制
4.1、高效发送的因素
如何让tcp高效的发送数据,主要考虑两个因素:
- 对方还能接收多少 (流量控制)
- 网络上还能发送多少(拥塞控制)
结论:发送窗口 swnd = min (rwnd, cwnd)
,这里我们先来介绍流量控制。
流量控制指的是点对点通信量的控制,匹配发送方的发送速率和接收方的读取速率,避免发送方发送速率过快,接收方来不及读出,导致接收方缓存区溢出。
方法:在确认报文中设置接收窗口rwnd
的值来限制发送速率。对应 tcp 报文首部的窗口 window
字段。先收缩窗口大小,再减少缓存。
传输层流量控制和数据链路层流量控制的区别
- 数据链路层定义两个中间的相邻结点的流量控制,窗口大小固定
- 传输层定义端到端用户间的流量控制,窗口大小动态变化
4.2、滑动窗口
tcp 采用选择重传 ARQ协议 + 累计确认
tcp_发送方滑动窗口
发送窗口有三个指针:
SND.WND
:表示发送窗口的大小,大小是由接收方指定的SND.UNA
:指向已发送但未收到确认(发送窗口)第一个字节的序列号SND.NXT
:指向未发送但在可发送范围内(可用窗口)的第一个字节的序列号
3 个指针将滑动窗口分为 4 个部分
- 已发送并收到ACK
- 已发送未收到ACK,
SND.UNA
指向 - 未发送但在接收方处理范围内,
SND.NXT
指向 - 未发送超过接收方处理范围内,
SND.UNA + SND.WND
指向
可用窗口大小 = SND.WND -(SND.NXT - SND.UNA) = SND.UNA + SND.WND - SND.NXT
接收方滑动窗口
tcp_接收方滑动窗口
接收窗口有两个指针
RCV.WND
:表示接收窗口的大小,它会通告给发送方。RCV.NXT
:指向期望从发送方发送来的下一个数据字节(接收窗口)的序列号
两个指针将滑动窗口分成三个部分
- 已接收数据并确认,等待应用进程读取
- 未收到数据但可以接收,
RCV.NXT
指向 - 未收到数据且不可以接收,
RCV.NXT + RCV.WND
指向
5、拥塞控制
概念:全局性的过程,防止过多数据注入网络,使网络能够承担现有负荷。
方法:根据自己估算的网络拥塞程度设置拥塞窗口 cwnd
来限制发送速率。
"如何判断网络拥塞?"
发送方没有在规定时间内接收到 ACK 应答报文,发生了超时重传,则认为网络出现了拥塞。
超时的计算:rtt(new) = 0.9 * rtt(old) + 0.1rtt
5.1、拥塞的控制算法
1、慢启动
tcp 刚建立好连接并开始发送数据时,cwnd
= 1,执行慢启动
规则:发送方每收到1个 ACK,cwnd + 1(1个MSS
: 最大报文段长度)
每经过一个 RTT,cwnd
加倍 ,cwnd
的指数增加。一直到达慢启动门限 ssthresh
,改用拥塞避免算法。若 2cwnd > ssthresh
,则下一个 RTT 时:cwnd = ssthresh
慢启动
2、拥塞避免
当拥塞窗口 cwnd
超过慢启动门限 ssthresh
就会进入拥塞避免算法。
规则:发送方每收到1个 ACK 时,cwnd + 1/cwnd。此时,每经过1个RTT,cwnd + 1。
拥塞避免
随着拥塞窗口的线性增加,当网络出现丢包现象,触发了重传机制,进入拥塞发生算法。
3、拥塞发生算法
当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种,注意区分:
- 超时重传:超时,慢启动
- 快速重传:收到3个冗余ACK,快恢复
“为什么超时执行慢启动,而收到3个冗余ACK却执行快恢复”
收到3个冗余 ACK 时,说明网络虽然拥塞,但是至少还有 ACK 报文能够正确被交付。而当超时发生时,说明网络可能已经拥塞到连ACK报文都传输不了,发送方只能等待超时后重传数据,因此,超时发生时,网络拥塞更严重,发送方应该最大限度地一直数据发送量,cwnd重置为1。
超时重传
发生超时重传时
ssthresh
设为cwnd/2
,cwnd
重置为1
- 慢启动
超时重传
快速重传和快恢复
当接收方发现丢了一个中间包的时候,发送三次该包前一个包的 ACK,于是发送端就会快速重传,不必等待超时再重传。
cwnd = cwnd/2
ssthresh = cwnd
;- 快速恢复算法
快速恢复算法
cwnd = ssthresh + 3
( 有 3 个数据包被收到了);或cwnd = ssthresh
- 重传丢失的数据包
快速重传和快恢复
完整的 tcp 拥塞控制算法如下:
tcp 拥塞控制算法