运输层协议为运行在不同主机上的应用进程之间的逻辑通信;
TCP与UDP为两种传输层的协议,TCP(传输控制协议)为调用它的应用程序提供一个可靠的、面向连接的服务,而UDP(用户数据协议)为调用它的应用程序提供一种不可靠的、无连接的服务;
TCP与UDP最基本的责任是将两个端系统间IP的交付服务扩展为运行在端系统上的两个进程之间的交付服务;
TCP报文头
Source Port/Destination Port: TCP与UDP的数据包都是不包含IP地址信息的,那是IP层面上的事情,但是TCP与UDP都会有源端口和目的端口。两个进程在计算机内部通信可以有管道、内存共享、信号量、消息队列等方法,而两个进程如果需要通信最基本的前提是能够唯一的标识一个进程,通过这个唯一的标识来找到对应的进程。在本地的进程通信的过程中我们可以使用PID(进程号)来唯一的标识一个进程,但是PID只在本地唯一,如果把两个进程放到不同的计算机中,它们如果想要进行通信,此时PID就不够用了,解决这个问题的方法就是在传输层中引入协议端口号(端口)。IP层的IP地址可以唯一的标识一个主机,而TCP协议和端口可以唯一标识主机中的一个进程,所以我们利用IP地址 + 协议端口号这样的组合方式来唯一标识网络中的一个进程。有时也把这种唯一标识的模式称为套接字(socket)。虽然通信的重点是应用进程,但是我们只需要将传送的报文交到目的主机的某一个合适的端口,剩下的工作就交给TCP来完成就好。
Sequence Number:序号是建立在字节流之上的,每一个报文段的序号即是该报文段中首字节的字节流编码。假设主机A中的一个进程想通过一条TCP连接向主机B上的一个进程发送数据流,主机A中的TCP将隐式地对数据流中的每一个字节编号,假设数据流由一个包含50000字节的文件组成,其MSS为1000字节,数据流的首字节编号为0,则TCP将为该数据流构建50个报文段,即第一个报文段的序号为0;第二个报文段的序号为100,第三个报文段的序号为200,以此类推,每个序号被填入到相应TCP报文段首部的序号字段中去。
Acknowledgment Number: ack确认号是期望收到对方下一个报文段中首个字节的序号即下一个报文的序列号。假如B收到了A发送过来的报文段,其序列号是501,并且数据的长度是100字节,这表明B正确收到了来自A的序号从501-600的数据,因此B期望接着收到A的序列号为601的报文段,所以B在发送给A的确认报文段中会把ack置为601。
Offset:即数据偏移,由于头部有可选字段长度不固定,因此它指出TCP报文的数据距离TCP报文的起始处有多远。
Reservd:保留区域,保留给将来使用,目前必须置为 0
TCP Flags: 控制位,由八个标志位组成,每个标志位代表一个控制功能 ;常见的标志位如下:
- URG: 紧急指针标志,用于保证TCP连接不被中断,并且督促中间层设备尽快处理。URG=1->紧急指针有效;URG=0->忽略紧急指针
- ACK: 确认序号标志,ACK=1-> 确认号有效;ACK=0 -> 报文不含确认信息,忽略确认号字段
- PSH: push标志 ,push=1->表示是带有push标志的数据,指示接收方在接收该报文段以后应该尽可能的将该报文段交给应用程序,而不是放到缓冲区排队。
- RST: 重置连接标志,用于重置由于主机崩溃或者其他原因而出现的错误连接,或者用于拒绝非法的报文段和拒绝连接请求
- SYN: 同步序号,用于建立连接过程,在连接请求中 SYN=1和ACK=0 表示该数据段没有使用捎带的确认域,而连接应答捎带一个确认域即SYN=1和ACK=1
- FIN: finish标志,用于释放连接,finish=1表示发送方已经没有数据发送了即关闭本方的数据流。
Window: 用来告知发送段接收端的缓存大小,以此控制发送端发送数据的速率,因而达到流量控制。
Checksum: 检验和指的是奇偶校验,此校验和是对整个的TCP报文段包括TCP头 部和TCP数据以16位进行计算所得,由发送端计算和存储,并由接收端进行验证。
Urgent Pointer: 只有当TCP Flags 中的URG=1时才有效,指出本报文段中的紧急数据的字节数。
TCP Options: 可选项,其长度可变,定义一些其他的可选参数。
UDP报文头部
Source Port: 源端口是一个可选字段,它表示发送方进程的端口号,接收方可以使用该字段向发送方发送信息;
Destination Port: 目的端口是数据报接收方的端口号,它只在目标的 IP 地址下才有意义;
Length: 长度是协议头和数据报中数据长度的总和,表示整个数据报的大小;
Checksum: 校验码使用 IP 首部、UDP 首部和数据报中的数据进行计算,接收方可以通过校验码验证数据的准确性,发现传输过程中出现的问题;
UDP的特点
- 面向非连接
- 不维护连接状态,支持同时向多个客户端传输相同的消息
- 数据包报头只有8个字节,相较于TCP的20个字节信息,额外开销较小
- 吞吐量只受限于数据生成速率、传输速率以及机器性能影响
- 尽最大努力交付,不保证可靠交付,不需要维持复杂的链接状态表格
- 面向报文,不对应用程序提交的报文信息进行拆分或者合并
注解
UDP是一个非连接的传输协议,传输之前源端与终端不建立连接,当它想传输时就简单的抓取来自应用层数据,并尽可能快的把它扔到网络上,在发送端UDP传送的速度仅仅受应用程序生成数据的速度、计算机的能力、和传输带宽的限制。在接收端UDP把每个消息段放到队列中,应用程序每次从消息队列中读取一个消息段。
由于UDP的连接为非连接状态,每次就不需要维护连接状态、包括收发状态等,因此一台服务器可同时向多个客户机传输相同的消息。
吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽 、源端与终端主机性能的限制。
UDP是面向报文的,发送端的UDP对应用程序交下来的报文在添加首部后就向下交付给IP层,既不拆分也不合并,而是保留报文的边界,因此应用程序需要选择合适的报文大小,可以看出UDP将绝大多数控制交给上层去解决。
DNS为什么使用UDP
实际上,DNS 不仅使用了 UDP 协议,也使用了 TCP 协议 。DNS 查询在刚设计时主要使用 UDP 协议进行通信,而 TCP 协议也是在 DNS 的演进和发展中被加入到规范的:
- DNS 在设计之初就在区域传输中引入了 TCP 协议,在查询中使用 UDP 协议;
- 当 DNS 超过了 512 字节的限制,我们第一次在 DNS 协议中明确了『当 DNS 查询被截断时,应该使用 TCP 协议进行重试』这一规范;
- 随后引入的 EDNS 机制允许我们使用 UDP 最多传输 4096 字节的数据,但是由于 MTU 的限制导致的数据分片以及丢失,使得这一特性不够可靠;
- 在最近的几年,我们重新规定了 DNS 应该同时支持 UDP 和 TCP 协议,TCP 协议也不再只是重试时的选择;
DNS 查询选择 UDP 或者 TCP 两种不同协议时的主要原因:
UDP 协议
- DNS 查询的数据包较小、机制简单;
- UDP 协议的额外开销小、有着更好的性能表现;
TCP 协议
- DNS 查询由于 DNSSEC 和 IPv6 的引入迅速膨胀,导致 DNS 响应经常超过 MTU 造成数据的分片和丢失,我们需要依靠更加可靠的 TCP 协议完成数据的传输;
- 随着 DNS 查询中包含的数据不断增加,TCP 协议头以及三次握手带来的额外开销比例逐渐降低,不再是占据总传输数据大小的主要部分;
无论是选择 UDP 还是 TCP,最核心的矛盾就在于需要传输的数据包大小,如果数据包小到一定程度,UDP 协议绝对最佳的选择,但是当数据包逐渐增大直到突破 512 字节以及 MTU 1500 字节的限制时,我们也只能选择使用更可靠的 TCP 协议来传输 DNS 查询和相应。
TCP与UDP的区别
面向连接与无连接:TCP面向连接而UDP面向无连接,TCP有三次握手的过程,UDP适合消息的多播发布即从单个点向多个点传输信息;
可靠性:TCP比较可靠,利用握手确认和重传机制来提供可靠性保证,而UDP可能会丢失,不能保证报文段是否被接收;
有序性:TCP利用序列号保证了报文段的顺序交付,到达可能无序,但TCP最终会排序,而UDP不具备有序性;
传输形式:TCP面向字节流;UDP面向报文段;
速度:TCP速度慢,因为要创建连接,保证消息的可靠性和有序性,相较于UDP需要做额外的很多事情;UDP则更适合对速度敏感的应用,比如在线视频媒体,电话广播、多人在线游戏等;
量级:TCP属于重量级的,UDP属于轻量级的,主要体现在报文头部;
TCP三次握手
TCP连接即:在一个应用进程与另外一个应用进程发送数据之前,这两个进程必须先”相互握手”,即它们必须相互发送某些预备的报文段,以建立确保数据传输的参数。在双方握手建立TCP连接之后,将会在两个应用之间建立一个全双工的通信,全双工的通信将会占用两个计算机之间的通信线路,直到它被一方或者双方关闭为止。
所谓三次握手(Three-way Handshake),是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的顺序号和确认号并交换 TCP信息。三次握手的过程如下:
- 第一次握手:建立连接时,服务器属于
LISTEN监听状态。客户端发送SYN包[SYN = 1; seq = x]到服务器,并进入SYN_SENT同步已发送状态,等待服务器确认。 - 第二次握手:服务器收到SYN包即客户端的连接请求后,将发送ACK确认回应,同时自己也将发送一个SYN建立连接请求,即SYN+ACK包
[SYN = 1; ACK = 1; seq = y; ack = x + 1],此时服务器进入SYN_RECV同步已收到状态。 - 第三次握手:客户端收到服务器的 SYN+ACK包,做为回应需要向服务器发送确认包即ACK包
[ACK = 1; seq = x + 1; ack = y + 1],在该包发送并接收完毕后,客户端与服务器都进入ESTABLISHED建立连接状态,至此连接建立成功。
注解
最开始在客户端与服务端首次进行通信,都处于CLOSED状态,假设客户端主动打开,服务器被动打开,那么服务器进程会首先创建传输控制块TCB,时刻准备接收其他客户进程的连接请求,此时服务器便进入LISTEN监听状态。
第一次握手中SYN = 1 为TCP Flags 中的同步序号标志位,表示要与服务器建立连接请求。在连接请求中 SYN=1和ACK=0 表示该数据段没有使用捎带的确认域,而连接应答捎带一个确认域即SYN=1和ACK=1,也就是说在收到对方的连接请求后如果同意连接都会向对方发送ACK = 1 进行确认反馈。这也就是在第二次与第三次的握手中都会有ACK= 1的原因。
第一次握手中的seq = x 是因为Sequence Number会初始化一个序号x,(x可以是任意的正整数值),在第二次握手中服务器的报文中 ack = x + 1 即对seq = x的回应,表示期待下一次收到来自客户端的报文序号为 x + 1;之所以加1是因为前两次握手并不能传输数据,每次发包都会消耗掉一个序列号。第二次握手服务器回应客户端发送报文段同样会初始化一个 Sequence Number即seq = y。
为什么需要三次握手才能建立连接?两次握手不行嘛?
三次握手的主要目的是1、确认自己和对方的发送和接收都是正常的,从而保证了双方能够进行可靠通信。同时2、初始化Sequence Number即上面的x与y。通过初始化的这个序号来保证在应用层接收到数据不会乱序,即TCP会根据这个序号来拼接数据。因此在第二次握手服务器发送给客户端初始化序号后,客户端还需要回发确认报文给服务器来告知其已经接收到并且连接正常。
如果改为了两次握手,在两次握手的设定下,服务器端在成功接受客户端的连接请求SYN后,向客户端发出ACK确定报文时,如果因为网络原因客户端没有接收到,则会一直等待服务器端的ACK报文,而服务器端则认为连接成功建立了,便开始向客户端发送数据。但是客户端因为没有收到服务器端的ACK报文,所以并不知道服务器的顺序号seq,则会认为连接未成功建立,忽略服务器发出的任何数据。如此客户端一直等待服务器端的ACK报文,而服务器端因为客户端一直没有接收数据,而不断地重复发送数据,从而造成死锁。
针对TCP连接的安全问题:SYN洪泛攻击
SYN攻击属于DOS攻击的一种,它利用TCP协议缺陷,通过发送大量的半连接请求,耗费CPU和内存资源。SYN攻击除了能影响主机外,还可以危害路由器、防火墙等网络系统,事实上SYN攻击并不管目标是什么系统,只要这些系统打开TCP服务就可以实施。
SYN攻击的原理
在三次握手过程中,服务器发送SYN-ACK(确认收到客户端请求的连接)之后,收到客户端的ACK(第三个包)之前的TCP连接称为半连接(half-open connect).此时服务器处于SYN_RECV(等待客户端相应)状态,如果接收到客户端的ACK,则TCP连接成功,如果未接受到,则会重发请求直至成功。SYN攻击就是由’’攻击客户端” 在短时间内伪造大量不存在的IP地址,向服务器不断地发送SYN包,服务器回复确认包,并等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,影响了正常的SYN,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。
防范
主要有两大类,一类是通过防火墙、路由器等过滤网关防护,另一类是通过加固TCP/IP协议栈防范.但必须清楚的是,SYN攻击不能完全被阻止,我们所做的是尽可能的减轻SYN攻击的危害,除非将TCP协议重新设计。
- SYN Cookies:该方案原理和 HTTP Cookies 技术类似,服务端通过特定的算法将半开连接信息编码成序列号或者时间戳,用作服务端给客户端的消息编号,随 SYN-ACK 消息一同返回给连接发起方,这样在连接建立完成前服务端不保存任何信息,直到发送方发送 ACK 确认报文并且服务端成功验证编码信息后,服务端才开始分配传输资源。若请求方是攻击者,则不会向服务端会 ACK 消息,由于未成功建立连接,因此服务端并没有花费任何额外的开销。
如果已经建立了连接,但是客户端突然出现故障了怎么办?
保活机制keep-alive
- 向对方发送保活探测报文如果未收到响应报文则继续发送
- 尝试次数达到保活探测数仍未收到响应则中断连接
TCP设有保活机制,在一段时间我们称为报活时间,即KeepAliveTime ,在这段时间内,连接处于非活动状态,开启保活功能的一端将向对方发送一个保活的探测报文,如果发送端没有收到响应报文,那么经过一个已经提前配置好的保活时间间隔将会继续发送保活探测报文,直到发送探测报文的次数达到保活探测数,这时对方主机将会被确认为不可达,连接也将被终止。
TCP四次挥手
“挥手”是为了终止连接,即断开一个TCP连接时,需要客户端和服务器总共发出4个包以确定连接的断开。TCP四次挥手的过程如下:
- 第一次挥手:刚开始客户端与服务器都处于
ESTABLISHED连接状态,假设客户端主动关闭,服务器被动关闭。客户端首先发送连接释放报文[FIN = 1; seq =m],并停止发送数据,主动关闭TCP连接,进入FIN_WAIT_1终止等待1状态,等待服务器的确认。 - 第二次挥手:服务器收到连接释放报文后即发出 确认报文
[ACK = 1; seq = n; ack = m + 1],并进入CLOSE_WAIT关闭等待状态,此时TCP处于半关闭状态,客户端到服务器的连接释放。同时客户端收到服务器的确认后进入FIN_WAIT_2终止等待2状态,并等待服务器发出的连接释放报文。 - 第三次挥手:当服务器的数据传输完毕后,服务器发出 连接释放报文
[FIN = 1; ACK = 1; seq = p; ack = m + 1],并进入LAST_ACK最后确认状态,等待客户端的最后确认。 - 第四次挥手:客户端在收到服务器的连接释放报文段后,做为响应发出 确认报文
[ACK = 1; seq = m + 1; ack = p + 1], 并进入TIME_WAIT时间等待状态。服务器在收到来自客户端的确认报文段后便进入到CLOSED连接断开状态,此时TCP未释放掉,客户端还需要经过时间等待计时器设置的时间2MSL后,才进入CLOSED连接断开状态,至此四次挥手结束TCP连接终止。
注解
第一次挥手的连接释放报文 [FIN = 1; seq =m],其中FIN = 1表示TCP Flags 中的finish=1表示发送方已经没有数据发送了即关闭本方的数据流,seq = m 其中m的值实际为ESTABLISHED连接状态下,客户端向服务器发送的最后一个数据报中最后一个数据的序号加1,而第二次挥手的确认报文中seq = n,n的值也同样是ESTABLISHED连接状态下服务器向客户端发送的最后一个数据报中最后一个数据的序号加1。TCP规定即使FIN报文段不携带任何数据,也要消耗掉一个序号。
在服务器处于CLOSE_WAIT关闭等待状态时,TCP服务器将通知高层的应用进程客户端要释放与服务器的连接了,这时候会处于半关闭状态,即客户端已经没有要发送 的数据了,但是服务器要发送数据客户端还是可以接收的,并且该状态将持续一段时间。
第三次挥手的连接释放报文 [FIN = 1; ACK = 1; seq = p; ack = m + 1]中,seq = p的p值表示在服务器处于CLOSE_WAIT关闭等待状态时向客户端发送的最后一个数据报中的最后一个数据的序号加1。
第四次挥手后客户端进入到TIME_WAIT时间等待状态,此时TCP连接还没有释放,必须经过2*MSL时间后,客户端撤销相应的TCB(保护程序)后,才能进入CLOSED连接断开状态。MSL即最长报文寿命,RFC793定义了MSL的值为2min,而Linux设置成了 30s,服务器在收到客户端的确认号后便立即进入CLOSED连接断开状态。可以看出,服务器结束TCP连接的时间要比客户端早一些。
为什么要四次挥手?
因为TCP是全双工的,所以在释放TCP连接时发送方和接收方都需要FIN报文和ACK报文, 当主动方在数据传送结束后发出连接释放的通知,由于被动方可能还有必要的数据要处理,所以会先返回 ACK 确认收到报文。当被动方也没有数据再发送的时候,则发出连接释放通知,对方确认后才完全关闭TCP连接。
TIME_WAIT 为什么是 2MSL
- 确保有足够的时间让对方收到ACK包
- 避免新旧连接混淆
TIME_WAIT时间等待状态是用来确保有足够的时间让对端收到ACK包。如果被动关闭的那一方(服务器)没有收到ACK,就会触发被动端重发FIN报文。假如客户端发送完了ACK包就立即断开,当最后一条报文发出后丢失了,那么服务器端就不会接收到这一报文,每隔一段时间,服务器端会再次发出FIN报文,此时如果客户端已经断开了,那么就无法响应服务器的二次请求,这样服务器会继续发出FIN报文,客户端就会用 RST 包来响应服务端,这将会使得对方认为是有错误发生,因此不会正常关闭连接。所以需要设置一个时间段,如果在这个时间段内接收到了服务器端的再次请求,则代表客户端发出的ACK报文没有接收成功。反之,则代表服务器端成功接收响应报文,客户端比才进入CLOSED连接断开状态,此次连接成功关闭。之所以是2*MSL 是客户端发出ACK报文到服务器端的最大时间 + 服务器没有接收到ACK报文再次回发FIN的最大时间 刚好等于2*MSL。
同时为了避免新旧连接混淆,如果客户端在发出ACK报文段后就进入CLOSED连接断开状态,此时服务端相应的端口并没有关闭,若客户端在相同的端口立即建立新的连接,则有可能接收到上一次连接中残留的数据包,可能会导致不可预料的异常出现。
服务器出现大量TIME_WAIT 状态会导致什么问题?如何解决?
大量连接处于TIME_WAIT的原因
我们考虑高并发短连接的业务场景,在高并发短连接的 TCP 服务器上,当服务器处理完请求后主动请求关闭连接,这样服务器上会有大量的连接处于 TIME_WAIT 状态,服务器维护每一个连接需要一个 socket,也就是每个连接会占用一个文件描述符,而文件描述符的使用是有上限的,如果持续高并发,会导致一些正常的 连接失败。
无论是客户端还是服务器主动关闭连接,从本质上来说,在高并发场景下主要关心的就是服务端的资源占用问题,而这也是采用 TCP 传输协议必须要面对的问题,其问题解决的出发点也是如何处理好服务质量和资源消耗之间的关系。
解决方案
服务器可以设置 SO_REUSEADDR 套接字选项来通知内核,如果端口被占用,但 TCP 连接位于 TIME_WAIT 状态时可以重用端口。如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时 SO_REUSEADDR 选项就可以避免 TIME-WAIT 状态。也可以采用长连接的方式减少 TCP 的连接与断开,在长连接的业务中往往不需要考虑 TIME-WAIT 状态,但其实在长连接的业务中并发量一般不会太高。
服务器出现大量CLOSE_WAIT状态的原因? 如何解决?
客户端关闭socket连接,服务器忙于读写没有及时关闭连接
检查代码,特别是释放资源的代码;检查配置,特别是处理请求的线程配置
在客户端发出连接释放报文之后并没有及时的收到来自服务器的ACK确认,即服务器没有检测到或者忘记要关闭连接,于是资源一直被服务器占用着,出现这种情况通常是程序中有bug,通常是某些连接没有及时释放导致的,或者某些配置如线程池中的线程数配置不合理,此时需要结合实际的业务去排查。
TCP的快速打开(TFO)
TCP Fast Open是对TCP的一种简化握手手续的拓展,用于提高两端点间连接的打开速度,在TCP的三次握手过程中就传输实际有用的数据,以此来优化三次握手提高效率。
请求Fast Open Cookie
- 首先客户端发送SYN给服务端,该数据包包含Fast Open选项,且该选项的Cookie为空,这表明客户端请求Fast Open Cookie;
- 支持TCP Fast Open的服务器生成Cookie,并将其置于SYN-ACK数据包中的Fast Open选项以发回客户端;
- 客户端收到SYN-ACK后,缓存Fast Open选项中的Cookie
实现TCP Fast Open
- 客户端发送Cookie+SYN+HTTP请求,该数据包包含数据以及此前记录的Cookie;
- 如果Cookie有效,服务器将在SYN-ACK数据包中对SYN和数据进行确认,服务器随后将数据递送至相应的应用程序,并返回HTTP响应,否则服务器将丢弃SYN数据包中包含的数据,且其随后发出的SYN-ACK数据包将仅确认SYN的对应序列号;
- 客户端回发ACK确认
注解
TFO 的优势并不在与首轮三次握手,而在于后面的握手,在拿到客户端的 Cookie 并验证通过以后,可以直接返回 HTTP 响应,充分利用了1 个RTT(Round-Trip Time,往返时延)的时间提前进行数据传输。
TCP定时器
TCP中有七种计时器
建立连接定时器:该定时器是在建立 TCP 连接的时候使用的,在 TCP 三次握手的过程中,发送方发送 SYN 时,会启动一个定时器(默认为 3 秒),若 SYN 包丢失了,那么 3 秒以后会重新发送 SYN 包,直到达到重传次数。
重传定时器:该计时器主要用于 TCP 超时重传机制中,当TCP 发送报文段时,就会创建特定报文的重传计时器,并可能出现两种情况:
- 若在计时器截止之前发送方收到了接收方的 ACK 报文,则撤销该计时器;
- 若计时器截止时间内并没有收到接收方的 ACK 报文,则发送方重传报文,并将计时器复位;
坚持计时器:我们知道 TCP 通过让接受方指明希望从发送方接收的数据字节数(窗口大小)来进行流量控制,当接收端的接收窗口满时,接收端会告诉发送端此时窗口已满,请停止发送数据。此时发送端和接收端的窗口大小均为0,直到窗口变为非0时,接收端将发送一个 确认 ACK 告诉发送端可以再次发送数据,但是该报文有可能在传输时丢失。若该 ACK 报文丢失,则双方可能会一直等待下去,为了避免这种死锁情况的发生,发送方使用一个坚持定时器来周期性地向接收方发送探测报文段,以查看接收方窗口是否变大。
延迟应答计时器:延迟应答也被称为捎带 ACK,这个定时器是在延迟应答的时候使用的,为了提高网络传输的效率,当服务器接收到客户端的数据后,不是立即回 ACK 给客户端,而是等一段时间,这样如果服务端有数据需要发送给客户端的话,就可以把数据和 ACK 一起发送给客户端了。
保活定时器:该定时器是在建立 TCP 连接时指定 SO_KEEPLIVE 时才会生效,当发送方和接收方长时间没有进行数据交互时,该定时器可以用于确定对端是否还活着。
FIN_WAIT_2 定时器:当主动请求关闭的一方发送 FIN 报文给接收端并且收到其对 FIN 的确认 ACK后进入 FIN_WAIT_2状态。如果这个时候因为网络突然断掉、被动关闭的一端宕机等原因,导致请求方没有收到接收方发来的 FIN,主动关闭的一方会一直等待。该定时器的作用就是为了避免这种情况的发生。当该定时器超时的时候,请求关闭方将不再等待,直接释放连接。
TIME_WAIT 定时器:我们知道在 TCP 四次挥手中,发送方在最后一次挥手之后会进入 TIME_WAIT 状态,不直接进入 CLOSE 状态的主要原因是被动关闭方万一在超时时间内没有收到最后一个 ACK,则会重发最后的 FIN,2 MSL(报文段最大生存时间)等待时间保证了重发的 FIN 会被主动关闭的一段收到且重新发送最后一个 ACK 。还有一个原因是在这 2 MSL 的时间段内任何迟到的报文段会被接收方丢弃,从而防止老的 TCP 连接的包在新的 TCP 连接里面出现。
TCP超时重传
发送方在发送一次数据后就开启一个定时器,在一定时间内如果没有得到发送数据包的 ACK 报文,那么就重新发送数据,在达到一定次数还没有成功的话就放弃重传并发送一个复位信号。
RTT与RTO
RTT:发送一个数据包到收到对应的ACK,所花费的时间
RTO:重传时间间隔,TCP在发送一个数据包之后会启动一个重传定时器,RTO即为这个定时器的重传时间。
由于RTO是本次发送当前数据包所预估的超时时间,所以RTO需要一个好的算法来统计,以更好的预测本次的超时时间,RTO不是固定写死的配置,而是经过RTT计算出来的。有了RTT才能计算RTO。基于RTO才有了确认重传机制,才能支撑起TCP的滑动窗口。
TCP流量控制
管理点对点的数据传输速率,防止快发送方压倒慢接收方
TCP 中利用可变长的滑动窗口机制实现对发送方的流量控制。对于发送端和接收端而言,TCP 需要把发送的数据放到发送缓存区, 将接收的数据放到接收缓存区。TCP报文头中的 Window 用于接收方告诉发送方自己还有多少缓冲区可以接收数据。发送方根据接收方的处理能力来发送数据,不会导致接收方接收不过来,如果对方的接收缓存区满了,就不能再继续发送了,这便是流量控制。
TCP滑动窗口
TCP使用滑动窗口做流量控制和乱序重排
- 提供TCP的可靠性
- 提供TCP的流量控制
TCP将数据分割成段进行发送,出于效率和传输速度的考虑,我们不可能一段一段的去发送,等到上一报文段被接收到并回发确认后再发送下一段,这样效率是非常低的。我们要想实现对数据的批量转发,TCP必须要解决可靠传输和包乱序问题,所以TCP需要了解网络中实际的数据处理带宽或数据处理速度,这样才不会引起网络拥塞而丢包。
滑动窗口数据的计算过程
相关参数
LastByteAcked: 指向收到的连续最大的ACK的位置,即从左端算起连续已经被接收端的程序收到并发送ACK回值确认的seq;
LastByteSent: 指向已经发送的最后一个字节的位置,该位置为已经发送但是还没有收到来自接收端的ACK回值确认;
LastByteWritten: 指向上层应用已写完的最后一个字节的位置,即发送端已经准备好的需要发送的最新的数据段;
LastByteRead: 指向接收端上层应用已经读完的最后一个字节的位置,即已经收到处理并做ACK回值确认的最后一个字节处;
NextByteExpected: 指向已经收到的连续最大的seq 位置,即已经收到但没有做回值确认;
LastByteRcvd: 指向已收到的最后一个字节的位置;与NextByteExpected 间的空缺是因为一些seq因为差错而没有准确到达;
MaxRcvBuffer: 接收方能够接收的最大数据量 即接收方的缓存区大小
AdvertisedWindow: 接收方缓冲区还能处理的数据量,该值即为TCP报文头中的 Window 中所返回的值;
EffectiveWindow: 发送方窗口内还允许发送的数据的大小,即除去已发送没确认的还能发送的数据大小;
相关公式
AdvertisedWindow=MaxRcvBuffer- (LastByteRcvd-LastByteRead)EffectiveWindow=AdvertisedWindow- (LastByteSent-LastByteAcked)
注解
LastByteRcvd - LastByteRead表示接收端缓存区已经接受到的数据或者 为还没有接受到的数据预留出来的一些空间(如空缺处),当前这些空间以及该占据了一些缓存,我们用接收方能够允许的最大缓存数减去已占据的缓存就得了接收方还能够接收的数据量。
LastByteSent - LastByteAcked 表示发送端已经发送出去的数据段,这些数据段很可能已经到达接收方或者还在传输的过程中,所以我们要保证LastByteSent - LastByteAcked <= AdvertisedWindow 即 已发送且待确认的数据量要小于接收方的Window大小。自然发送方窗口还可以发送的数据量的大小 EffectiveWindow 即为它们的差值。AdvertisedWindow - (LastByteSent - LastByteAcked);
TCP滑动窗口的基本原理
TCP 滑动窗口分为两种: 发送窗口和接收窗口。
TCP会话的发送窗口
对于TCP会话的发送方,任何时候在其发送缓存区中的数据都可以分为以下四种:
Sent and Acknowledged:已经发送并且得到端的回应的;Sent BUT NOT Yet Acknowledged:已经发送但还没有接收到端的回应;Not Sent Recipient Ready to Receive:未发送但对端允许发送;Not Sent Recipient NOT Ready to Receive:未发送但是达到了Window的大小,对端不允许发送的数据;
发送窗口:Sent BUT NOT Yet Acknowledged + Not Sent Recipient Ready to Receive
TCP会话的接收窗口
对于TCP的接收方来讲,在任一时刻它的接收缓存区中数据会存在三种状态:
Received and Acknowledged:已接收并且已发送回值的状态;
Not Yet Received,Transmitter Permitted To Send:未接收但是可以接收(准备接收);
Not Yet Received,Transmitter May Not Sent: 未接收但是不能接收(达到了窗口的阈值);
接收窗口:由于ACK由TCP栈回复,默认没有应用延迟的,不存在已接收但未回复ACK的状态。其中Not Yet Received,Transmitter Permitted To Send未接收并且准备接收的部分为接收方的接收窗口。
原理总结
TCP最基本的传输可靠性来源于 确认重传机制,TCP滑动窗口的可靠性也是建立在确认重传的基础上的。发送窗口只有收到了接收方对于本段发送窗口内字节的ACK确认才会移动发送窗口的左边界,接收方只有在前面所有数据段都确认的情况下才会移动左边界。当在前面还有字节未接收但收到后面字节的情况下(即出现了空位),窗口也不会移动并不会对后续字节进行确认,以此确保发送端会对这些数据进行重传。同时滑动窗口的大小可以根据一定的策略动态的调整,应用会根据自身处理能力的变化通过对本端TCP接收窗口的大小的控制,来实现对发送端进行流量控制。
如果接收方滑动窗口满了,发送方怎么办?
基于 TCP 流量控制中的滑动窗口协议,我们知道接收方返回给发送方的 ACK 包中会包含自己的接收窗口大小,若接收窗口已满,此时接收方返回给发送方的接收窗口大小为 0,此时发送方会等待接收方发送的窗口大小直到变为非 0 为止,然而,接收方回应的 ACK 包是存在丢失的可能的,为了防止双方一直等待而出现死锁情况,此时就需要坚持计时器来辅助发送方周期性地向接收方查询,以便发现窗口是否变大,当发现窗口大小变为非零时,发送方便继续发送数据。
TCP拥塞控制
网络拥塞: 太多的源端口想以过高的速率发送数据,导致网络来不及处理,网络性能变差。如路由器缓存溢出,导致丢包等现象。若出现拥塞而不进行控制,整个网络的吞吐量将随输入负荷的增大而下降。
判断网络拥塞的依据 : 没有按时收到应当到达的确认报文,即发生了重传。
拥塞崩溃 : “ 拥塞崩溃 ” 是为何要 拥塞控制 或者 限制有效交流 的原因。拥塞崩溃通常发生在 进入网络的通讯 超出 离开网络的带宽 这样的网络瓶颈下。本地局域网和广域网之间的连接点是常见的瓶颈。
拥塞控制
调解通讯网络的通讯入口,为了避免由超额认购导致的拥塞崩塌;
控制发送者发送数据进入网络的速率,使数据流保持在引发网络崩溃的水平之下;
TCP 流量控制 VS TCP 拥塞控制
拥塞控制 防止发送者发送太快太多使网络无法应对,关注的是两节点之间的连接
流量控制 防止发送者发送太快太多使接收者无法应对,关注的是另一个节点
拥塞控制算法
slow-start慢开始congestion avoidance拥塞避免fast retransmit快重传fast recovery快恢复
相关概念
cwnd:拥塞窗口,其值取决于网络的拥塞程度,动态变化;rwnd: 接收窗口,接收端的流量控制;swnd: 发送窗口->swnd= min[rwnd,cwnd];ssthresh: 慢开始门限状态变量;
慢启动 (cwnd < ssthresh)
刚开始进入传输数据的时候,你是不知道现在的网路到底是稳定还是拥堵的,如果做的太激进,发包太急,那么疯狂丢包,造成雪崩式的网络灾难。拥塞控制首先就是要采用一种保守的慢启动算法来慢慢地适应整个网路。
- 三次握手后,双发宣告自己的接收窗口
rwnd大小; - 初始化 拥塞窗口
cwnd大小,一般为一个MSS,初始化ssthresh慢启动门限 ,大多数TCP实现中ssthresh= 65536Byte; cwnd呈指数增长,当收到ACK确认后 拥塞窗口就翻倍;- 当
cwnd>=ssthresh时,切换为拥塞避免算法;
拥塞避免 (cwnd >ssthresh)
当cwnd到达了慢启动阈值,就不能无止境的翻倍下去了,否则迟早要出事的。这时候就需要拥塞避免算法来收着涨了。
- 线性增长,每个
RTT,cwnd增加1*MSS - 只要不出现ACK异常就继续上述操作
问题引入:前面两个机制都是在没有检测到拥塞的情况下,那么当出现拥赛,cwnd 该如何调整呢?
- 重传计时器超时间 Timeout ,判断网络出现了拥塞;
- 将
ssthresh的值更新为发生拥塞时的cwnd的一半 - 拥塞窗口
cwnd重置成 1MSS - 整完后重新进行慢开始算法
- 当
cwnd的值又增加到ssthresh时,再次更新到 拥塞避免算法
上述两种算法为1988年提出的TCP Tahoe版本,但是有时个别报文段会在网络中丢失,但是实际网络并未发生拥塞,这将导致发送发超时重传,并误以为网络发生了拥塞,所以发送方错误地启动了慢开始算法,并把拥塞窗口重置为最小值 1,因而降低了传输效率。所以为了优化由于错误判断拥塞而导致的效率低下的问题,1990年又增加了两种算法,即TCP Reno版本 快重传和快恢复的。
TCP的拥塞控制流程如下:
快重传
- 要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认;
- 即使收到失序的报文段也要立即发出对已收到报文段的重复确认;
- 当发送方一旦收到3个连续的重复确认,就将相应报文段立即重传,而不是等该报文段的重传计时器超时再重传。
快恢复
- 收到了3个连续的重复确认后,知道只是出现了丢失个别报文段并不是出现网络拥塞;
- 并不启动慢开始算法,而是执行快速恢复算法;
- 将
ssthresh与cwnd都调整为当前窗口cwnd的一半 ->ssthresh=cwnd/2;cwnd=cwnd/ 2; - 也有快速实现是把拥塞窗口
cwnd的值再增大些,调整为cwnd=ssthresh+ 3 - 接着执行拥塞避免算法,线性增大
cwnd
注解
cwnd = ssthresh + 3 => 既然发送方收到了3个连续的重复确认,就表明3个数据已经离开了网络,这3个报文段不再消耗网络资源而是停留在接收方的接收缓冲区中,可见现在网络中不再是堆积了报文段,反而是减少了3个报文段,因此可以适当把拥塞窗口扩大些。
快速恢复和拥塞避免的主要区别是在遇到报文丢失的场景时,拥塞避免会直接将cwnd置为1,而快速恢复则是先执行快速重传,然后将cwnd减半。
参考资料