【计算机网络】传输层TCP协议

时间:2023-02-15 08:52:02


认识TCP协议

传输控制协议 (TCP,Transmission Control Protocol)是一种面向连接、可靠的、基于字节流的传输层通信协议,由IETF的RFC 792 定
义。

TCP协议旨在适应支持多网络应用的分层协议层次结构。 连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠TCP提供可靠的通信服务。TCP假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。 原则上,TCP应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。

TCP协议是当前应用最广泛的传输层协议,其根本原因在于它具有可靠性,基于TCP应用层协议有HTTP、HTTPS、SSH、Telnet、FTP、SMTP等。

TCP协议最大的特点就是面向字节流,

TCP协议的格式

【计算机网络】传输层TCP协议

字段的含义

  • 源端口和目的端口:源端口号表示数据从那个进程来,目的端口号表示数据到哪里去。
  • 32位序号与32位确认序号:32位序号表示报文中各个数据的编号,32位确认序号表示接收方对已经接收数据的确认,二者保证了TCP协议通信的可靠性。
  • 4位TCP头部长度:表示TCP报头的长度,以4字节为单位,一般为20字节,最长为60字节(20字节的固定首部加上加上选项的大小)。
  • 6位保留字段:TCP报头中暂未被使用的6个比特位。
  • 6位标志位:区分报文类型。
  • 16位窗口大小:保证TCP通信的可靠性和效率的字段。
  • 16位校验和:包括TCP报头和TCP数据两部分,由发送端填充,采用CRC校验。如果接收方校验失败,则认为接收到的数据有误。
  • 16位紧急指针:标识紧急数据在报文中的偏移量,需要配合标志位中的URG标识使用。
  • 选项字段:TCP报头中允许额外携带的字段,最大为40字节。

序号与确认号

32位序号:

如果双方在进行数据通信时,只有收到了上一次发送数据的响应才能发下一个数据,那么此时双方的通信过程就是串行的,效率可想而知。

因此双方在进行网络通信时,允许一方向另一方连续发送多个报文数据,只要保证发送的每个报文都有对应的响应消息就行了,此时也就能保证这些报文被对方收到了。

【计算机网络】传输层TCP协议

但在连续发送多个报文时,由于各个报文在进行网络传输时选择的路径可能是不一样的,因此这些报文到达对端主机的先后顺序也就可能和发送报文的顺序是不同的。但报文有序也是可靠性的一种,因此TCP报头中的32位序号的作用之一实际就是用来保证报文的有序性的。

TCP将发送出去的每个字节数据都进行了编号,这个编号叫做序列号。

比如现在发送端要发送3000字节的数据,如果发送端每次发送1000字节,那么就需要用三个TCP报文来发送这3000字节的数据。此时这三个TCP报文当中的32位序号填的就是发送数据中首个字节的序列号,因此分别填的是1、1001和2001。

【计算机网络】传输层TCP协议

当接收方收到这三个报文之后,就会根据其中的序列号进行排序,排序完成后再放入TCP缓冲区中,这样就能保证发送出去的和接收到的数据顺序保持一致。

32位确认号:

TCP报头当中的32位确认序号是告诉对方,当前已经收到了哪些数据,并且数据下一次应该从哪里开始发送

以上文的例子为例,当主机B收到主机A发送过来的32位序号为1的报文时,由于该报文当中包含1000字节的数据,因此主机B已经收到序列号为1-1000的字节数据,于是主机B发给主机A的响应数据的报头当中的32位确认序号的值就会填成1001。

  • 一方面是告诉主机A,序列号在1001之前的字节数据我已经收到了。
  • 另一方面是告诉主机A,下次向我发送数据时应该从序列号为1001的字节数据开始进行发送。
    【计算机网络】传输层TCP协议

如果报文在传输过程中丢失了,例如最终只要序号为1和2001的报文被主机B收到,那么当B对报文进行排序的时候,就会发现少了1001 ~ 2000 之间的数据,那么此时向主机A响应的报文中的32位确认序号的值就是1001,告诉主机A下次再次发送1001开始的数据。
【计算机网络】传输层TCP协议
【注意】
如果此时主机B在给主机A响应时,其32位确认序号不能填3001,因为1001-2000是在3001之前的,如果直接给主机A响应3001,就说明序列号在3001之前的字节数据全都收到了。因此主机B只能给主机A响应1001,当主机A收到该确认序号后就会判定序号为1001的报文丢包了,此时主机A就可以选择进行数据重传。

