当应用程序希望通过 TCP 与另一个应用程序通信时,它会发送一个通信请求。这个请求必须被送到一个确切的地址。在双方“握手”之后,TCP 将在两个应用程序之间建立一个全双工 (full-duplex) 的通信。

1. 建立连接 [三次握手]

客户端和服务器通过 TCP 发送数据之前,必须先建立连接。建立连接的过程也被称为 TCP 握手,三次握手的目的是建立可靠的通信信道。确认自己与对方的发送与接收机能正常。

  1. 第一次握手

    客户端 TCP 首先向服务器端 TCP 发送连接请求报文段,这时首部中的同步位 SYN = 1,同时选择一个初始随机序号 seq = x。此时,客户端进程进入同步已发送(SYN-SENT)状态

    TCP 规定,SYN 报文段(即 SYN = 1的报文段)不能携带数据,但是要消耗掉一个序号

    6

  2. 第二次握手

    服务器收到连接请求报文段后,如同意连接,则服务器会为该 TCP 连接分配缓存和变量,并向客户端返回确认报文段,在确认报文段中同步位 SYN = 1 和 确认位 ACK = 1,确认号 ack = x + 1,同时也为自己选择一个初始序号 seq = y。这时 TCP 服务器进程进入同步收到(SYN-RCVD)状态

    如果 server 端接到了 client 发的 SYN 后回了 SYN-ACK 后 client 掉线了, server 端没有收到 client 回来的ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server端如果在一定时间内没有收到的TCP会重发SYN-ACK。

    在 Linux 下,默认重试次数为 5 次,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。

  3. 第三次握手

    客户进程在收到服务器进程的确认报文后,客户端为该 TCP 连接分配缓存和变量,并向服务器端返回一个报文段,这个报文段是对服务器确认报文段进行确认,该报文段中 ACK = 1,确认号 ack = y + 1,而自己序号为 x + 1。客户端在发送 ACK 报文段后进入已建立连接(ESTABLISHED)状态,这时 TCP 连接已经建立。当服务器收到客户端的确认后,也进入ESTABLISHED状态。

完成三次握手,随后客户端与服务器之间可以开始传输数据了。



总结

为什么在三次握手的过程中要初始化序列号,为什么要使用随机序号,而不能使用固定的序号?

这样选择序号的目的是为了防止由于网络路由 TCP 报文段可能存在延迟抵达与排序混乱的问题,从而而导致某个连接的一方对它作错误的解释

如果客户服务器双方建立连接使用固定的序号,在经过三次握手后建立了一个TCP连接,在传输数据时有网路问题而导致数据未能到达,这个报文段在网络中停留。之后,由于某些原因(如客户端主机故障)导致这个TCP连接被释放,等到客户端主机恢复后,使用相同的序列号重新建立一个连接时,在之后的某个时间段,如果之前由于网络问题的报文段达到服务器端,那么服务器就可能会收下这个报文段,而这个数据是属于之前的旧连接的数据,所以这就会导致数据乱序的问题。

由于一个TCP连接是被一对端点所表示的,其中包括2个IP地址和2个端口号构成的4元组,因此即便是同一个连接也会出现不同的实例,如果连接由于某个报文段长时间延迟而关闭,然后又以相同的4元组被重新打开,那么可以相信延迟的报文段又会被视为有效据重新进入新连接的数据流中,这就会导致数据乱序问题。

为了避免上述的问题,避免连接实例间的序号重叠可以将风险降至最低

一个 TCP 报文段只有同时具备连接的 4 元组与当前活动窗口的序列号,才会在通信过程中被对方认为是正确的。然而,这也反应了 TCP 连接的脆弱性:如果选择合适的序列号、IP地址和端口号,那么任何人都能伪造一个TCP报文段,从而打断 TCP 的正常连接。所以使用初始化序号的方式(通常随机生成序号)使得序列号变得难猜,或者使用加密来避免利用这种缺点被攻击。

为什么客户端第一次握手不能携带数据而第三次握手可以携带数据?

假如第一次握手可以携带数据的话,如果有人使用伪 TCP 报文段恶意攻击服务器,那么每次都在第一次握手中的 SYN 报文中携带大量的数据,因为它不会理会服务器的发送和接收能力是否正常,不断地给服务器重复发送这样携带大量数据的 SYN 报文,这会导致服务器需要花费大量的时间和内存来接收这些报文数据,这会将导致服务器连接资源和内存消耗殆尽。

之所以第一次握手不能携带数据,其中的一个原因就是避免让服务器受到攻击。而对于第三次握手,此时客户端已经建立了连接,通过前两次已经知道了服务器的接收正常,并且也知道了服务器的接收能力是多少,所以可以携带数据。

为什么连接建立需要三次握手,而不是两次握手?

