一、TCP基础

1 TCP介绍

TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,它建立在IP层之上,用于解决可靠传输、有序传输等问题。

TCP包又称TCP报文段,报文段分为TCP首部和TCP数据两个部分。TCP首部最小为20B,最大为60B,如图:

其中:

  • 源端口和目的端口字段:各占2B(2^16-1=65535,这是端口号的上限)
  • 序列号字段:占4B,TCP是基于字节流的通信协议,在传输数据流时,要为每个字节编号。比如一个TCP报文的序列号字段为101,总共占100B,则代表这个TCP报文的最后一个字节序号为200,下一个TCP报文的起始序号字段就为201。
  • 确认号字段:占4B,表示期望收到的另一个TCP报文的序列号,ack=seq+1(期望收到的下一个seq)。比如:我收到了一个TCP报文,其seq为301,长度为300B,此时我正确接收了301-600序号的数据,接下来期望收到seq为601的TCP报文,于是我把自己的ack置为601。
  • 数据偏移:占4bit,它表示首部长度,即:TCP数据部分距离TCP整个报文的首部偏移量为多少,数据偏移以4B为单位,最小为(0101)~2~,即5×4B=20B,最大为(1111)~2~,即15×4B=60B。
  • 保留字段:占6bit,暂时没有用处,置0处理。
  • 标志位:占6bit,每个标志占1bit,含义分别如下:
    • URG:紧急位,置1表示紧急位生效,即报文中的数据为紧急数据,应优先传输,需要和紧急指针搭配使用。
    • ACK:确认位,置1表示上面的确认号字段ack生效,若置0则不生效。在TCP连接建立后,报文段中此字段都为1。
    • PSH:推送位,置1表示接收方应该尽快将这个TCP报文交给应用层,而不是等到buffer填满后再上交。
    • RST:复位位,置1表示连接出现错误,需要释放连接并重置连接。
    • SYN:同步位,置1表示这是一个请求连接或者请求接收的TCP报文。
    • FIN:终止位,置1表示此报文的发送方已经将数据发送完毕,并要求断开连接。
  • 窗口字段:占2B,它表示还能接收多少数据(单位B)。假设我方发送的某个TCP报文段中,确认号为301,窗口为500,表示我方还可以接收301-800序号(500B)的数据。
  • 校验和:占2B,用于计算校验和,它的校验范围包括TCP首部和TCP数据部分。
  • 紧急指针:占2B,和紧急位搭配使用,表示本报文的前多少个字节是紧急数据。
  • 选项:长度可变,最大长度为40字节,用于定义一些特殊的功能,比如认证,超时等。选项后可能还会有填充字段,它保证了整个的TCP首部长度为4B的整数倍。

TCP的连接分为三个阶段:建立连接,传输数据,关闭连接。连接采用C/S架构,基于套接字socket,即连接由一个4元组构成,分别是两个IP地址和两个端口号,通过这个唯一的4元组标识唯一的连接。

2 建立连接

TCP通过三次握手建立连接,如图:

三次握手.png

其中:

  • 第一次:客户端TCP首先向服务端TCP发起连接请求报文,这个报文中不含应用层数据。客户端随机生成一个起始序列号seq=x,并且将SYN置为1。
  • 第二次:服务端收到连接后,如果同意建立连接,就会向客户端发送确认,并且为该TCP创建资源,并发送一个TCP报文,这个报文中不含应用层数据,且将SYN和ACK置1,ack=x+1(表示前x字节的数据已经成功接收,期望收到第x+1序号的数据),并随机生成一个起始序列号seq=y。
  • 第三次:客户端收到服务端的确认报文后,它同样会为连接创建资源,并发送一个TCP报文告知服务器已经正确收到确认报文,这个报文已经可以发送数据,且SYN置0,ACK置1,seq=x+1,ack=y+1(表示前y字节的数据已经成功接收,期望收到第y+1序号的数据)服务器收到这个报文后,TCP连接成功建立,接下来就可以传输数据。(如果该报文段没有携带数据,下次的seq仍然为x+1)

服务器的buffer和变量资源是在第二次握手时分配的,而客户端此时还没有分配资源。想象一下,假如客户端只向服务器发送SYN,并且不发送第三次握手,即故意不完成建立连接所需要的三次握手过程,那么服务器的资源就会被白白消耗。这种攻击方式叫做SYN洪泛攻击。