TCP报头中有了序号和确认号的机制,一定程度上保证了数据传输的完整性,同时也保证了TCP传输的可靠性。

六个标志位

TCP报文的种类多种多样,除了正常通信时发送的普通报文,还有建立连接时发送的请求建立连接的报文,以及断开连接时发送的断开连接的报文等等。收到不同种类的报文时需要对应执行动作,因此需要用标志位进行区分不同的报文类型。这六个标志位都只占用一个比特位,为0表示假,为1表示真。

URG:

双方在进行网络通信的时候,由于TCP是保证数据按序到达的,即便发送端将要发送的数据分成了若干个TCP报文进行发送,最终到达接收端时这些数据也都是有序的,因为TCP可以通过序号来对这些TCP报文进行顺序重排,最终就能保证数据到达对端接收缓冲区中时是有序的。

虽然TCP的有序到达是我们想要的目的,并且接收方的对端上层也是从接收缓冲区中按顺序读取的,但是有时候发送方也会发送紧急数据,那么就要让接收方的对端上层也要紧急读取该数据,因此就需要使用的URG标志位。
【计算机网络】传输层TCP协议

  • 当URG标志位设置位1时,需要使用TCP报头中的16位紧急指针找到紧急数据,因此一般情况下不会使用到报头中的紧急指针。
  • 16位紧急指针表示了紧急数据在报文中的偏移量。

ACK:

  • 报文中的ACK标志设置为1,表示该报文可以对接收到的报文进行确认。
  • 一般除了第一个请求连接的报文没有设置ACK外,其余报文基本上都设置了ACK,因为携带了ACK的报文需要对接收到的报文进行确认。

PSH:

当PSH标志位设置为1时,会提示接收端应用程序立刻从TCP缓冲区读取数据,并交付给上层应用。

一般我们会认为当使用read从缓冲区读取数据时,如果缓冲区中有数据,那么这些数据就会被返回,如果没有数据就会阻塞式的等待write向缓冲区中写入数据再进行读取。

其实这种说法并不准确,因为在缓冲区中都有应该水位线的概念,例如下图:
【计算机网络】传输层TCP协议

  • 当缓冲区存储的数据没有达到水位线的时候,read就会进行阻塞等待,只要超过水位线后才会进行读取。因为如果缓冲区中有一点数据就进行读取的话会导致频繁的调用read,势必会造成效率的低下。

  • 当报文当中的PSH被设置为1时,实际就是在告知对方操作系统,尽快将接收缓冲区当中的数据交付给上层,尽管接收缓冲区当中的数据还没到达所指定的水位线。

RST:

  • 报文当中的RST被设置为1,表示需要让对方重新建立连接。
  • 在通信双方在连接未建立好的情况下,一方向另一方发数据,此时另一方发送的响应报文当中的RST标志位就会被置1,表示要求对方重新建立连接。
  • 在双方建立好连接进行正常通信时,如果通信中途发现之前建立好的连接出现了异常也会要求重新建立连接。

SYN:

  • 报文当中的SYN被设置为1,表明该报文是一个连接建立的请求报文。
  • 只有在连接建立阶段,SYN才被设置,正常通信时SYN不会被设置。

FIN:

  • 报文当中的FIN被设置为1,表明该报文是一个连接断开的请求报文。
  • 只有在断开连接阶段,FIN才被设置,正常通信时FIN不会被设置。

窗口大小

当发送端要将数据发送给对端时,本质是把自己发送缓冲区当中的数据发送到对端的接收缓冲区当中。但缓冲区是有大小的,如果接收端处理数据的速度小于发送端发送数据的速度,那么总有一个时刻接收端的接收缓冲区会被填满,这时发送端再发送数据过来就会造成数据丢包,进而引起丢包重传等一系列的连锁反应。

