1. 准备
我的操作是这样的,让手机和电脑在同一个局域网内(比如连接同一个 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 四次挥手
2. 三次握手
2.1. TCP 协议内容
作为整个过程的第一个 TCP 包,这里对它做一个详细的剖析,理解一下 TCP 报文的格式和内容。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 |
2.2. Round 1
A 发出一个带 SYN 同步位的包,通知服务端要建立连接第一次握手,发出的 TCP 包的数据和 Wireshark 解析的结果如下:
灰色部分就是 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| 窗口 |
+-----------+-----------------+-----------------+-----------------------------------------------+
| 校验和 | 紧急指针 |
+-----------------------------------------------+-----------------------+-----------------------+
| 选项 | 填充 |
+=======================================================================+=======================+
| 数据 |
- 源端口,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 该结束了。
- 窗口,2 字节,0xffff = 65535。发送本报文的接收窗口大小,比如 A 发送了这个报文,表示能接收的数据量为从确认号算起来加上窗口大小,B 发送的报文字节数不能超过这个限制。这个值受 A 的缓存影响,是动态变化的。前面给出确认号为 0, 然后窗口大小 65535,表示还有 65535 的缓存空间可以接受序号 0 ~ 65535 的字节。
- 检验和,2 字节,0x068f。接收方受到报文要计算一下数据包的检验和是否和该值匹配,确保数据包的完整。这里只保证了数据完整性,并没有确认服务端身份,也没有用摘要算法。目的在于能快速检验,而且采用检验和的方式还可以使用硬件加速。
- 紧急指针,2 字节,0x0000f。和控制位 URG 配合使用,意义在于,有紧急数据要处理,这里的值表示紧急数据在报文中的位置。
固定首部后,就是可长度可以变化的选项了:
- 选项,可达 40 字节
- 最大报文长度,0x020405b4。0x2 表示这是个 MSS 选项,0x04 表示该选项一共有 4 字节,这里的 0x05b4 = 1460 为该选项的值。这个表示 TCP 数据部分的最大字节数。
- 时间戳,0x080a00f3ee0b00000000。0x8 表示这是个时间戳选项,0x0a 表示该选项一共有 10 字节,0x00f3ee = 15986187 就是发送者的发送时间,0x00000000 = 0 表示接收端的时间。
+------------------------+--------------------------------------------------------------+
| TCP 首部,20 ~ 60 字节 | 数据部分,受 MSS 大小限制 |
+------------------------+--------------------------------------------------------------+
35973 → 8888 [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 包后,A 端进入 SYN-SENT 状态。
2.3. Round 2
B 收到 SYN 包,发出 SYN + ACK 确认包这个包,既是确认收到了第一次握手的包,也是一个由 B 端发出的同步包,表示自己准备好了,可以开始传数据了。
因为 Wireshark 已经帮我们分析好包的内容了,上面列举的包二进制数据和 TCP 报文结构只是为了学习,实际应用可以直接看 Wireshark 的解析内容。包的内容如下:
TCP 报文包相对于第一次握手的包可以窥见一些变化:
- 源端口和目的端口:8888 -> 35973。
- 窗口大小:8192,可以看成接收端的窗口只有 8192,和发送端差距还是挺大的。
- 控制位:既有 SYN 又有 ACK。因为这既是一个接收端 B 的自己同步包,里面有一个接收端的初始序号,Wireshark 转化为相对序号 0;同时这也是对第一次握手的包的确认。因此这个包也不带数据。
- 发送时间戳:59355465
- 应答时间戳:15986187
所以,接收方 A 可以利用这个值来计算这一次 RTT,收到第二次握手的包后,计算当前时间戳减去该包的应答时间戳就是一个 RTT 的延时了。
这虽然是 ACK 包,但也是 SYN 包,所以也要消耗一个序号。
在发出这个包后,B 端进入 SYN-REVD 状态。
2.4. 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,然后因为两次握手 B 觉得连接建立,于是等着发送端 A 发数据,A 也发了数据。
- 失效的包经过艰苦跋涉,也到达了接收端 B,B 并不清楚这是失效包,又开始等待发送端 A 发数据。然而此时 A 不发数据,所以 B 的资源就浪费掉了。
2.5. 小结
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 请求流程