上文介绍了TCP传输控制协议的报头,并且渗透了TCP保证可靠性的策略:如流量控制、按序到达、确认应答机制以及超时重传。本文继续讲解TCP剩下的协议,剩下俩个大话题,难度都比较麻烦。
本文将介绍TCP协议最常见的三次握手和四次挥手、理解为什么需要这样做。另外TCP为了提高效率也做了很多努力,如滑动窗口、延迟应答。另外保证可靠性的另一个措施:拥塞窗口。
三次握手
三次握手的目的是双方可靠的建立连接 。在TCP报文中将标记位SYN设为1。然后向目标服务器发送携带SYN标记位的报文。对端收到报文后,会对报文进行ACK,告知建立连接。
介绍一下这个过程:
以客户端向服务器主动建立连接为例子,讲解一下三次握手。
- 客户端会向服务器发送SYN请求建立连接的报文。
- 服务器收到报文后要对报文ACK回复,同时服务器也要和客户端建立连接,也要发送SYN建立连接的报文。为了提高效率服务器的SYN+ACK被压缩成一条捎带应答发送给客户端。客户端接收到ACK后,会在OS中创建连接管理,维护连接的属性。同时对服务器进行ACK。
- 服务器收到最后一条ACK后,在OS中创建链接管理信息,连接真正的被建立。
最后一次握手是不可靠的,因为客户端不知道,服务器是否收到ACK报文。但是RST重置位标志保证了双发是可靠的建立链接。
建立一个共识:
建立连接是需要成本的!双方会在内核中开辟结构体,填充相应的字段,比如端口号,序号,缓冲区,窗口大小等。建立连接后,上层调用accept获取文件描述符,将对应的读/写通道提供给上层。所以处于connection没有accpet是不影响的。
常见的问题
为什么要进行三次握手?
本文将从三个角度讲讲为什么需要三次握手?以客户端主动连接服务器为例。
1.最小成本的验证全双工。因为tcp是全双工的,可读可写。
第一次握手表明客户端就有发的能力,并且等待服务器的确认。第二次握手,服务器确认客户端消息,并且表明自己有收和发的能力。第三次客户端确认收到服务器的ack+syn,知道客户端有收的能力。至此客户端和服务器都确认了双发就有收数据和发数据的能力。
2.奇数次握手,客户端会优先建立链接,维护结构体等等。而服务器只有在收到客户端的ack之后,才建立链接。避免了单机程序,恶意发起连接建立引发syn洪水。
3.保证100%可靠的收到建立连接的请求。三次握手本质是四次握手。三次握手当中,有一次是syn+ack是捎带应答。如果将syn + ack拆开的话。就单看俩个朝向上。客户端syn被可靠的接收,另一端 服务器的syn 也被可靠的得知,所以建立连接是可靠的。
四次挥手
四次挥手的目的是断开连接。客户端向服务器发起FIN(结束连接)TCP报文。服务器收到FIN请求后进入WAIT_CLOSE状态,并且向客户端发送ack报文。一般发送完ACK之后,再发送FIN请求断开连接的TCP报文。客户端收到服务器的FIN报文发送ACK报文后进入TIME_WAIT状态。
为什么不能是三次挥手呢?
客户端主动发起FIN,代表客户端不会给服务器发消息了。C->S朝上是关闭的,而S->C上的朝向是开着的。服务器收到FIN时,可能还有数据需要发送给客户端,短暂的延迟后,才关闭S->C的通道,是为了让客户端将剩余的数据收完。
TIMEWAIT(等待状态)
主动断开连接的一方会进入TIME_WAIT状态。进入TIME_WAIT状态,连接并不会被彻底释放掉,会保留一段时间,也就导致地址和端口被占用。
存在的意义:
1.最后一次ack丢失,需要重复给服务器
2.网络中可能由于阻塞,存在尚未发送完全的报文。如果立马消失,就会导致上一次没法完的数据,影响新连接。所以一般都要等待旧报文消失。
存在的时间:
2*MSL:报文在网络中存在的时间的2倍。
一般一个MSL是60秒
如何保证旧报文不会影响新连接呢?
策略1:TIME_WAIT延时
策略2:随机一个起始序号
滑动窗口
为了保障效率,TCP允许并发发送大量暂时不需要ack的报文。那么要发多少数据是由对方的接收缓冲区决定的,也就和上文的流量控制有关系,那么要如果控制呢?这就要学习本文的滑动窗口
可以将发送缓冲区想象成一个char 类型的数组,数组中天然的就有下标。
滑动窗口是什么?
在发送缓冲区中,维护了start和end指针,start指针和end指针所包含的区域就是滑动窗口。
滑动窗口的大小是有对方接收能力大小约定的。
粗略的更新规则:
- start=确认序号。
- end=确认序号+对方窗口大小。
来自图解TCP/IP的经典图画
主机A发送序号为2000的报文,主机B确认并且告知窗口大小是4000
那么start=2001,end=2001+4000=6001
但是这样解释并不全面。下面更深刻的理解一下滑动窗口
建立一个共识:
将缓冲区的数据根据滑动窗口划分成三部分,最左边的以及发送,以及ack的数据。中间的可发送的数据。
数据2000-5000的数据被并发发出去。会出现这几种情况
1.最左端的报文丢失:2000的报文丢失/ack丢失、
那么就需要重新发送报文。报文该如何发呢?
如果2000报文丢失,3000、4000、5000正常收到。那么确认序号是多少i?是1001,因为确认序号表示,之前的数据被全部收到。
会发送四条1001的ack给主机A,主机A收到连续三条相同的报文后,会进行重发最左端的报文。
真正收到2001ACK后就会滑动窗口
2.中间丢失呢?
中间丢失会转化为最左端的数据丢失,如果3000丢失,会收到2001ack。重发3000
3.最右报文丢失呢?也会转化会最左端丢失。
如何知道起始的窗口大小?
在三次握手期间,会进行窗口大小的交换。
所以滑动窗口就是流量控制的实际策略。
快重传
刚才提到一个概念,连续收到三条相同序号的ACK后,会立即重发报文。这个机制叫做快重传。
快重传VS超时重传
既然有了快重传,那为什么还要有超时重传?
快重传是提高效率的,如果连续收到三条相同序号的ACK就会重发。但是在极端情况下,数据只有2条。只有4000,5000,就不能发送三条连续报文。所有超时重传是兜底的。二者相互构成TCP重发策略。
拥塞控制
之前谈到TCP保证可靠性,都是都在双方主机上。如果网络出现拥塞,那么就算滑动窗口再大,也不能将数据报文全部发出。否则就会导致网络更加拥堵。
控制单次发送量的多少是由滑动窗口来决定的。也就是说在网络不好的时候,窗口就应该减小。网络好的时候,再由接收端的缓冲区决定。
TCP协议还考虑了网络健康状态,设计了一种策略避免网络的算法——拥塞控制。
如果网络发生了拥挤,说明当前的网络群出现了问题。就发送小部分报文进行试探。如果试探出是正常的,则快速增加单次发送的大小。这就是拥塞控制的特点:前期慢,增长快。
当然,也不能一直无限制的快速增长,增长到某一阈值的时候,增速就放慢。如果网络再次出现拥塞,发送的大小就立马减少到1。
重点关注:
阈值、窗口大小
这种机制叫做慢启动,先发送少量数据,探探路,摸清楚当前的网络状况后,快速恢复。
再回到流量控制,流量控制的策略是滑动窗口,而滑动窗口不仅依赖于对方窗口大小,还依赖于拥塞窗口的大小。
滑动窗口
- start=确认序号 ack_seq
- end=确认序号+min(对端窗口,拥塞窗口)
延迟应答
如果每一份报文都采用ack。那么上层来不及将数据取走,就会导致窗口的减少,而确认序号表示:确认序号之前的报文全部收到。那么就没必要每一个报文都立马进行ACK。
延迟应答是提高效率的策略,每隔N个报文就进行一次ACK,那么上层就有时间取走数据。窗口就更大,发送的数据就更多。
粘包问题
由于TCP协议是面向字节流的,数据是无边界的,发多少就收多少。一份数据的结尾可能读到下一份数据,这就产了一个严重的问题。数据粘包:读到报文的实际数据偏少或者偏多。
为了解决粘包问题,常见的策略
- 固定长度
- 变长+自描述字段:比如再报头添加长度
- 变长+结尾分隔符
另外粘包问题是针对TCP协议,UDP无粘包问题。因为UDP面向数据包,报文本身就有自描述字段,天然是一份完整的报文。
TCP异常
进程终止:OS会自动进行四次挥手,自动关闭链接
重启:自动进行四次挥手,自动关闭链接
机器断电:保活机制
如果机器发生断电:对端的服务会定时发送一个报文,如果在保活期间得不到ACK后会自动关闭链接。但是这个保活时间一般会维持120分钟。不推荐用TCP自带的保活机制,由引用层自己设置。
至此TCP协议的绝大多数的内容都已经介绍完毕了
TCP保证可靠性的策略
- 流量控制
- 按序到达
- 确认应答
- 超时重传
- 连接管理
- 拥塞控制
不仅如此TCP为了提高效率也做了很多准备
提高效率
- 延迟应答
- 快重传
- 滑动窗口
- 捎带应答
另外TCP效率不一定比UDP慢!TCP是可靠的,UDP是不可靠的。可靠与不可靠是中性的。