因此TCP报头中就引入的16位窗口大小来加以控制。这个16位窗口中填充的就是自身缓冲区剩余空间的大小,发送给对方后就能让对方知道自己缓冲区的存储能力,从而控制传输的速率。

  • 窗口大小字段越大,说明接收端接收数据的能力越强,此时发送端可以提高发送数据的速度。
  • 窗口大小字段越小,说明接收端接收数据的能力越弱,此时发送端可以减小发送数据的速度。
  • 如果窗口大小的值为0,说明接收端接收缓冲区已经使用完了,此时发送端就不应该再发送数据了。

确认应答(ACK)机制

确认应答机制是保证TCP通信可靠性的机制之一,它是由32位序号和32位确认序号来保证的。
【计算机网络】传输层TCP协议
TCP是面向字节流的,它会为每个字节的数据都进行了编号,即序列号:
【计算机网络】传输层TCP协议

每一个ACK都带有对应的确认序列号,意思是告诉发送者,当前接收方已经收到了哪些数据,下一次发送方应该发送哪些数据。

超时重传机制

主机A发送数据给主机B之后,可能因为网络拥堵等原因,导致数据无法到达主机B。如果主机A在一个特定时间间隔内没有收到主机B发来的确认应答,就会进行数据重传,这就是超时重传机制。
【计算机网络】传输层TCP协议

如果主机A也没有收到来自主机B的确认应答,也可能是因为ACK丢失了。

【计算机网络】传输层TCP协议

当ACK发生丢包时,由于存在超时重传机制,主机B就会收到重复的数据,此时主机B就会意识到自己发送的确认应答有可能发生了丢包,导致主机A没有收到,因此就会重新发送确认应答,并且主机B会根据其前面接收到的数据的序号,丢弃掉重复的数据。

那么超时重传的时间该如何设定呢?

最理想的情况下,找到一个最小的时间,保证确认应答一定能在这个时间内返回。但是这个时间的长短,随着网络环境的不同,而存在差异。如果超时时间设的太长,会影响整体的重传效率;如果超时时间设的太短,有可能会频繁发送重复的包。

TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间:

  • Linux中(Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
  • 如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传。
  • 如果仍然得不到应答,等待 4*500ms 进行重传,依次类推,以指数形式递增。
  • 当累计到一定的重传次数,TCP就会认为网络或者对端主机出现异常,最后强制关闭连接。

连接管理机制

在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接,以下是TCP连接到断开的全部过程:
【计算机网络】传输层TCP协议

三次握手

客户端与服务端建立连接的过程称为三次握手。
【计算机网络】传输层TCP协议

当客户端要与服务端之间相互通信时,首先就需要建立连接,此时客户端会主动向服务器发送建立连接的请求,然后双方实现三次握手。

第一次握手:客户的给服务端发送SYN报文,初始序列号为x,并且需要消耗应该序号。此时客户端进入SYN_SENT状态。当SYN=1,而ACK=0时,表明这是一个请求连接的报文。注意SYN=1时的报文不能携带数据,因此第一次握手和第二次握手都不会携带数据。因为如果携带数据的话,假如有人想要攻击服务器,只需要每次第一次握手时在SYN报文中携带大量的数据,导致服务器花费大量的缓冲区,造成服务器的崩溃。

通过第一次握手服务器可以知道:客户端发送数据的能力,以及以及自己的接受能力都处于正常状态。

第二次握手:服务端接收到了来的客户端的SYN报文后,对其进行确认,并且会把自己的SYN响应给对方,此时标志位ACK=1就是对第一次握手的报文进行的确认,并且ack = seq + 1,即为x + 1,初始序列号为y。此时服务器进入SYN_RECV状态。

通过第二次握手客户端能够知道:服务端的接收和发送能力正常,客户端自己的发送和接收能力也正常。但是服务器不知道客户端的接收能力是否正常,因此就需要进行第三次握手。

第三次握手客户端收到了服务端的SYN+ACK数据包,此时客户端就会认为自己和服务端都愿意进行连接,因此客户端会进入ESTABLISHED状态,并且会给服务端发送一个ACK报文,表示自己收到了来自服务端的SYN+ACK响应。此时确认序号ack = y + 1,发送给服务器的第二个报文段的seq = x + 1。

通过第三次握手服务端就能够知道:客户端的接收能力以及自己的发送能力都正常,此时双方建立连接成功,可以进行网络通信了。

为什么是三次握手,而不是其他的握手次数呢?

假如是两次握手,举个打电话的例子:

  • 假如在半夜你有一个重要的事情要告诉你的对象,于是打了一个电话,打通了过后,你向电话里面说:“喂,你听得到我说话吗?我有事跟你说。"
  • 对方:“你有毛病啊,大半夜打什么电话,有什么屁快放。”,此时却从电话里面再也听不到你的消息,非常生气,以为你大半夜搞恶作剧呢,便把你拉黑了。

其实可能是你对象麦克风或者网络的原因,导致自己听不到对方说话,引起误会。客户端和服务端之间也是如此,如果只有两次握手的话,服务端就不知道客户端的接收能力以及自己发送数据的能力,所以还需要进行第三次握手才行。

为什么要三次握手,难道四次或更多次不可以吗?

因为三次握手是安全的,并且效率是最高的。如果客户端发送请求时出现了丢包情况,因为自己没发送,又重新传了一遍,然而等数据传输完成后客户端和服务端都释放了连接,重发的传输的数据在释放连接前给服务器传了过去,但第一次传输的数据假如由于网络原因滞留的时间长了,在释放连接后到达了服务端,这个时候服务端就会误以为客户端又发出了一次新的请求,服务端确认了客户端第一次发出的报文段并同意建立了新的连接,发送报文给客户端,此时服务端会一直等待客户端的答复,而客户端此时正处于释放连接状态,所以导致白白浪费了资源。

半连接队列和全连接队列

  • 半连接队列(syn queue)
    客户端发送SYN包,服务端收到后回复SYN+ACK后,服务端进入SYN_RCVD状态,此时双方还没有完全建立连接,这个时候的socket会放到半连接队列。
  • 全连接队列(accept queue)
    当服务端收到客户端的ACK后,socket会从半连接队列移出到全连接队列。当调用accpet函数的时候,会从全连接队列的头部返回可用socket给用户进程。全连接队列中存放的是已完成TCP三次握手的过程,等待被处理的连接,在客户端及服务端的状态均为 ESTABLISHED

四次挥手

当双方结束通信,断开连接的过程称为四次挥手。四次挥手,顾名思义就是客户端和服务端四个步骤的释放连接,断开连接需要发送四个包,别名连接终止协议。因为TCP连接是全双工的,因此每个方向的连接都必须分别断开。断开的基本原则是,双方完成了数据传输的任务之后,由一方先发起一个FIN的报文来终止这个方向上的连接。收到一个FIN只意味着这一方向上没有数据流动,但另一方向上还可以发送数据,因此需要对方再发送一个FIN报文断开另一个方向上的连接。例如:
【计算机网络】传输层TCP协议
双方再断开连接前都处于ESTABLISHED状态,

第一次挥手:客户端主动断开连接,向服务端发送一个带FIN的报文。其中包含将FIN标志位置为1,序列号seq = u。发送完毕之后客户端进入FIN_WAIT_1状态,即关闭自己到服务端的连接,等待客户端的回应,但是此时可以接收服务端发来的报文。

第二次挥手:服务端收到FIN后,知道了客户端想要与自己断开连接,因此进入CLOSE_WAIT状态,并且向客户端响应一个带ACK的确认报文,此时客户端收到该报文就知道服务端接收到了自己的断开连接请求,但是此时服务端还可能会发送数据。

第三次挥手:此时服务端要与客户端断开自己这个方向上的连接,向客户端发送一个FIN报文,然后服务端进入LAST_ACK状态,等待来自客户端最后的确认。

第四次挥手:客户端收到 FIN 报文之后,同样会发送一个 ACK 报文作为应答,此时客户端进入TIME_WAIT状态,TIME_WAIT状态是为了等待足够的时间以确保服务器能够接收到到连接中断请求的确认。

注意:

  • 此时由服务端到客户端的 TCP 连接并未释放掉,客户端需要经过时间等待计时器设置的时间 2MSL(一个报文的来回时间) 后才会进入
    CLOSED状态,服务端收到 ACK 报文之后,就关闭连接了,处于 CLOSED 状态。
  • 这样做的目的是确保服务端收到自己的 ACK 报文如果服务端在规定时间内没有收到客户端发来的 ACK 报文的话,服务端会重新发送 FIN 报文给客户端,客户端再次收到 FIN 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK报文给服务端

**为什么是等待2MSL? **

  • 防⽌客户端最后⼀次发给服务器的确认在⽹络中丢失以⾄于客户端关闭,⽽服务端并未关闭,导致资源的浪费。
  • 等待最⼤的2MSL可以让本次连接的所有的⽹络包在链路上都完全传输完毕,以防造成不必要的⼲扰。

为什么客户端需要TIME_WAIT状态?

  • 假设最终的ACK丢失,服务端将重发FIN,客户端必须维护TCP状态信息以便可以重发最终的ACK,否则服务端认为传输中发生错误,导致会发送RST报文进行重新连接。
  • TCP实现必须可靠地终止连接的两个方向(全双工关闭),客户端必须进入TIME_WAIT 状态,因为客户端可能面临重发最终ACK的情形。

同样的全双工,为什么握手是三次,挥手是四次?

  • 因为握手的时候并没有数据传输,所以服务端的 SYN 和 ACK 报文可以一起发送,但是挥手的时候有数据在传输,所以 ACK 和 FIN 报文不能同时发送,需要分两步,所以会比握手多一步。

为什么三次挥手不可行?

  • 因为服务端在接收到FIN,往往不会立即返回FIN,必须等到服务端所有的报文都发送完毕了,才能发FIN。因此先发一个ACK表示已经收到客户端的FIN,延迟一段时间才发FIN,这就造成了四次挥手。
  • 如果是三次挥手会造成: 如果将服务端的两次挥手合为一次,等于说服务端将ACK和FIN的发送合并为一次挥手,这个时候长时间的延迟可能会导致客户端误以为FIN没有到达客户端,从而让客户端不断的重发FIN。
  • 所有只能第二次握手先发送ACK确认接收到了客户端的数据,等服务器发送完了数据,再发送FIN包进行第三次挥手。

滑动窗口

每发送一个数据就发出一个确认应答,直到发送端收到ACK报文再发送下一个数据段,这样的做最大的缺点就是性能比较差,尤其是在数据往返时间较长的时候表现得尤为明显。
【计算机网络】传输层TCP协议
既然这样一发一收的方式性能较低,那么就可以考虑一次发送多条数据,就可以大大的提高数据传输的性能。实际上就是是将多个数据段的等待时间重叠在一起,这样TCP就引入了滑动窗口的机制。

滑动窗口的概念:
滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。

滑动窗口原理:
滑动窗口协议的基本原理就是在任意时刻,发送方都维持了一个连续的允许发送的帧的序号,称为发送窗口;同时,接收方也维持了一个连续的允许接收的帧的序号,称为接收窗口。发送窗口和接收窗口的序号的上下界不一定要一样,甚至大小也可以不同。不同的滑动窗口协议窗口大小一般不同。发送方窗口内的序列号代表了那些已经被发送,但是还没有被确认的帧,或者是那些可以被发送的帧。

例如下图:
【计算机网络】传输层TCP协议

  • 滑动窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000个字节(四个段)。
  • 发送前四个段的时候,不需要等待任何ACK,直接发送即可。
  • 当主机A收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据。后续数据的发送依次类推。
  • 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答。只有确认应答过的数据,才能从缓冲区删掉。
  • 滑动窗口越大,则网络的吞吐率就越高。

主机B向主机A发送请求序列号为2001的报文,主机A收到确认报文后,滑动窗口向右移动至2001的位置。
【计算机网络】传输层TCP协议

如果出现了丢包的情况,窗口又该如何滑动呢?

情况一:主机A发送的数据包已经被主机B接收了,但是发给主机A的确认报文ACK丢失了。
【计算机网络】传输层TCP协议
这种情况下,部分ACK丢失并不会影响数据包的正常传输,因为主机A接收到的对后续数据包的确认,同时也能对前面发出的数据包进行确认

情况二:主机A发送的数据包部分直接丢失了。

【计算机网络】传输层TCP协议

  • 在这种情况下,主机A发送的在1001 ~ 2000的数据包丢失,但该滑动窗口中的其他数据包还是继续正常发送,只是由于主机B没有收到1001 ~ 2000的数据包,因此会一直向主机A发送带有1001确认号的响应,表示没有接收的1001 ~ 2000的数据包。
  • 如果主机A连续3次收到同样的带有确认号1001这样的确认应答,就会对 1001 ~ 2000 的数据包进行重传。
  • 当主机B收到了 1001 ~ 2000 的数据包后,就会向主机A响应确认号为7001的确认报文,因为2001 ~ 7000 的数据主机B已经收到了,被放在了其操作系统内核的接收缓冲区中。

这种机制被称为 “高速重发机制”,也叫做 “快重传”。

流量控制

接收端处理数据的速度是有限的,如果发送端发送得太快,就会导致接收端的缓冲区迅速被写满,如果这个时候发送端继续发送数据就会造成丢包,继而引起丢包重传等一系列连锁反应。

因此TCP支持根据接收端的处理能力来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)
流量控制的基本原理如下:

  • 接收端将自己可以接受数据的缓冲区大小写入TCP首部的 “窗口大小” 字段中,通过发送的ACK确认报文响应给发送端。
  • 窗口大小字段越大,说明网络的吞吐量和接收端的缓冲区大小越大。
  • 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置为一个更小的值响应给发送端。
  • 当发送端知道了这个窗口大小之后,就会减慢自己的发送速度。
  • 如果接收端的缓冲区满了,就会将窗口大小设置为0,此时发送端不再给接收端发送数据,但是需要定期发送一个窗口探测的数据段,让接
    收端把自己的窗口大小告诉发送端。

