Wireshark 抓包理解 HTTPS 请求流程(1) - TCP 三次握手

本文目录:

准备

我的操作是这样的,让手机和电脑在同一个局域网内(比如连接同一个 wifi),接着在手机的wifi上设置代理,电脑使用 Charles 做代理,IP 为电脑在局域网 IP,我这边的环境,手机 IP 为 172.17.32.117,电脑 IP 为 172.17.32.19。再设置代理端口为 8888。设置代理后,接下来手机的请求都会通过电脑的网卡代理请求发送出去。

其实可以不用这么绕。我之所以多设了一个代理,是因为自己电脑创建的 wifi 热点,手机接收不到。为了让手机的包能经过电脑网络嗅探到才这么处理的。

最便捷的方式,就是电脑放个 wifi 热点给手机连接完事。

创建后代理连接后,然后使用 Wireshark 嗅探网卡,比如我这里使用的是 etho0 网卡去访问网络的。这时候玩玩手机,打开几个请求,Wireshark 上面就会出现捕捉的大量的包,各种各样的协议都有,有 ARP 寻人启事(寻找 IP 对应的物理地址),有 TCP 连接包,有 HTTP 请求包。

抓包-所有数据

这里我设置了一下过滤规则,把对网易的一个 https://nex.163.com 的一个的请求过滤出来如下:

整个完整的 HTTPS 请求的过程如下:

  • TCP 三次握手
  • 因为我使用电脑作为代理,所以还有一个 CONNECT 请求用来建立 HTTP 代理
  • 使用 TLSv1.2 进行 SSL 握手
  • 使用握手协商好的密钥对 HTTP 进行加密传输
  • TCP 四次挥手

接下来把手机称为 A(172.17.32.211),电脑称为 B(172.17.32.19),对完整的过程进行简要分析。

三次握手

TCP 协议内容

作为整个过程的第一个 TCP 包,这里对它做一个详细的剖析,理解一下 TCP 报文的格式和内容。TCP 是传输层协议,负责可靠的数据通信,它在整个体系结构的位置如下:

体系结构

作为传输层协议,主要为上层协议提供三个功能:

  • 可靠传输,为每个字节安排好序号,排好序,并且有重传机制保证信息不丢失。
  • 流量控制,有滑动窗口,避免发送端和接收端速率不一致导致发包过快来不及接收。
  • 拥塞避免,在网络环境差的时候,控制好传包的时间间隔,避开高峰期,不给原本已经很拥堵的网络添堵。

TCP 协议为 HTTP 和 SSL 协议提供了基础的通信功能。所以 SSL 协议是基于 TCP 的。

三次握手的内容有:

No. Time Source Destionation Protocol Length Info
379 4.623811 172.17.32.211 172.17.32.19 TCP 74 35973 → 8888 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM=1 TSval=15986187 TSecr=0 WS=256
380 4.623860 172.17.32.19 172.17.32.211 TCP 74 8888 → 35973 [SYN, ACK] Seq=0 Ack=1 Win=8192 Len=0 MSS=1460 WS=256 SACK_PERM=1 TSval=59355465 TSecr=15986187
393 4.781431 172.17.32.211 172.17.32.19 TCP 66 35973 → 8888 [ACK] Seq=1 Ack=1 Win=87808 Len=0 TSval=15986193 TSecr=59355465

对每个包进行详细的分析:

Round 1

A 发出一个带 SYN 同步位的包,通知服务端要建立连接

第一次握手,发出的 TCP 包的数据和 Wireshark 解析的结果如下:

三次握手-1

灰色部分就是 TCP 报文的数据内容,第一个两个字节 0x8c85 = 35973 表示源端口。

TCP 报文的格式如下,对应的如上图的灰色部分。非灰色部分分别为 IP 首部和数据帧首部。

 0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    源端口                      |                     目的端口                   |
+-----------------------------------------------+-----------------------------------------------+
|                                             序列号                                             |
+-----------------------------------------------------------------------------------------------+
|                                             确认号                                             |
+-----------+-----------------+-----------------+-----------------------------------------------+
|  首部长度  |     保留位       | U| A| P| R| S| F|                      窗口                      |
+-----------+-----------------+-----------------+-----------------------------------------------+
|                    校验和                      |                     紧急指针                   |
+-----------------------------------------------+-----------------------+-----------------------+
|                    选项                                                |        填充            |
+=======================================================================+=======================+
|                                             数据                                               |