根据前面描述,在第一次握手,客户端向服务发送建立连接请求,第二次握手,服务器同意建立连接,并向客户端返回一个确认报文,至此客户端已经知道了服务器同意建立连接,为什么客户端还需要对服务器的允许连接报文段进行确认?

三次握手的最主要目的是保证连接是双工的,可靠更多的是通过重传机制来保证的。

为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。

SYN 洪泛攻击以及如何解决 SYN 洪泛攻击

在三次握手的过程中,服务器为了响应一个收到的 SYN 报文段,会分配并初始化连接变量和缓存,然后服务器发送一个SYNACK报文段进行响应,并等待客户端的 ACK 报文段。如果客户不发送 ACK 来完成该三次握手的第三步,最终(通常在一分多钟之后)服务器将终止该半开连接并回收资源。

这种 TCP 连接管理协议的特性就会有这样一个漏洞,攻击者发送大量的 TCP SYN 报文段,而不完成第三次握手的步骤。随着这种 SYN 报文段的不断到来,服务器不断为这些半开连接分配资源,从而导致服务器连接资源被消耗殆尽。这种攻击就是 SYN 洪泛攻击

为了应对这种攻击,现在有一种有效的防御系统,称为 SYN cookie。SYN cookie 的工作方式如下:

  1. 当服务器接收到一个 SYN 报文段时,它并不知道该报文段是来自一个合法的用户,还是这种 SYN 洪泛攻击的一部分。因为服务器不会为该报文段生成一个半开的连接。相反,服务器生成一个初始 TCP 序列号,该序列号是 SYN 报文段的源IP地址和目的IP地址,源端口号和目的端口号以及仅有服务器知道的秘密数的复杂函数(散列函数)。这种精心制作的初始序列号称为为”cookie”。服务器则发送具有这种特殊初始序号的SYNACK报文分组。服务器并不记忆该cookie或任何对应于SYN的其他状态信息。

  2. 如果该客户是合法的,则它将返回一个 ACK 报文段。当服务器收到该 ACK 报文段,需要验证该ACK是与前面发送的某个SYN相对应。由于服务器并不维护有关SYN报文段的记忆,所以服务器通过使用SYNACK报文段中的源和目的IP地址与端口号以及秘密数运行相同的散列函数。如果这个函数的结果(cookie值)加1和在客户的 ACK 报文段中的确认值相同的话,那么服务器就会认为该 ACK 对应于较早的 SYN 报文段,因此它是合法的。服务器则会生成一个套接字的全开连接。

  3. 另一方面,如果客户没有返回一个 ACK 报文段,说明之前的 SYN 报文段是洪泛攻击的一部分,但是它并没有对服务器产生危害,因为服务器没有为它分配任何资源。

2. 释放连接 [四次挥手]

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于 ESTABLISHED 状态,然后客户端主动关闭,服务器被动关闭。由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

  1. 第一次挥手

    客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部 FIN = 1,其序列号为seq = u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入 FIN-WAIT-1 状态。

    TCP 规定,FIN 报文段即使不携带数据,也要消耗一个序号。

  2. 第二次挥手

    服务器收到连接释放报文,发出确认报文,ACK=1,ack number=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了 CLOSE-WAIT (关闭等待)状态。

    TCP 服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT 状态持续的时间。

  3. 第三次挥手

    客户端收到服务器的确认请求后,此时,客户端就进入 FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文。服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN = 1,ack number= u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

  4. 第四次挥手

    客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack number=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了 TIME-WAIT(时间等待)状态。注意此时 TCP 连接还没有释放,必须经过 2MSL(最长报文段寿命)的时间后,等待 2MSL 后依然没有收到回复,则证明 Server 端已正常关闭,客户端撤销相应的 TCB 后,才进入 CLOSED 状态。

3. 应用场景

对实时性要求高和高速传输的场合下使用 UDP;在可靠性要求低,追求效率的情况下使用 UDP;

需要传输大量数据且对可靠性要求高的情况下使用 TCP

注:由于TCP提供可靠交付和有序性的保证,它是最适合需要高可靠并且对传输时间要求不高的应用。UDP 是更适合的应用程序需要快速,高效的传输的应用,如游戏。UDP 是无状态的性质,在服务器端需要对大量客户端产生的少量请求进行应答的应用中是非常有用的。在实践中,TCP 被用于金融领域,如FIX协议是一种基于 TCP 的协议,而 UDP 是大量使用在游戏和娱乐场所。

4. TCP 报文段的首部格式

一个 TCP 报文段分为首部数据部分两部分。

TCP 报文段首部的前 20 个字节是固定的,后面有 4n 字节是根据需要而增加的选项。因此 TCP 首部的最小字节是 20 字节

源端口和目的端口

各占 2 字节,分别写入源端口号和目的端口号,TCP 的分用功能也是通过端口实现的。