【计算机网络】传输层TCP协议

接收端如何把窗口大小告诉发送端呢?回忆前面的TCP首部中,有一个16位窗口字段,存放的就是窗口大小信息。那么问题来了,16位数字最大表示65535,那么TCP窗口最大就是65535字节么?
实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移 M 位。

拥塞控制

虽然TCP有了滑动窗口机制,能够高效可靠的发送大量数据,但是如果在刚刚传输数据的阶段就发送大量的数据,仍然可能会引起问题。因为当今世界上时时刻刻都存在着大量的计算机在进行网络通信,可能当前的网络状态就比较拥堵了,如果此时在不清楚网络状况的情况下就贸然发送大量的数据,还是可能会造成丢包等情况。

为了解决这个问题,TCP就引入了 “慢启动” 机制,即开始先发送少量的数据,去摸清当前网络的拥堵状况,再决定按照多大的速度发送数据。

此时便为 “慢机制” 引入了一个拥塞窗口的概念,其基本原理如下:

【计算机网络】传输层TCP协议

  • 在发送开始时,设置拥塞窗口的大小为1,发送端每次收到一个ACK确认应答,拥塞窗口大小就增长一倍。
  • 每次发送数据包的时候,发送端就会对自己的拥塞窗口和接收端反馈的滑动窗口的大小进行比较,取二者的较小值作为实际的窗口大小。