参考谢希仁版本的 《计算机网络》一书,对照着整个报文格式表,把整个 TCP 报文的二进制信息和相关意义做些说明:

  • 源端口,2 字节,0x8c85 = 35973。

  • 目的端口,2字节,0x22b8 = 8888。

  • 序号,4 字节,0xc7a3ce5c。TCP 连接传送的每一个字节都有编号,而这里表示的是要发送的数据(data)的第一个字节的序号。后面按照这个序号递增。这里发送的是 TCP 连接的第一个包,这里的序号是随机产生的。

  • 确认号,4 字节,0x00000000。期望收到的下一个报文段数据部分第一个字节的序号。如果发出了 N 确认号,就表示 N-1 之前的数据都收到了。

  • 首部长度(偏移),4 位,0xa = 10。这里每一位表示4字节,所以 10*4 = 40 字节。TCP 首部的长度,因为 4 位最大的数为 15,所以整个首部最大 60 字节。首部后面就是数据字段。

  • 保留位,6 位,0x000。这里还没有用到为 0,这里的设计作为一个扩展以便未来会用上。

  • 控制位,6 位,0x002 = 二进制 000010。每一位都有意义,这里对应 6 种类型:

    • URG,Urgent,表示紧急数据要提交,有了这个标记为整个报文就有了插队的特权,和紧急指针一起用。
    • ACK,Acknowledge,1 表示这是一个确认报文,用来确认收到了包,确认报文是不带数据的。
    • PSH,Push,1 表示这是一个推送报文,通知对方尽快响应。因为服务端可能因为缓存问题要等了一会儿才发包。这里就是催促一下对方赶紧发包。
    • RST,Reset,1 表示拒绝了这个包,网络发生错误的时候这种包会非常多。比如重复的包就会被 reset 掉。
    • SYN,Synchronization,1 表示建立连接用来同步序号。用来握手阶段,通知对方包的初始序号。
    • FIN,Finish,1 表示发送方 B 完成数据发送,通知接收方 A 该结束了。

    我们这里作为整个请求过程中的第一个 TCP 报文,仅仅设置一个控制位 SYN,一方面通知响应者 B 要建立连接了,另一方面同步一下序号。

  • 窗口,2 字节,0xffff = 65535。发送本报文的接收窗口大小,比如 A 发送了这个报文,表示能接收的数据量为从确认号算起来加上窗口大小,B 发送的报文字节数不能超过这个限制。这个值受 A 的缓存影响,是动态变化的。前面给出确认号为 0, 然后窗口大小 65535,表示还有 65535 的缓存空间可以接受序号 0 ~ 65535 的字节。

  • 检验和,2 字节,0x068f。接收方受到报文要计算一下数据包的检验和是否和该值匹配,确保数据包的完整。这里只保证了数据完整性,并没有确认服务端身份,也没有用摘要算法。目的在于能快速检验,而且采用检验和的方式还可以使用硬件加速。

  • 紧急指针,2 字节,0x0000f。和控制位 URG 配合使用,意义在于,有紧急数据要处理,这里的值表示紧急数据在报文中的位置。

到这里的话,TCP 数据报首部固定部分结束,固定部分一共有 20 字节。也就是 TCP 首部,至少要有 20 字节。

固定首部后,就是可长度可以变化的选项了:

  • 选项,可达 40 字节

    • 最大报文长度,0x020405b4。0x2 表示这是个 MSS 选项,0x04 表示该选项一共有 4 字节,这里的 0x05b4 = 1460 为该选项的值。这个表示 TCP 数据部分的最大字节数。

    • 时间戳,0x080a00f3ee0b00000000。0x8 表示这是个时间戳选项,0x0a 表示该选项一共有 10 字节,0x00f3ee = 15986187 就是发送者的发送时间,0x00000000 = 0 表示接收端的时间。

整个所以 TCP 数据包的大小可以这样表示:

+------------------------+--------------------------------------------------------------+
|  TCP 首部,20 ~ 60 字节 |           数据部分,受 MSS 大小限制                          |     
+------------------------+--------------------------------------------------------------+

我们 Wireshark 后面的一长串的信息就指出了该 TCP 报文的一些主要信息:

359738888 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM=1 TSval=15986187 TSecr=0 WS=256
  • 源端口和目的端口,35973->8888。
  • 序号,Seq=0,这是一个相对值而非绝对值,相对第一个包的序列号。因为是整个 TCP 流的第一包,所以 Wireshark 认定该包的序列号为 0。
  • 窗口大小,win=65535,也就是发送端的当前窗口最多容纳 65535 个字节。
  • 数据部分大小,Len=0,不带数据。
  • 最大报文大小选择,MSS=1460,数据部分最多有 1460 个字节。
  • 选择确认选项,SACK_PERM=1。
  • 发送时间戳,TSval=15986187,发出这个数据包的时候的时间戳。
  • 应答时间戳,TSecr=0,当前要发送的包应答的那个包的发送时间戳,因为是第一个包,应答的时间戳为 0。
  • 窗口扩大,WS=256。

从上面的分析可以看出,这个 SYN 包并没有携带数据,但是按协议这里要消耗一个序号。

在发出 SYN 包后,A 端进入 SYN-SENT 状态。

Round 2

B 收到 SYN 包,发出 SYN + ACK 确认包

这个包,既是确认收到了第一次握手的包,也是一个由 B 端发出的同步包,表示自己准备好了,可以开始传数据了。

