# TCP
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议
# 特点
TCP 是一种面向广域网的通信协议,目的是在跨越多个网络通信时,为两个通信端点之间提供一条具有下列特点的通信方式:
- 基于流的方式
- 面向连接
- 可靠通信方式
- 在网络状况不佳的时候尽量降低系统由于重传带来的带宽开销
- 通信连接维护是面向通信的两个端点的,而不考虑中间网段和节点
# 协议规定
- 数据分片:在发送端对用户数据进行分片,在接收端进行重组,由 TCP 确定分片的大小并控制分片和重组;
- 到达确认:接收端接收到分片数据时,根据分片数据序号向发送端发送一个确认;
- 滑动窗口:TCP 使用的流量控制协议是可变大小的滑动窗口协议。TCP 连接每一方的接收缓冲空间大小都固定,接收端只允许另一端发送接收端缓冲区所能接纳的数据,TCP 在滑动窗口的基础上提供流量控制,防止较快主机致使较慢主机的缓冲区溢出;
- 超时重发:发送方在发送分片时启动超时定时器,如果在定时器超时之后没有收到相应的确认,重发分片;
- 失序处理:作为 IP 数据报来传输的 TCP 分片到达时可能会失序,TCP 将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层;
- 重复处理:作为 IP 数据报来传输的 TCP 分片会发生重复,TCP 的接收端必须丢弃重复的数据;
- 数据校验:TCP 将保持它首部和数据的检验和,这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到分片的检验和有差错,TCP 将丢弃这个分片,并不确认收到此报文段导致对端超时并重发。
# 报文首部
TCP 的固定包头为 20 个字节,每一行 32bit(4Byte),5行。
# 第一行:源端口与目的端口
根据 OSI 七层模型我们知道 TCP 属于传输层,IP 属于网络层,最终数据都在物理层上传输,其中从传输层到网络层会在发送数据前会封装 IP 首部,表示要传给那台 IP 地址的机器。然后我们需要知道端口的概念:端口可以认为是 设备与外界通讯交流的出口。端口可分为虚拟端口和物理端口。我们这里指的源端口与目的端口是指虚拟端口的网络端口。根据计算机网络的知识我们知道端口对多有 65535 个。也就是 16 个二进制位。因此源端口与目的端口各 16 bit,占了一行。
# 第二行:序列号
在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
# 第三行:确认应答号
指下一次期望收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收,用来解决不丢包的问题。如果确认号为N,那么表示发送该报文的机器已经接收到了N-1及其以前的数据。
# 第四行:数据偏移,保留,URG,ACK,PSH,RST,SYN,窗口
数据偏移:指 TCP 数据起始位置距离 TCP 报文起始位置的距离,一般情况下为报文首部长度 20 字节,但 TCP 首部有一个可选长度要注意。
保留:很明显就是现在没用,留给以后使用
控制位:
- ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
- RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
- SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
- FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
- URG:紧急标志位,表示的是此报文段中有紧急数据,将紧急数据排在普通数据的前面;当接受端收到此报文后后必须先处理紧急数据,而后再处理普通数据。
- PSH:催促标志位,当发送端将 PSH 置为1时,TCP会立即创建一个报文并发送。接受端收到 PSH 为1的报文后就立即将接受缓冲区内数据向上交付给应用程序,而不是等待缓冲区满后再交付。
- 窗口:占 2 个字节,窗口指的是发送本报文段的一方的接收窗口,不是自己的发送窗口,告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量。窗口值作为接受方让发送方设置其发送窗口的依据,一般用来平衡双方的数据传输速率(带宽)不一致问题。 用于 TCP 流量控制,告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
# 第五行:校验和紧急指针
- 校验和:由发送端填充,接收端对 TCP 报文段执行 CRC 算法以检验 TCP 报文段在传输过程中是否损坏,检验的范围包括头部、数据两部分,是 TCP 可靠传输的一个重要保障。占 2 字节。校验和字段检验的范围包括首部和数据这两部分。
- 紧急指针:一个正的偏移量。它和序列号字段的值相加表示最后一个紧急数据的下一个字节的序列号,用于发送端向接收端发送紧急数据。占 2 个字节,紧急指针仅在 URG=1 时才有意义,它指出本报文段中的紧急数据的字节数。当所有紧急数据处理完毕时,TCP 就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为 0 时也可发送紧急数据。
# 优点
- TCP 协议可以保证接收端毫无差错地接收到发送端发出的字节流,为应用程序提供可靠的通信服务。
- 对可靠性要求高的通信系统往往使用 TCP 传输数据。
- 但是由于各种数据校验机制,导致工作效率较 UDP 来说较低
# TCP 三次握手
# 目的
对每次发送的数据量进行跟踪与协商,确保数据段的发送和接收同步,根据所接收到的数据量而确认数据发送、接收完毕后何时撤消联系,并建立虚连接。
为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
# SYN
同步序列编号(Synchronize Sequence Numbers)。是 TCP/IP 建立连接时使用的握手信号。是 TCP 连接的第一个包,非常小的一种数据包。SYN 攻击包括大量此类的包,由于这些包看上去来自实际不存在的站点,因此无法有效进行处理。每个机器的欺骗包都要花几秒钟进行尝试方可放弃提供正常响应。
# ACK
确认字符 (Acknowledge character)。在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。在 TCP/IP 协议中,如果接收方成功的接收到数据,那么会回复一个 ACK 数据。通常 ACK 信号有自己固定的格式,长度大小,由接收方回复给发送方。
# seq
是数据包本身的序列号
# ack
是对收到的数据包的确认,值是下次希望接收的数据包的序列号。
# seq 和 ack
在第一次消息发送中,A 随机选自取一个序列号作为自己的初始序号(seq=x)发送给 B;第二次消息 B 使用 ack 对 A 的数据包进行确认,因为已经收到了序列号为 x 的数据包,正准备接收序列号为 x+1 的包,所以 ack=x+1,同时 B 告诉 A 自己的初始序列号,就是seq=y;第三度条消息 A 告诉 B 收到了 B 的确认消息并准备建立连接,A 自己此条消息的序列号是 x+1,所以 seq=x+1,而 ack=y+1 是表示知 A 正准备接收 B 序列号为 y+1 的数据包。
# 具体步骤
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN©。此时客户端处于 SYN_SEND 状态。
首部的同步位 SYN=1,初始序号 seq=x,SYN=1 的报文段不能携带数据,但要消耗掉一个序号。
第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN+1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。
在确认报文段中 SYN=1,ACK=1,确认号 ack=x+1,初始序号 seq=y。
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN+1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
确认报文段 ACK=1,确认号 ack=y+1,序号 seq=x+1(初始为 seq=x,第二个报文段所以要 +1),ACK 报文段可以携带数据,不携带数据则不消耗序号。
发送第一个SYN的一端将执行主动打开(active open),接收这个 SYN 并发回下一个 SYN 的另一端执行被动打开(passive open)。
在socket编程中,客户端执行connect()时,将触发三次握手。
三次握手
第一次握手:客户端向服务端发送连接请求报文段。该报文段的头部中 SYN=1,ACK=0,同时选择一个初始序号 seq=x。请求发送后,客户端便进入SYN-SENT 状态。
第二次握手:服务端收到连接请求报文段后,如果同意连接,会发送一个应答:SYN=1,ACK=1,seq=y,ack=x+1。发送完应答后服务端进入SYN-RCVD 状态。
第三次握手:客户端收到服务端连接同意的应答后,还会向服务端发送一个确认报文段,表示:服务端发来的连接同意应答已经成功收到。该报文段的头部为:ACK=1,seq=x+1,ack=y+1。该报文发送完毕后,客户端和服务器端都进入 ESTABLISHED 状态,完成 TCP 三次握手。
# 重要概念
# 半连接队列
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。
当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
# Backlog 参数
表示内核为相应套接字排队的最大连接个数。SYN-ACK 重传次数服务器发送完 SYN-ACK 包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同。
# 半连接存活时间
半连接队列的条目存活的最长时间,也即服务器从收到SYN包到确认这个报文无效的最长时间,该时间值是所有重传请求包的最长等待时间总和。有时我们也称半连接存活时间为 Timeout 时间、SYN_RECV 存活时间。
# 常见问题
# Q1. 为什么要握三次,而不是两次或四次?
首先非常明确的是两次握手是最基本的。三次握手是为了防止已失效的连接请求报文段突然又传送到了服务端,造成服务端资源的浪费。不是四次握手:因为三次握手已经能说明握手时的通信是正常的,四次握手、五次握手就显得浪费了。
第一次握手:客户端发送网络包,服务端收到了。
这样,服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端发包,客户端收到了。
这样,客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
第三次握手:客户端发包,服务端收到了。
这样,服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。
因此,需要三次握手才能确认双方的接收与发送能力是否正常。
第一次握手,客户端发了个连接请求消息到服务端,服务端收到信息后知道自己与客户端是可以连接成功的,但此时客户端并不知道服务端是否已经接收到了它的请求。
第二次握手,服务端接收到消息后的应答,客户端得到服务端的反馈后,才确定自己与服务端是可以连接上的, 客户端只有确定了自己能与服务端连接上才能开始发数据。所以两次握手肯定是最基本的。
第三次握手,是为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误。譬如发起请求遇到类似这样的情况:客户端发出去的第一个连接请求由于某些原因在网络节点中滞留了导致延迟,直到连接释放的某个时间点才到达服务端,这是一个早已失效的报文,但是此时服务端仍然认为这是客户端的建立连接请求第一次握手,于是服务端回应了客户端,第二次握手。如果只有两次握手,那么到这里,连接就建立了,但是此时客户端并没有任何数据要发送,而服务端还在傻傻的等候佳音,造成很大的资源浪费。所以需要第三次握手,只有客户端再次回应一下,就可以避免这种情况。
❓ 试想如果是用两次握手,则会出现下面这种情况:
如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
# Q2. 如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP 还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为 2 小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔 75 秒钟发送一次。若一连发送 10个 探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
# TCP 四次挥手
TCP连接是双向的,在四次挥手中,前两次挥手用于断开一个方向的连接,后两次挥手用于断开另一方向的连接。
# 具体步骤
刚开始双方都处于ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:
第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。
在socket编程中,任何一方执行close()操作即可产生挥手操作。
四次挥手
第一次挥手:客户端数据发送完成,则它向服务端发送连接释放请求。该请求只有报文头,头中携带的主要参数为:FIN=1,seq=u。此时,客户端将进入 FIN-WAIT-1 状态。TCP 规定,FIN 报文段即使不携带数据,也要消耗一个序号。
第二次挥手:服务器收到客户端连接释放报文,通知相应的高层应用进程,告诉它客户端向服务器这个方向的连接已经释放了。此时服务端进入了CLOSE-WAIT(关闭等待)状态,并向客户端发出连接释放的应答,其报文头包含:ACK=1,ack=u+1,seq=v。客户端收到该应答后,进入FIN-WAIT-2状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
第二次挥手完成后,客户端到服务端方向的连接已经释放,服务端不会再接收客户端的数据,客户端也没有数据要发送了。但服务端到客户端方向的连接仍然存在,服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
第三次挥手:服务端将最后的数据发送完毕后,就向客户端发送连接释放报文,其报文头包含:FIN=1,ack=u+1,由于在 CLOS-WAIT 状态,服务端很可能又发送了一些数据,假定此时的序列号为 seq=w,此时,服务器就进入了 LAST-ACK(最后确认)状态,等待客户端的确认。
第四次挥手:客户端收到服务器的连接释放报文后,向服务端发出确认应答,报文头:ACK=1,ack=w+1,seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。该状态会持续 2MSL(最长报文段寿命)时间,这个期间 TCP 连接还未释放,若该时间段内没有服务端的重发请求的话,客户端就进入 CLOSED 状态,服务端只要收到了客户端发出的确认,立即进入 CLOSED 状态。就结束了这次的 TCP 连接。可以看到,服务器结束 TCP 连接的时间要比客户端早一些。
# 常见问题
# Q1. 为什么连接的时是三次握手,关闭的时却是四次握手或者说四次挥手?
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。
# Q2. 为什么客户端最后还要等待 2MSL?
MSL 最大分段寿命(Maximum Segment Lifetime),是一个 TCP 分段可以存在于互联网系统中的最大时间。TCP 允许不同的实现可以设置不同的MSL值。
第一,保证客户端发送的最后一个 ACK 报文能够到达服务器。因为这个 ACK 报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK 报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个 2MSL 时间段内收到这个重传的报文,接着给出回应报文,并且会重启 2MSL 计时器。
第二,防止类似与三次握手中提到了的已经失效的连接请求报文段出现在本连接中。客户端发送完最后一个确认报文后,在这个 2MSL 时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文
# TCP 与 UDP
- TCP 面向连接(TCP 三次握手),UDP 是无连接的,即发送数据之前不需要建立连接。
- 每一条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一和多对多的交互通信。
- TCP 面向字节流,实际上是 TCP 把数据看成一连串无结构的字节流;UDP是面向报文的。
- TCP 有序,UDP 无序;消息在传输过程中可能会乱序,后发送的消息可能会先到达,TCP 会对其进行重排序,UDP不会。
- TCP 有流量控制(拥塞控制),UDP 没有。
- TCP 传输速率慢,由于传输要进行三次握手,以及会进行拥塞控制等。
- TCP 应用场景:效率要求相对低,但对准确性要求相对高的场景。因为传输中需要对数据确认、重发、排序等操作,相比之下效率没有UDP 高。举几个例子:文件传输(准确高要求高、但是速度可以相对慢)、接受邮件、远程登录。UDP 应用场景:效率要求相对高,对准确性要求相对低的场景。举几个例子:QQ 聊天、在线视频、网络语音电话(即时通讯,速度要求高,但是出现偶尔断续不是太大问题,并且此处完全不可以使用重发机制)、广播通信(广播、多播)。
流量控制
TCP 利用滑动窗口机制在 TCP 连接上实现对发送方的流量控制, 如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。
拥塞控制
防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提:网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机、路由器,以及与降低网络传输性能有关的所有因素。当出现网络抖动时,TCP 会自觉降低发送速度,他会努力维护次序,但 UDP 依然保持速度不变。TCP 提供可靠的服务。也就是说,通过 TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付。
TCP | UDP | |
---|---|---|
连接 | 面向连接 | 无连接 |
连接方式 | 点到点 | 支持一对一,一对多,多对一和多对多 |
模式 | 流模式(TCP) | 数据报模式(UDP) |
头部大小 | 20字节 | 8字节 |
有序性 | 有序 | 无序 |
传输速率控制 | 流量控制(拥塞控制) | 无 |
可靠性 | 可靠 | 不可靠 |
传输速率 | 慢 | 快 |
占用系统资源 | 高 | 低 |
应用场景 | 效率要求相对低,但对准确性要求相对高的场景。 | 效率要求相对高,对准确性要求相对低的场景 |
TCP 字节流和 UDP 数据报区别
两者的区别在于 TCP 接收的是一堆数据,而每次取多少由主机决定。而 UDP 发的是数据报,客户发送多少就接收多少。
拥有这些区别的原因是由于 TCP 和 UDP 的特性不同而决定的。TCP 是面向连接的,也就是说,在连接持续的过程中,socket中收到的数据都是由同一台主机发出的,因此,知道保证数据是有序的到达就行了,至于每次读取多少数据自己看着办。 而UDP是无连接的协议,也就是说,只要知道接收端的IP和端口,且网络是可达的,任何主机都可以向接收端发送数据。这时候,如果一次能读取超过一个报文的数据,则会乱套。比如,主机 A 向发送了报文 P1,主机 B 发送了报文 P2,如果能够读取超过一个报文的数据,那么就会将 P1 和 P2 的数据合并在了一起,这样的数据是没有意义的。
参考资料:
面试官,不要再问我三次握手和四次挥手_猿人谷 三次握手四次挥手-CSDN博客 (opens new window)
TCP字节流和UDP数据报区别 - hoohack - 博客园 (cnblogs.com) (opens new window)
【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(上)-腾讯云开发者社区-腾讯云 (tencent.com) (opens new window)