像上面这样的拥塞窗口大小的增长速度是指数级别的增长,“满启动” 只是指初始传输数据的速度满,但其增长的速度很快。解决方法如下:

  • 为了阻止拥塞窗口增长过快,不能使拥塞窗口大小单纯的只进行加倍
  • 因此引进了一个叫做 “慢启动” 的阈值。
  • 当拥塞窗口超过这个阈值的时候,步骤按照指数方式进行增长,而是以线性的方式增长。

如图:
【计算机网络】传输层TCP协议

  • 当TCP开始启动的时候,“慢启动” 的阈值等于窗口的最大值。
  • 每次进行超时重传的时候,“慢启动” 的阈值就会减少为原来的一半,同时拥塞窗口被重置为1。

当数据传输过程中出现少量的丢包,仅仅会触发TCP的超时重传机制,如果出现大量的丢包现象,那么就可以判断为出现了网络拥塞。当TCP通信开始时,网络吞吐量会逐渐上升;当网络出现了拥塞,吞吐量就会立即下降。拥塞控制,归根结底是TCP协议尽快可能的想将数据传输给对方,但是又要避免给网络造成太大压力的折中方案。

延迟应答

如果每次接收端接收到数据就立刻响应ACK应答的话,这个时候返回的滑动窗口大小就会比较小,例如:

  • 假如接收端缓冲区大小为1M,一次性收到的数据大小为500K,如果立即应答,那么返回的滑动窗口大小也就是500K,
  • 但是实际上可能处理端的处理速度很快,10ms之内就将500K的数据从缓冲区中处理完了。在这种情况下,接收端接收数据的能力还远远没有达到自己的极限,即使窗口再大一点,也得处理的过来。
  • 如果接收端延迟一会再做出应答,比如等待200ms后再应答,此时向发送端返回的窗口大小就是1M了,提高了传输数据的效率。