为什么需要三次握手?

为什么需要三次握手,而不是两次?这个经典问题,在谢希仁《计算机网络》中是这样回答的:这是为了防止已经失效的TCP连接请求又传送到服务器,因而产生错误。这是其中的一个原因,但细节不止如此,对于更深层次的理解简单一两句话并不能概括。从TCP解决的问题来看,由于要保证可靠传输,所以要在连接时确认双方收发数据的能力是否正常。

查阅RFC793文档,可以看到这样的解释:

A three way handshake is necessary because sequence numbers are not tied to a global clock in the network, and TCPs may have different mechanisms for picking the ISN’s. The receiver of the first SYN has no way of knowing whether the segment was an old delayed one or not,unless it remembers the last sequence number used on the connection(which is not always possible), and so it must ask the sender to verify this SYN.

由于seq不依赖于一个网络中的全局时钟,所以三次握手是需要的。TCPs可以有不同的机制来挑选ISN(初始seq)。第一个SYN的接收者无法知道分片是否是个旧的延迟的分片,除非它记得上次用于连接的系列号(这并不总是可行的),因此它必须要发送者验证这个SYN。

同时也给出了一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 TCP A                                                TCP B

1. CLOSED LISTEN

2. SYN-SENT --> <SEQ=100><CTL=SYN> ...

3. (duplicate) ... <SEQ=90><CTL=SYN> --> SYN-RECEIVED

4. SYN-SENT <-- <SEQ=300><ACK=91><CTL=SYN,ACK> <-- SYN-RECEIVED

5. SYN-SENT --> <SEQ=91><CTL=RST> --> LISTEN


6. ... <SEQ=100><CTL=SYN> --> SYN-RECEIVED

7. SYN-SENT <-- <SEQ=400><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED

8. ESTABLISHED --> <SEQ=101><ACK=401><CTL=ACK> --> ESTABLISHED

Recovery from Old Duplicate SYN

Figure 9.

3:一个之前发送的重复的SYN到达TCP B。

4:TCP B不能断定这是之前发送的旧包,所以它正常响应。

5:TCP A检测到ACK字段不正确就返回了一个RST(reset)。TCP B在收到RST后,返回到LISTEN状态。

7:新的SYN在6到达,同步过程正常开始。

如果6的SYN在RST前到达,则将发生更复杂的交换(两个方向都会发送RST)。

这很像谢希仁《计算机网络》中的示例:

“已失效的连接请求报文段” 的产生在这样一种情况下:client 发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达 server。本来这是一个早已失效的报文段。但 server 收到此失效的连接请求报文段后,就误认为是 client 再次发出的一个新的连接请求。于是就向 client 发出确认报文段,同意建立连接。假设不采用 “三次握手”,那么只要 server 发出确认,新的连接就建立了。由于现在 client 并没有发出建立连接的请求,因此不会理睬 server 的确认,也不会向 server 发送数据。但 server 却以为新的运输连接已经建立,并一直等待 client 发来数据。这样,server 的很多资源就白白浪费掉了。采用 “三次握手” 的办法可以防止上述现象发生。例如刚才那种情况,client 不会向 server 的确认发出确认。server 由于收不到确认,就知道 client 并没有要求建立连接。”

将它稍作修改,就是RFC793中的例子:

client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,而client又发送了一个新的请求连接报文。在新的报文还没到达之前,旧的报文到达server,这是一个早已失效的报文段。但 server 无法区分,于是就向 client 发出确认报文段,同意建立连接。假设不采用 “三次握手”,那么只要server发出确认,新的连接就建立了。client虽然等待收到的是新seq的确认,却收到了旧seq确认,因此认为出错,会向 server 发送复位包。但此时的server已经建立连接,并一直等待 client 发来数据,却收到的是复位。这样,server的很多资源就白白浪费掉了。采用 “三次握手” ,可以使server保持在SYN-RECEIVED状态下。

RFC793中提到:

The three-way handshake reduces the possibility of false connections. It is the implementation of a trade-off between memory and messages to provide information for this checking.