序号

占 4 字节,序号范围是[0, 232 - 1],共 232 个序号。首部中的序号字段值则指的是本报文段所发送的数据的第一个字节的序号

序号是可以重用的,当序号增加到 [232-1] 后,下一个序号就又回到了0,所以序号逻辑上可以表示为一个循环数组

例如,若一个报文段的序号字段值是 301,而携带的数据共有 100 字节,这就表明:本报文段的数据第一个字节的序号是 301,最后一个字节的序号是400。如果还有下一个报文段,则其序号字段的的值应为401。

确认号

占 4 字节,是期望收到对方下一个报文段的第一个数据字节的序号

例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而该报文段的数据长度是200字节[序号501~700],这表明B正确收到了A发送的到序号700为止的数据,因此B期望收到A的下一个数据序号是701,TCP是可靠传输,收到数据后需要给发送方回复确认信息,所以B在收到数据后给A发送的确认收到的报文段中就把确认号置为701。

若确认号 = N,则表明:到序号N - 1为止的所有数据都已正确收到。

数据偏移

占 4 位,单位:4B。它指出 TCP 报文段数据起始处距离 TCP 报文段的起始处有多远。这个字段实际上是指出 TCP 报文段的首部长度

保留

占 6 位,保留今后使用

控制位

  • 紧急 URG(URGent)

    仅当 URG = 1,表明后面的紧急指针字段才有效。它表明系统此报文段有紧急数据,应尽快传送,而不要按照原来的排队顺序来传送

  • 确认 ACK(ACKnowledgment)

    仅当 ACK = 1 时,确认号字段才有效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。

  • 推送 PSH(PUSH)

    通常如果 TCP 缓存中字节很少,TCP 会等待积累有足够多的字节后再构成报文段发送出去,当发送方将 PSH 置为 1时,并立即创建一个报文段发送出去,接收方 TCP 收到 PSH = 1 的报文段,就尽快地交付接收应用进程,而不再等到整个缓存都填满在向上交付。

  • 复位 RST(ReSeT)

    当 RST = 1 时,表明 TCP 连接中出现了严重差错,必须释放连接,然后再重新建立传输连接。RST置为1还可以用来拒绝一个非法的报文段或拒绝打开一个连接。RST也可称为重建位或重置位。

  • 同步 SYN(SYNchronization)

    在连接建立时用来同步序号。当SYN = 1而ACK = 0时,表明这是一个连接请求报文段。对方同意建立连接,则应在响应的报文段中使用SYN = 1和ACK = 1.因此,SYN 置为1表示这是一个连接请求或连接接收报文。

  • 终止 FIN(FINis)

    用来释放一个连接。当FIN = 1时,表明此报文段的发送方的数据发送完毕,并要求释放传输连接。

窗口

占2字节,是指发送本报文段的一方的接收窗口。窗口的值表示:从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量。即窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。

例如,A是发送方,B是接收方,B给A发送一个确认接收数据的报文,其确认号701(表示701之前的所有数据都已经正确收到,期望A下一个报文段的第一个数据字节序号为701),窗口字段的值是1000,这就是告诉发送方A:从701号算起,我的接收缓存还可以接收1000个字节数据,在你给我发送数据的时候,你需要考虑一下我的接收能力。

窗口字段明确指出了现在允许对方发送的数据量。窗口值是经常在动态变化

校验和

占2字节。校验和字段校验的范围包括首部数据这两个部分。和UDP用户数据报一样,在计算校验和时,需要在TCP报文段的前面加上12字节的伪首部。伪首部的格式和UDP伪首部的格式一样,只是需要将协议字段改为6,TCP协议号是6。

紧急指针:紧急指针只有在URG= 1时才有意义,它和URG字段配合使用,它指出了报文段中紧急数据的字节数,因此,紧急指针指出了紧急数据的末尾在报文段中的位置

即使窗口的值为0也可以发送紧急数据。

选项和填充:长度可变,最长可达40字节。填充字段是为了使整个TCP首部的长度是4字节的整数倍。

5. TCP 特点

  1. TCP 是面向连接的传输层协议

    应用程序是使用 TCP 协议之前,必须建立 TCP 连接。在传送数据完毕后,必须释放已建立的 TCP 连接。TCP 连接是一条虚连接(逻辑连接),而不是一条真正的物理连接。

  2. 每一条 TCP 连接只能有两个端点,每条TCP连接只能是点对点的(一对一)。

  3. TCP 提供可靠交付的服务。通过 TCP 连接传送的数据,无差错、不丢失、不重复、并且按序达到

  4. TCP 提供全双工通信

  5. TCP 面向字节流

    虽然应用程序和 TCP 的交互是一次一个数据块(大小可以不等),但是TCP把应用程序交下来的数据块看成仅仅一连串的无结构的字节流。