因为 Wireshark 已经帮我们分析好包的内容了,上面列举的包二进制数据和 TCP 报文结构只是为了学习,实际应用可以直接看 Wireshark 的解析内容。包的内容如下:

三次握手-2

TCP 报文包相对于第一次握手的包可以窥见一些变化:

  • 源端口和目的端口:8888 -> 35973。
  • 窗口大小:8192,可以看成接收端的窗口只有 8192,和发送端差距还是挺大的。
  • 控制位:既有 SYN 又有 ACK。因为这既是一个接收端 B 的自己同步包,里面有一个接收端的初始序号,Wireshark 转化为相对序号 0;同时这也是对第一次握手的包的确认。因此这个包也不带数据。

  • 发送时间戳:59355465

  • 应答时间戳:15986187

可以看到,这个包的应答时间戳刚好是第一次握手的发送时间戳。从这里也可以理解到,这个包就是在响应第一次握手的包

所以,接收方 A 可以利用这个值来计算这一次 RTT,收到第二次握手的包后,计算当前时间戳减去该包的应答时间戳就是一个 RTT 的延时了。

这虽然是 ACK 包,但也是 SYN 包,所以也要消耗一个序号。

在发出这个包后,B 端进入 SYN-REVD 状态。

Round 3

A 收到后,再发出一个 ACK 确认包

发出的包如下:

No. Time Source Destionation Protocol Length Info
393 4.781431 172.17.32.211 172.17.32.19 TCP 66 35973 → 8888 [ACK] Seq=1 Ack=1 Win=87808 Len=0 TSval=15986193 TSecr=59355465

这里我们产生一个疑问,这里发送端 A 发连接请求信息、接收端 B 发确认信息,又互相同步了序号,是不是已经可以传输数据了?但实际上 A 还要再发一个 ACK 确认报文,如图所示,确认收到了 B 第二次握手发出的包,这个时候,在这个 ACK 包后 A 和 B 才正式进入 ESTABLISHED 状态。这就是第三次握手。

这是为什么呢?

假设我们用两次握手,然后在第一次握手期间,A 发了第一次握手包后出现了这样的场景:一直没有得到响应而进行超时重传,又发了一次包,然后我们称上一次包为失效包。然后我们可以看到,会出现这样的情形:

  • 新包到达接收端 B,然后因为两次握手 B 觉得连接建立,于是等着发送端 A 发数据,A 也发了数据。
  • 失效的包经过艰苦跋涉,也到达了接收端 B,B 并不清楚这是失效包,又开始等待发送端 A 发数据。然而此时 A 不发数据,所以 B 的资源就浪费掉了。

所以,只有接收端 B 在发送端 A 发出了第三次握手包后,才认为连接已经建立,开始等待发送端 A 发送的数据,才不会因为失效的连接请求报文导致接收端异常。

小结

TCP 三次握手的时序图如下:

 三次握手-时序图

三次握手,有几个重要的任务,一个是同步序号,接收端和发送端都发出同步包来通知对方初始序号,这样子后面接收的包就可以根据序号来保证可靠传输;另一个是让发送端和接收都做好准备。然后就开始传数据了。

整个过程都发生在 HTTP 报文发出之前。HTTP 协议就是依靠着 TCP 协议来做传输的管理。TCP 可以认为是它的管家,管理着传输的大大小小的事务,比如要不要保证包顺序一致?什么时候发包?要不要收包?TCP 是很严格的。

三次握手在 Java API 层面,对应的就是 Socket 的连接的创建(最终调用的是 native 层的 socket 创建):

socket.connect(address, connectTimeout);

这里的 connectTimeout 对应的是三次握手的总时长,如果超时了就会被认为连接失败。

比如一个场景,客户端发出一个 SYN 报文后,迟迟没有收到服务端的 SYN + ACK。这时候客户端触发重传机制,每次重传的间隔时间加倍,同样没有收到包。然后如果这段时间超出了连接超时时间的设置,那么建立连接超时就发生了。

所以,如果三次握手要花的时间,总是大于这里的 connectTimeout 时间,这个 Socket 就无法建立连接。

我们这一次请求的三次握手时间在 180ms 左右。

像在 OkHttp 中,如果是三次握手阶段的连接超时,是会有重试机制的。也就是重新建联,重新发出 SYN 报文发起 TCP 连接。重新建联的时候会更换连接的路由,如果已经没有可选择路由的话,那么这个就真的失败了。

在 OkHttp 3.9.0 的默认配置中,连接超时的时间为 10000ms = 10s。在 OkHttpClient.Builder 中。

      connectTimeout = 10_000;
      readTimeout = 10_000;
      writeTimeout = 10_000;

实际应用的时候,根据业务场景来调整。Wireshark 抓包理解 HTTPS 请求流程

猜你喜欢

转载自blog.csdn.net/firefile/article/details/80537003