三次握手减少了错误连接的可能性,它实现了在内存和提供检查信息的消息之间的一个平衡。通过这句话可以看出,三次握手并不能完全避免错误连接,而是为了降低错误连接,在内存和消息检查之间的一种权衡。

3 关闭连接

TCP通过四次挥手关闭连接,如图:

四次挥手.jpeg

其中:

  • 第一次:当客户端需要关闭连接时,向服务端发出连接释放报文,并停止发送数据。将FIN置1,序列号seq=u(前面传输数据的最后一个序号+1,因为FIN报文段即使不携带数据也要占据一个序列号)。此时客户端可以正常收数据,但不能发送数据。
  • 第二次:服务端收到客户端发的FIN报文后,对客户端回复确认报文,将ACK置1,确认号ack=u+1,序列号seq=v(前面传输数据的最后一个序号+1)。此时从客户端到服务端方向已经关闭,但是服务端可能还有数据没发完,因此服务端到客户端还可以继续发送数据,这个状态还要持续一段时间,
  • 第三次:服务端将最后数据发送完毕后(或者没有要发送的数据),就向客户端发出连接释放报文,将FIN和ACK置1,seq=w(前面传输数据的最后一个序号+1),ack=u+1(这是因为客户端没有发送数据)。
  • 第四次:客户端收到服务端的连接释放报文后,必须发出确认。确认报文ACK置1,ack=w+1、seq=u+1。客户端发出确认报文后,不会立刻释放TCP连接,而是要经过2MSL(一般是2×2=4分钟)后才释放TCP连接。服务端收到客户端发出的确认报文,会立刻释放TCP连接,所以服务端结束TCP连接的时间一般要比客户端早。

二、使用wireshark分析TCP

首先确定请求地址,这里以浏览器访问百度(www.baidu.com) 为例,从F12里获取远程地址,用于在wireshark中过滤。

image-20220217003219120

过滤到第一个TCP数据包如下:

image-20220217013422032

根据TCP报文的结构:

1
2
3
4
5
6
7
8
9
10
11
12
e1 bc 01 bb 01 fa e4 a0 00 00 00 00 80 02 fa f0
75 2e 00 00 02 04 05 b4 01 03 03 08 01 01 04 02

e1 bc: 客户端端口,57788
01 bb:服务端端口,443
01 fa e4 a0:序列号,33219744
00 00 00 00:确认号,0
80 02:保留字段和标志位,二进制为10000000 00000010,1000是首部长度,为8×4B=32B,00000010即SYN置1
fa f0:窗口,还可以接收64240B数据
75 2e:校验和
00 00:紧急指针
02 04 05 b4 01 03 03 08 01 01 04 02:选项,一些参数的设置

可以看出这是第一次握手的TCP报文。

来看看第二次握手:

1
2
3
4
5
6
7
8
9
10
11
12
01 bb e1 bc a1 14 d4 f9 01 fa e4 a1 80 12 20 00
99 ed 00 00 02 04 05 a0 01 03 03 05 01 01 04 02

01 bb:443
e1 bc:57788 可以看出发送方和接收方调换顺序,这是服务器向我们发的报文
a1 14 d4 f9:序列号,2702497017
01 fa e4 a1:确认号,33219745,可以发现等于我们发送的seq+1,说明baidu已经收到了我们发送的报文
80 12:保留字段和标志位,二进制为10000000 00010010,1000是首部长度,为8×4B=32B,00010010即SYN置1,ACK置1
20 00:窗口,还可以接收8192B数据
99 ed:校验和
00 00:紧急指针
02 04 05 a0 01 03 03 05 01 01 04 02:选项,一些参数的设置

第三次握手:

1
2
3
4
5
6
7
8
9
10
11
e1 bc 01 bb 01 fa e4 a1 a1 14 d4 fa 50 10 02 05
75 22 00 00

e1 bc: 客户端端口,57788
01 bb:服务端端口,443
01 fa e4 a1:序列号,33219745
a1 14 d4 fa:确认号,2702497018,等于baidu发送的seq+1,说明我们已经收到了baidu发送的报文
50 10:保留字段和标志位,二进制为01010000 00010000,0101是首部长度,为5×4B=20B,没有选项字段。00010000即ACK置1
02 05:窗口,还可以接收517B数据
75 22:校验和
00 00:紧急指针

