Ref: https://www.cnblogs.com/kaleidoscope/p/9701117.html

TCP 报文结构

image.png
image.png

  • 源端口和目的端口:各占 2 个字节,分别写入源端口号和目的端口号。
  • 序号:占 4 个字节。序号使用 mod 运算。TCP 是面向字节流的,在一个 TCP 连接中传送的字节流中的每一个字节都按顺序编号。故该字段也叫做 “报文段序号”。
  • 确认序号:占 4 个字节,是期望收到对方下一个报文段的第一个数据字节的序号。若确认序号 = N, 则表明:到序号 N-1 为止的所有数据都已正确收到。
  • 数据偏移:占 4 位,表示 TCP 报文段的首部长度。注意,“数据偏移” 的单位是 32 位字(即以 4 字节长的字为计算单位)。故 TCP 首部的最大长度为 60 字节。
  • 保留:占 6 位,保留为今后使用,目前置为 0;
  • 紧急 URG:当 URG=1,表明紧急指针字段有效。这时发送方 TCP 就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据。
  • 确认 ACK:当 ACK=1 时,确认字段才有效。当 ACK=0 时,确认号无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
  • 推送 PSH:接收方 TCP 收到 PSH=1 的报文段,就尽快地交付给接收应用进程,而不再等到整个缓存都填满了后再向上交付。
  • 复位 RST:当 RST=1 时,表明 TCP 连接中出现严重差错,必须释放连接,然后再重新建立运输连接。
  • 同步 SYN(Synchronize Squence Number):在连接建立时用来同步序号。当 SYN=1 而 ACK=0 时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使 SYN=1 和 ACK=1。故 SYN 置为 1,就表示这是一个连接请求和连接接收报文。
  • 终止 FIN:用来释放连接。当 FIN=1 时,表明此报文段的发送方的数据已发送完毕,并要求释放运输连接。
  • 窗口:占 2 个字节。窗口值作为接收方让发送方设置其发送窗口的依据。
  • 检验和:占 2 字节。检验和字段检验的范围包括首部和数据这两部分。和 UDP 数据报一样,在计算检验和时,也要在 TCP 报文段的前面加上 12 字节的伪首部。伪首部的格式与 UDP 用户数据报的伪首部一样,但要将伪首部第四个字段中的 17 改为 6(协议号),把第 5 字段中的 UDP 长度改为 TCP 长度。
  • 紧急指针:占 2 字节。紧急指针仅在 URG=1 时才有意义,它指出本报文段中的紧急数据的字节数。

TCP 三次握手

image.png
整个流程为:

  1. 客户端主动打开,发送连接请求报文段,将 SYN 标识位置为 1,Sequence Number 置为 x(TCP 规定 SYN=1 时不能携带数据,x 为随机产生的一个值),然后进入 SYN_SEND 状态。
  2. 服务器收到 SYN 报文段进行确认,将 SYN 标识位置为 1,ACK 置为 1,Sequence Number 置为 y,Acknowledgment Number 置为 x+1,然后进入 SYN_RECV 状态,这个状态被称为半连接状态。
  3. 客户端再进行一次确认,将 ACK 置为 1(此时不用 SYN),Sequence Number 置为 x+1,Acknowledgment Number 置为 y+1 发向服务器,最后客户端与服务器都进入 ESTABLISHED 状态。


    为什么在第 3 步中客户端还要再进行一次确认呢?
    这主要是为了防止已经失效的连接请求报文段突然又传回到服务端而产生错误的场景:所谓 “已失效的连接请求报文段“ 是这样产生的。正常来说,客户端发出连接请求,但因为连接请求报文丢失而未收到确认。于是客户端再次发出一次连接请求,后来收到了确认,建立了连接。数据传输完毕后,释放了连接,客户端一共发送了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,没有 “已失效的连接请求报文段”。
    现在假定一种异常情况,即客户端发出的第一个连接请求报文段并没有丢失,只是在某些网络节点长时间滞留了,以至于延误到连接释放以后的某个时间点才到达服务端。本来这个连接请求已经失效了,但是服务端收到此失效的连接请求报文段后,就误认为这是客户端又发出了一次新的连接请求。于是服务端又向客户端发出请求报文段,同意建立连接。假定不采用三次握手,那么只要服务端发出确认,连接就建立了。
    由于现在客户端并没有发出连接建立的请求,因此不会理会服务端的确认,也不会向服务端发送数据,但是服务端却以为新的传输连接已经建立了,并一直等待客户端发来数据,这样服务端的许多资源就这样白白浪费了。
    采用三次握手的办法可以防止上述现象的发生。比如在上述的场景下,客户端不向服务端的发出确认请求,服务端由于收不到确认,就知道客户端并没有要求建立连接。

