43.传输层协议TCP(下)
理解4次挥手过程
三次握手本质:建立双方通信的共识,建立全双工通信
四次挥手:同样的,建立双方断开连接的共识
为什么是握手是三次,而挥手是四次?
握手在本质也是四次,因为第二次是捎带应答;握手的通用场景:客户端向服务器发起建立连接请求,服务器作为给客户端提供服务的,被设定了要无脑提供服务,面对客户端发起建立连接请求不能拒绝,因此第二次捎带应答的含义是服务端无脑同意建立连接。
挥手四次原因:以cs为例,客户端向服务端发起断开连接,表示客户端不想给服务端发了,服务端收到了需要应答,但服务端可能还想给客户端发,可以暂时不关闭,发完在关闭,就先回应答。等到服务端发完了,服务端向客户端发起断开连接,客户端收到发送应答会服务端,服务端在关闭。因此一般情况:客户端想断开时,服务端可能不想断开,就不能捎带应答了(无脑同意断开)
注意:
如果客户端已经退出,或者关闭,服务器端就是不关闭,close_wait,依旧占用fd,连接没有释放(fd用完,必须关掉,fd泄漏问题,服务端的连接会一直处于close_wait状态)。
关闭单向读写系统调用:shutdown
理解TIME_WAIT
TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态(MSL 是 TCP 报文的最大生存时间)MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7/Ubuntu上默认配置的值是60s;为什么要等待2个MSL时间?网络中,比如路由器中可能存在之前连接中发出去的数据,但这个数据已经被判定为丢失了,重传过了。以cs模式举例,客户端主动关闭连接,不等待。客户端直接重启,有概率OS分配的端口号就是上次的,此时客户端重新向服务端发起连接请求,三次握手,在三次握手期间服务端收到了网络中的陈旧报文,就会干扰连接建立。再者:服务端和客户端重现建立好连接后,正常收发时,此时服务端收到了陈旧报文,也会有影响。等待2个MSL时间可以保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失。在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发⼀个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);解决TIME_WAIT状态引起的bind失败的方法在 server 的 TCP 连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求)。这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉),就会产生大量TIME_WAIT连接.客户端可能重新连接的时候,IP不变,OS自动分配的端口号与上一次的重复了。使用 setsockopt ()设置 socket 描述符的 选项 SO_REUSEADDR 为 1 , 表示允许创建端口号相同但IP地址不同的多个 socket 描述符。设置完后可以立马重启。表示允许创建端口号相同但IP地址不同的多个 socket 描述符。扩充:三次握手期间,通信双方会交换序号,这个序号的选择是随机的,也可以用来对历史网络中的陈旧数据进行鉴别(例如:通信双方的16位窗口大小是在tcp报头上的,可以根据缓冲区剩余大小+随机起始序号来确定序号的范围,以此来区分陈旧报文)。不只有TIME_WAIT。socketaddr,timewait的时候照样绑定原端口,如果此时建立新的连接和原来的timewait的连接五元组完全相同,那么timewait是不是没有意义了?可以这样做:TCP 协议允许在特定条件下重用 TIME_WAIT 连接,前提是:
新连接的初始序列号严格大于旧连接的最终序列号
时间戳选项被启用(用于检测过时的数据包)
SO_REUSEADDR
就是告诉操作系统:"我知道风险,我接受在 TIME_WAIT 状态下重用的后果"。
SO_REUSEADDR
选项主要影响 bind() 系统调用,它允许:
在同一端口上绑定多个套接字(如果IP地址不同)
立即重用处于 TIME_WAIT 状态的地址/端口组合
滑动窗口
滑动窗口大小:无需等待确认应答而可以继续发送数据的最大值
滑动窗口:发送缓冲区的一部分。
逻辑上理解:把发送缓冲区理解成一个char类型的数组,滑动窗口维护的就是两个数组下标start和end,滑动窗口大小 = end-start。
发送缓冲区构成:
- 滑动窗口左边数据:已发送以确认的数据
- 滑动窗口内部:可以直接发,暂时不要应答的数据
- 滑动窗口右边:待发送和空区域
发送缓冲区的逻辑结构理解成环形队列,滑动窗口一直在向右滑动,左边的已发送已确认数据不用刻意清空,滑过即代表清空。序号在发送的轮次中,数字一直增大,意味着滑动窗口整体是向右滑动的。
滑动窗口本质:start && end 下标变化(另一角度:流量控制的具体实现方案)
滑动窗口(报文丢和应答丢两种情况)
如果最左边报文(1001,2000)丢了,中间(2001,4000)+最右(4001,5000)都收到了,应答报头的确认序号也只能填1001,因为确认序号表达的含义是在确认序号之前的报文全部收到了。
如果仅仅是最左边的应答丢了,中间+最右的应答都收到了,那么中间的确认序号填的是自己序号+1,最右的也填的是5000+1,自己序号+1。那么确认序号只填自己的序号+1即可滑动窗口核心问题:
滑动窗口丢包了,不会跳过报文进行应答(确认序号只能填丢包前的那个序号+1)
丢包的三种情况:
a.最左侧
b.中间
c.最右侧
- 最左侧报文的数据丢了,滑动窗口左边不动
- 最左侧报文应答丢了,但是数据没丢,滑动窗口正常工作(根据确认序号:收到的后续报文应答确认序号只要大于原确认序号,就可以视为没有丢)
快重传理解:收到3次重复的确认应答,就可以确定该确认序号开始的报文丢了。此时就会进行快重传。例如图中所示,快重传后1001-2000后,收到的报文确认序号是7001,说明7001以前的全部收到了,下次就可以从7001开始发。
如果中间还有丢数据包,怎么办?
后面还有数据传送,会累计收到三次重复确认应答进行快重传;如果没有了,会超时重传。
tcp暂时没有应答的时候,必须让对应的报文暂时保存起来 -> 保存到哪里?滑动窗口内部。如何理解保存?滑动窗口未收到应答表明收到时,不动。
流量控制补充话题
TCP支持根据接收端的处理能力来决定发送端的发送速度. 这个机制就叫做流量控制(FlowControl);
如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端(类似生产者消费者模型:生产者和消费者同步,等待在条件变量上,不过这次是消费者即发送方“自己唤醒自己”)
接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP⾸部中, 有⼀个16位窗⼝字段, 就是存放了窗口大小信息;那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口带下是窗口字段的值左移 M 位;
拥塞控制
场景:客户端向服务器发送1000个报文,其中900多个都丢失了,那么就可以判定网络出问题了(网络拥塞)
要怎么解决丢包?-> 立即重发吗?->不能立即重发 -> 同一个网络中,有数不其数的tcp报文,如果选择立即重发,大家用的都是tcp协议,那么网络负载就加剧了,不仅没解决问题还加重了。
解决方案:慢启动,一开始慢慢地发,指数级2^n。
前期大家都会慢,瞬间tcp报文少了很多,网络不拥塞了 -> 指数级,增长快,尽快恢复到网络通信的过程。
为了支持拥塞控制算法 -> 拥塞窗口(int):一个临界值,值以内,网络大概率不拥塞,值以上,网络可能拥塞(衡量网络是否会拥塞的标准,值会随网络更新变化)
更正概念:滑动窗口 = min(对方win,拥塞窗口)(谁小谁是主要矛盾,木桶效应)
拥塞窗口不能一直指数增长。
为什么前期要指数增长?前期慢,减缓网络压力;后期快,尽快探测并恢复网络通信。
- 指数增长:解决拥塞,恢复网络和通信
- 线性增长:不断探测网络的新的拥塞窗口的值
拥塞窗口在增加 != 发送的数据量一直在增加(还受对方win影响)
拥塞窗口在线性探测过程中,不会一直增大。
延时应答(效率问题)
延时应答:接收方主机接收到发送方的数据后,不立刻应答,而是等一会在应答。
目的:等一会,有大概率情况,接收缓冲区数据被读走了,接收方应答时相比于立刻应答可以更新出一个更大的窗口大小(进而更能更新出更大的滑动窗口大小)。
TCP小结
面向字节流会导致粘包问题 -> 解决:明确报文和报文之间的边界 -> 协议+序列化和反序列化
TCP异常情况
- 进程终止/机器重启:进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没区别。
- 机器断电/网线断开:接收端认为连接还在,一旦接收端有写入操作,发现连接不在,发送reset,连续发送都没有响应,关闭连接。
TCP自己也内置了一个保活机制,会定期询问对方在不在,大几十分钟,兜底的。
保活机制,一般是需要应用层自己完成的(TCP自己不能随意关闭连接,因为连接场景很复杂)
内核中TCP和UDP对应的结构体(网络和文件关联)
进程task_struct -> 包含struct files_struct文件描述符表 -> 包含fd文件描述符 -> 指向具体的struct file文件对象 -> struct file中包含 void *private_data指针指向 struct socket ->struct socket内部也有文件指针回指(通过struct file中的private_data指针指向对应的socket,socket在回指,实现了socket和文件file关联)。
struct socket内部有struct sock *sk,指向具体的通信。struct sock内部有接收和发送队列,tcp缓冲区连续(看成一维字符数组)和面向字节流是被抽象过一层的,udp是一块一块的。
struct tcp_sock 和 struct udp_sock 是一套用C语言实现的多态机制,tcp比udp多一层inet_connection_sock。