但是我们一定要记住,窗口越大,网络吞吐量就越大,传输的效率就越大,但是是在保证网络不拥塞的基础之上尽量提高传输效率的。因此不少所有的包都可以延迟应答,例如:

  • 存在数量限制:比如每隔n个数据包就应答一次。
  • 存在时间限制:超过最大延迟时间就应答一次。

具体的数量和延迟时间并不统一,随着操作系统的不同也会存在差异,但是一般n取2,最大延迟时间取200ms。
【计算机网络】传输层TCP协议

捎带应答

在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一发一收” 的,意味着客户端给服务器说了 “How are you”,服务器也会给客户端回一个 “Fine, thank you” 。那么这个时候ACK就可以搭顺风车,和服务器回应的 “Fine, thank you” 一起发送给客户端。这就是捎带应答。
【计算机网络】传输层TCP协议

面向字节流

我们都知道TCP传输数据是面向字节流的,比如创建一个TCP的socket,同时会在系统内核中创建一个发送缓冲区和一个接收缓冲区

  • 当调用write时,数据会先写入发送缓冲区中。
  • 如果发送的字节数太长,会被拆分成多个TCP的数据包发出。
  • 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去。
  • 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区。然后应用程序可以调用read从接收缓冲区拿数据。
  • 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区。那么对于这一个连接,既可以读数据,也可以写数据。这个概念叫做 “全双工” 。

由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:

  • 写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节。
  • 读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read 100个字节,也可以一次read一个字节,重复100次。

粘包问题

粘包问题中的 “包” ,是指的应用层的数据包。在TCP的协议头中,没有如同UDP一样的 “报文长度” 这样的字段,但是有一个序号这样的字段。站在传输层的角度,TCP是将一个一个报文传输过来的,按照序号排好序放在缓冲区中。但是站在应用层的角度,看到的只是一串连续的字节数据,那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。

那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界

  • 对于定长的包,保证每次都按固定大小读取即可。
  • 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。例如TCP的头部长度。
  • 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可) 。

对于UDP协议来说,是否也存在 “粘包问题” 呢?

对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是把一个一个把数据交付给应用层,因此就有很明确的数据边界。站在应用层的站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现"半个" 的情况。因此对于UDP而言不会出现 “粘包问题” 。

TCP异常情况

TCP常见的异常情况如下:

  • 进程终止:进程终止会释放文件描述符,仍然可以发送FIN,和正常断开TCP连接没有什么区别。
  • 计算机重启:和进程终止的情况相同。
  • 机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset。即使没有写入操作, TCP自己也内置了一个保活定时器,会定期询问对方是否还在。如果对方不在,也会释放连接。
  • 另外,应用层的某些协议,也有一些这样的检测机制。比如在HTTP长连接中,也会定期检测对方的状态。例如QQ,在QQ断线之后,也会定期尝试重新连接。

总结

为什么TCP协议会这么复杂?因为既要保证其可靠性,同时又尽可能的提高性能,导致实现TCP就变得很困难。
TCP的可靠性得益于:

  • 校验和
  • 序列号(按序到达)
  • 确认应答
  • 超时重传
  • 连接管理
  • 流量控制
  • 拥塞控制

提供性得益于:

  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答

其他:

  • 定时器(超时重传定时器、保活定时器、TIME_WAIT定时器等)