TCP 四次挥手

TCP 三次握手是 TCP 连接建立的过程,TCP 四次挥手则是 TCP 连接释放的过程下面是 TCP 四次挥手的流程图:
image.png
当客户端没有数据再需要发送给服务端时,就需要释放客户端的连接,这整个过程为:

  1. 客户端发送一个报文给服务端(没有数据),其中 FIN 设置为 1,Sequence Number 置为 u,客户端进入 FIN_WAIT_1 状态
  2. 服务端收到来自客户端的请求,发送一个 ACK 给客户端,Acknowledge 置为 u+1,同时发送 Sequence Number 为 v,服务端进入 CLOSE_WAIT 状态
  3. 服务端发送一个 FIN 给客户端,ACK 置为 1,Sequence 置为 w,Acknowledge 置为 u+1,用来关闭服务端到客户端的数据传送,服务端进入 LAST_ACK 状态
  4. 客户端收到 FIN 后,进入 TIME_WAIT 状态,接着发送一个 ACK 给服务端,Acknowledge 置为 w+1,Sequence Number 置为 u+1,最后客户端和服务端都进入 CLOSED 状态

image.png**

3 次握手与 4 次挥手

为什么建连接要 3 次握手,断连接需要 4 次挥手?

  • 对于建连接的 3 次握手,主要是要初始化 Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的 Sequence Number(缩写为 ISN:Inital Sequence Number)—— 所以叫 SYN,全称 Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP 会用这个序号来拼接数据)。
  • 对于 4 次挥手,其实你仔细看是 2 次,因为 TCP 是全双工的,所以,发送方和接收方都需要 Fin 和 Ack。只不过,有一方是被动的,所以看上去就成了所谓的 4 次挥手。如果两边同时断连接,那就会就进入到 CLOSING 状态,然后到达 TIME_WAIT 状态。下图是双方同时断连接的示意图(你同样可以对照着 TCP 状态机看):

image.png

使用 Wireshark 抓包验证 TCP 三次握手过程
为了加深对 TCP 三次握手的理解,抓包看一下 TCP 三次握手的过程。
抓包下来的内容为:
image.png
这里多说一句,由于 wireshark 抓包针对的是网卡,因此只要某张网卡上有网络访问,就会有数据包,这会导致 Wireshark 的抓包结果里面会有大量数据包,而大多数都不是想要的,这种情况可以使用 Wireshark 的过滤规则。我这里由于知道目标 ip,因此使用的是 “ip.src == xxx.xxx.xxx.xxx or ip.dst == xxx.xxx.xxx.xxx“ 这条规则只过滤特定的 ip。

从抓包结果看来,整个过程符合 TCP 三次握手的预期:

  1. 客户端发送 SYN 给服务端
  2. 服务端返回 SYN+ACK 给客户端
  3. 客户端确认,返回 ACK 给服务端

至于 Sequence Number 和 Acknowledge Number 就不看了,但是注意,前面说了 Sequence Number 是随机产生的一个值,但是这里确是 0,不光这里是 0,抓其他的任何包这个值都是 0。但其实这里并不是真的 0,而是 Wireshark 为了显示更好阅读,使用了 relative sequence number 相对序号,Sequence Number 具体值我们也是可以看到的:
image.png
第一个红框就是上面说的 relative sequence number,第二个红框就是 Sequence Number 的真实值 0xc978aa7e,转换为十进制为 3380128382,就是随机产生的 Sequence Number。
顺便能看到,下一个数据包就是 HTTP 的数据包,因为 TCP 三次握手已完成,连接建立,正式传输应用层数据,传输的 HTTP 内容大小为 704 字节。