如此一来,TCP连接就建立了。

接下来看看四次挥手的过程,找到第一个FIN的TCP报文:

image-20220217050947486

1
2
3
4
5
6
7
8
9
10
11
12
01 bb e1 bc a1 14 df e9 01 fa fc 98 50 11 05 cc
d1 f6 00 00


01 bb: 发送方,443
e1 bc:接收方,57788 可以看出是baidu主动要求断开连接
a1 14 df e9:序列号,2702499817
01 fa fc 98:确认号,33225880
50 11:保留字段和标志位,二进制为01010000 00010001,首部长度5×4B=20B,00010001即FIN置1,ACK置1
05 cc:窗口,还可以接收1484B数据
d1 f6:校验和
00 00:紧急指针

第二次挥手:

1
2
3
4
5
6
7
8
9
10
11
e1 bc 01 bb 01 fa fc 98 a1 14 df ea 50 10 02 02
75 22 00 00

e1 bc: 客户端端口,57788
01 bb:服务端端口,443
01 fa fc 98:序列号,33225880
a1 14 df ea:确认号,2702499818
50 10:保留字段和标志位,二进制为10000000 00010000,首部长度5×4B=20B,00010000即ACK置1
02 02:窗口,还可以接收514B数据
75 22:校验和
00 00:紧急指针

第三次挥手也是我们发出的:

1
2
3
4
5
6
7
8
9
10
11
e1 bc 01 bb 01 fa fc 98 a1 14 df ea 50 11 02 02
75 22 00 00

e1 bc: 客户端端口,57788
01 bb:服务端端口,443
01 fa fc 98:序列号,33225880 由于没有要发送的数据,所以与第二次挥手的seq相同
a1 14 df ea:确认号,2702499818 确认号也相同
50 11:保留字段和标志位,二进制为10000000 00010000,首部长度5×4B=20B,00010001即FIN置1,ACK置1
02 02:窗口,还可以接收514B数据
75 22:校验和
00 00:紧急指针

有趣的是,百度并没有为我们回复确认报文。

image-20220217052312298

在三次挥手后,由于百度没有回复确认报文,根据TCP的重传机制,我们向百度发送了10次重传,均没有得到回复,最后发送了RST复位,也没有得到回复,关闭了连接(可恶,百度居然不遵守2MSL!!)。

三、其他

基于IP层的传输是不可靠的,TCP则解决了这个问题,它提供了与UDP基本相同的校验机制,以及通过seq序号进行确认和重传的机制。

这里要聊一个名词叫做累计确认,意思是TCP只确认数据流中的第一个 丢失字节为止之前的字节。比如,A发送了1、2、3、4、5个报文段,B只接收了1、2、5号报文,而其余的都丢失了,此时B会发送一个ack=3,告知A“前2个字节已经正确接收,期待收到第3个字节”,尽管收到了5号报文,ack也不会等于5。

在上面wireshark的四次挥手分析中,由于百度没有进行第四次挥手,于是触发了重传机制。这是由于超时触发的,TCP在每次发送报文后,就会对这个报文设置一个计时器。并且,TCP还设计了一个算法,它记录报文发出的时间,以及收到确认的时间,这两个时间差叫做Round trip time(RTT),中文叫做往返时间,在此基础上动态计算出RTT~S~,中文叫做加权平均往返时间,计时器设置的重传时间应该略大于RTT~S~。当计时器到达这个重传时间还未收到确认,就会重传报文。

实际上,还有另一种触发重传的方法就是冗余确认机制。举个例子,A发送了1、2、3、4、5、6个报文段,其中3号报文丢失,此时,即使B收到了4、5、6报文,也不会对它们进行确认(累计确认机制),但是TCP还规定每收到一个失序报文(本例中就是4、5、6号报文),就要发送一个冗余的ACK,即B会发送3个冗余的ACK,表示自己期望收到3号报文。当A收到这3个冗余的ACK时,就认为3号报文丢失,此时会对3号报文重传,这就是TCP的快速重传机制。

此外,在RFC5681中,还介绍了拥塞控制、快速重传等相关内容,感兴趣的可以去看一看。