# 输入 URL 按下回车后

作为 HTTP 协议中的 user agent（请求方），浏览器要收发数据，就要建立 TCP 连接，因为 HTTP 是基于 TCP/IP 实现的数据的可靠传输。而要建立 TCP 连接，就得知道 `[transport protocol, IP address, port number]`。

## 1. 域名解析

### 1.1 解析 URL

浏览器根据 [URL 的语法结构](https://anjia1.gitbook.io/cs/http/urn-url-uri#2.-uri-de-tong-yong-yu-fa)解析 URL，提取里面的 scheme（协议）、host（主机）、port（端口）、path（路径）、query（参数）等。

<figure><img src="https://605825044-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVUa7MztTVfoHQxXozBQf%2Fuploads%2F43sAHguAFwwcdb7o63DF%2Fimage.png?alt=media&#x26;token=28f164cf-8941-4d2c-afac-feddd384f79a" alt=""><figcaption></figcaption></figure>

其中，scheme 和 path 是通用 URI 必需的，host 是 [HTTP URL 必需的](https://anjia1.gitbook.io/cs/http/url/url)。

* 若没有提供 port，则默认取 [scheme 对应的 port](https://anjia1.gitbook.io/cs/http/urn-url-uri#2.5-port)
* 若没有提供 path，则默认是 `/`

### 1.2 域名解析

如果 URL 里的 host 不是 IP address 而是 domain name，那就要用 [DNS 协议来解析域名](https://anjia1.gitbook.io/cs/http/dns#3.3-dns-jia-gou)了，即把 domain name 转成 IP address。下图分别是 IPv4 和 IPv6 的地址格式：

![](https://605825044-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVUa7MztTVfoHQxXozBQf%2Fuploads%2F9cbXpjFNPKI1NO0qHzX2%2Fimage.png?alt=media\&token=028dd814-d1ea-4d30-bede-6a134742bd59)

域名解析的过程，大致如下：

1. 本地域名 server（递归查询，所以查询顺序≠生效顺序）
   1. 浏览器 cache
   2. OS cache
   3. hosts file
2. 远程查询
   1. 非权威 DNS server
   2. DNS 核心系统：根域名 server -> 顶级域名 server -> 权威域名 server

## 2. 建立 TCP 连接

拿到 IP address 和 port 之后，浏览器会依据 TCP 协议的规范，使用三次握手与 web 服务器建立连接。

TCP 协议用 [scheme 和 port 来区分不同的服务](https://anjia1.gitbook.io/cs/http/urn-url-uri#2.5-port)。

* 浏览器在开始建立 TCP 连接之前，会得到一个动态 port，比如 52085 端口
* web 服务器会 bind 到某个 port，并一直 listen 着，比如 80 端口、443 端口

&#x20;     ![](https://605825044-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVUa7MztTVfoHQxXozBQf%2Fuploads%2FzdTuUxzOXt2g6BomKwMb%2Fimage.png?alt=media\&token=eb054dbd-b225-410b-9306-676e4c05f969)

### 2.1 三次握手

可以用 [Wireshark](https://anjia1.gitbook.io/cs/http/tools/wireshark) 抓到最开始的三个包：`[SYN]`、`[ACK, SYN]`、`[ACK]`。

<figure><img src="https://605825044-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVUa7MztTVfoHQxXozBQf%2Fuploads%2FJM5nCxFqEcOHKnacJXE0%2Fimage.png?alt=media&#x26;token=00efd58a-9908-45c5-9cd6-7e8883ba8a66" alt=""><figcaption></figcaption></figure>

### 2.2 为什么要三次？

三次握手并不是 TCP 本身的要求，而是为了解决“在不可靠的信道上进行可靠传输”的问题。三次通信是理论上的最小值，因为客户端和服务端要确认彼此都可以正常地“收发”。

> 三次握手的目的：（比较书面化，知道即可）
>
> * 为了防止“已失效的连接请求报文段”突然又传送到了服务端，进而产生错误
> * 或为了解决“网络中存在延迟的重复分组”的问题
>
> 如果信道是可靠的（即无论什么时候发送消息，对方都一定能收到）或是我们就不关心对方能否收到消息，那么就可以像 UDP 那样发送消息了。

TCP 连接是全双工通信，也就是说它能进行双向通信，且有两个通道，从 A→B 是一个通道，而从 B→A 又是另一个通道。

> 1. 双工通信（duplex communication）可以在两个方向上相互通信，有两种类型：
>    * 全双工（full-duplex）可以双向通信，且允许同时发生，比如老式电话。严格来说它们之间有两个 communication channels（沟通信道）
>    * 半双工（half-duplex 或 semiduplex）可以双向通信，但一次只能提供一个方向，比如对讲机
> 2. 单工通信（simplex communication）只能向一个方向发送信息，比如广播电台、电视、监控摄像头

1. A 给 B 发送一个建立连接的请求 `SYN`
2. B 回一个同意并确认 `ACK`
3. B 给 A 发送一个建立连接的请求 `SYN`
4. A 回一个同意并确认 `ACK`

其中，第 2 个和第 3 个可以合并成一个请求，所以有三次握手。如下：

![](https://605825044-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVUa7MztTVfoHQxXozBQf%2Fuploads%2F6s4Jpq7cHF3RMPOhAV1k%2Fimage.png?alt=media\&token=a9b3f785-c2ec-4d6f-955e-19724275bd01)

> 大写的 ACK 是报文类型，小写的 ack 是报文里的确认号（值是对方的 seq+1）

1. 一次握手 `SYN`：客户端向服务端发起连接请求，服务端（不一定）收到请求
   * 若收到了（便达成了）服务器知道了“客户端能发”
2. 二次握手 `ACK + SYN`：服务端确认收到了请求，同时告知客户端自己也要发送数据
   * 若收到了（便达成了）客户端知道了“服务器能收”，且“服务器能发”
3. 三次握手 `ACK`：客户端确认
   * 若收到了（便达成了）服务端知道了“客户端能收”

在三次握手的过程中，客户端和服务端也会交换彼此的 <mark style="color:purple;">ISN</mark>（Init Sequense Number，初始化序列号），以便让对方知道后续如何按序列号组装收到的数据。还会交换 TCP 的<mark style="color:purple;">窗口大小</mark>信息。

经过了三次握手，浏览器和服务器之间就建立了全双工通信。有了可靠的 TCP 连接通道之后，HTTP 协议就可以开始工作了。

## 3. 用 HTTP 收发数据

请求方和应答方，都必须依据 HTTP 的规范来构建和解析报文，且在收到 HTTP 报文之后，会给对方回一个 TCP 的确认，表示已经收到了。

比如，浏览器按照 HTTP 协议规定的格式，通过 TCP 发送了一个请求报文 `GET / HTTP/1.1`，即下图中的第 4 个包。随后，web 服务器回复了第 5 个包，是在 TCP 协议的层面对第 4 个包进行了确认“刚才的报文我已经收到了”。

<figure><img src="https://605825044-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVUa7MztTVfoHQxXozBQf%2Fuploads%2Fgvy85ulTY7BZawjM89jW%2Fimage.png?alt=media&#x26;token=7b1daf93-155f-4f20-9fc9-f76558ed17e7" alt=""><figcaption></figcaption></figure>

web 服务器在收到 HTTP 的请求报文之后，也是依据 HTTP 协议的规定，解析报文，然后把处理结果拼接成符合 HTTP 格式的报文，再发回去，即第 6 个包 `HTTP/1.1 200 OK`。同样，在浏览器收到之后，会给服务器一个 TCP 的 `ACK` 确认，即第 7 个包。

> 一对 HTTP 请求 + 响应，一来一回外加两个确认，共四个包。

至此，图中出现的 11 个包（3次握手+2个HTTP请求），交互逻辑大致如下：

![](https://605825044-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVUa7MztTVfoHQxXozBQf%2Fuploads%2FnDInS9xBgQbXpvPHHHKC%2Fimage.png?alt=media\&token=19af3372-04d2-4824-83b9-bf674b097946)

在抓包的截图里，没有出现[关闭 TCP 连接的四次挥手](#5.-guan-bi-tcp-lian-jie)，这是因为 HTTP/1.1 的长连接特性，默认不会立即关闭连接。

## 4. 浏览器渲染页面

浏览器收到响应之后，就开始解析并渲染页面。

1. 解析 HTML，构建 <mark style="color:purple;">DOM 树</mark>，同时根据样式表构建 <mark style="color:purple;">CSSOM 树</mark>
   * 在此过程中浏览器可能还会发起其它 HTTP 请求，比如下载外链 CSS 和 JS 以及图片等资源，它会并行下载页面中的各种资源，其中 HTTP 1.1 每个域的并行数量有限制
   * 若遇 GET 请求还可能会命中浏览器的缓存
2. 将 DOM 树和 CSSOM 树，合并成 <mark style="color:purple;">Render 树</mark>
   * 会忽略非视觉节点，比如元数据、`display:none`
3. 根据 Render 树来进行布局（确定各个元素的位置和尺寸）和渲染

## 5. 关闭 TCP 连接

### 5.1 TCP 的四次挥手

TCP 连接是全双工通信，所以两个方向需要单独关闭。

1. A 给 B 发送一个关闭连接的请求 `FIN`
2. B 回一个同意并确认 `ACK`
3. B 给 A 发送一个关闭连接的请求 `FIN`
4. A 回一个同意并确认 `ACK`

之所以不能像握手时将第 2 个和第 3 个合并成一个 `[ACK, FIN]`，是因为：虽然一方不发数据了，但它要接收的数据可能还没完；而且结束数据传输的指令是由应用层给出的，TCP 也是没法立马回对方一个 `FIN`。

三次握手时的发起者只能是客户端，而四次挥手的发起者可以是任何一方。假设是由客户端发起的关闭 TCP 连接的请求 `FIN`，如下：

![](https://605825044-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVUa7MztTVfoHQxXozBQf%2Fuploads%2FDtbqcQ61RKhjUfGlqsoM%2Fimage.png?alt=media\&token=49ab6a6e-184f-4e6e-8129-556ae0fea977)

1. 客户端给服务器发送 `FIN`，表示从客户端→服务器的通道要关闭了。此时，客户端就不能再向服务器发正常的数据请求了，而服务器还是可以向客户端发送正常的数据请求的
2. 服务器给客户端发送 `ACK`，表示收到了
3. 服务器给客户端发送 `FIN`，表示从服务器→客户端的通道要关闭了
4. 客户端给服务器发送 `ACK`，之后客户端进入 `TIME_WAIT` 状态，然后等 2\*MSL 再进入 `CLOSED` 状态。而当服务端收到该确认之后，就成功变成 `CLOSED` 状态

MSL，Maximum Segment Lifetime。客户端之所以要等 2\*MSL，是因为：要确保 `ACK` 能到达服务器，如果 `ACK` 丢了，服务器会再次发送 `FIN`，这 2\*MSL 就是留给客户端收重传报文的；要等还在路上的报文，要么收到、要么被重传、要么过期，结果就是让本次 connection 中的重复数据都能从网络中消失。如果不等这 2\*MSL 就直接进入 `CLOSED` 状态，假如客户端又再次向服务端发起新的连接且 port 和上次一样，那么新连接就会收到脏数据。

### 5.2 相关的 TCP 知识

TCP，Transmission Control Protocol，传输控制协议。

1. TCP 提供 reliable、ordered、error-checked 的八位字节流传输
2. TCP 是 connection-oriented，在发送 data 前 client 和 server 间的 connection 要建立好
3. 确保 reliability（可靠性）的手段：（有状态，但增加了 latency 延迟）
   * three-way handshake：三次握手
   * ACK + retransmission：确认 + 重传
   * error detection：错误检测
4. flow control（流量控制）和 congestion control（拥塞控制）

结合 TCP 的三次握手和四次挥手，浅浅地感受下 TCP 的状态和报文段首部格式。

![](https://605825044-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVUa7MztTVfoHQxXozBQf%2Fuploads%2F6s4Jpq7cHF3RMPOhAV1k%2Fimage.png?alt=media\&token=a9b3f785-c2ec-4d6f-955e-19724275bd01)![](https://605825044-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVUa7MztTVfoHQxXozBQf%2Fuploads%2FDtbqcQ61RKhjUfGlqsoM%2Fimage.png?alt=media\&token=49ab6a6e-184f-4e6e-8129-556ae0fea977)

#### 5.2.1 TCP 的状态

TCP 协议的操作，可以分为三个阶段：connection establishment、data transfer 以及 connection termination（close connection、release（释放）所有分配的资源）。

TCP connection 由 operating system 通过 Internet socket（套接字）来管理。Internet socket 是 computer network 中 network node 里的软件结构。

随着 TCP/IP 协议的标准化，术语 network socket 通常用在 Internet 协议族的上下文中，此时 socket 会通过三元组的 socket address <mark style="color:purple;">`[transport protocol, IP address, port number]`</mark> 将自己标识给其它 host。

> 术语 socket 也用于 node（节点）内部 IPC（Inter-Process Communication，进程间通信）的 software endpoint（软件端点），它通常使用和 network socket 相同的 API。

TCP socket states 有：

<table><thead><tr><th width="160">state</th><th width="80">client</th><th width="83">server</th><th>说明</th></tr></thead><tbody><tr><td><mark style="color:purple;">CLOSED</mark></td><td>✔</td><td>✔</td><td></td></tr><tr><td><mark style="color:purple;">LISTEN</mark></td><td></td><td>✔</td><td>等待来自任意远程 TCP end-point 的 connection request</td></tr><tr><td>SYN-SENT</td><td>✔</td><td></td><td>等待匹配的 connection request，在发送了一个 connection request 之后</td></tr><tr><td>SYN-RECEIVED</td><td></td><td>✔</td><td>等待确认，在接收和发送了连接请求之后</td></tr><tr><td><mark style="color:purple;">ESTABLISHED</mark></td><td>✔</td><td>✔</td><td>一个 open connection<br>data transfer 阶段的正常状态</td></tr><tr><td>FIN-WAIT-1</td><td>✔</td><td>✔</td><td>等待来自远程 TCP 的 connection termination request，或等待对先前发送的连接终止请求的确认</td></tr><tr><td>FIN-WAIT-2</td><td>✔</td><td>✔</td><td>等待来自远程 TCP 的 connection termination request</td></tr><tr><td>CLOSE-WAIT</td><td>✔</td><td>✔</td><td>等待来自 local user 的 connection termination request</td></tr><tr><td>CLOSING</td><td>✔</td><td>✔</td><td>等待来自远程 TCP 的连接终止请求确认</td></tr><tr><td>LAST-ACK</td><td>✔</td><td>✔</td><td>等待对先前发送到远程 TCP 的连接终止请求的确认</td></tr><tr><td><mark style="color:purple;">TIME-WAIT</mark></td><td>✔</td><td>✔</td><td>等待足够的时间以确保 connection 上的所有剩余  packets 都已过期</td></tr><tr><td><mark style="color:purple;">CLOSED</mark></td><td>✔</td><td>✔</td><td></td></tr></tbody></table>

#### 5.2.2 TCP segment

TCP segment（报文段）由 header 和 data 组成：segment header 包含 10 个必填字段和一个可选的扩展字段（options，即表格中的粉色背景），data 跟在 header 之后，是为 application 携带的 payload data。

segment header 中没有指定 data section 的长度，但它可以算出来，用 IP header 中指定的 IP datagram 总长度减去 IP header 和 segment header 的组合长度。

<figure><img src="https://605825044-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVUa7MztTVfoHQxXozBQf%2Fuploads%2FgmYG5aBNlT01DBAqttrL%2FWX20230208-105436%402x.png?alt=media&#x26;token=897945bb-39d7-437d-b1db-5c2200017d21" alt=""><figcaption></figcaption></figure>

1. source port 和 destination port：源端口和目的端口，各占两个字节
   * 标明了 send port 和 receive port
   * IP address + port number 就可以确定一个进程地址
2. <mark style="color:red;">seq</mark>，sequence number，序列号
   * 若 SYN=1，则是 initial sequence number（初始序列号）
   * 若 SYN=0，则是当前 session 的该段的第一个 data byte 的累积序列号
3. <mark style="color:red;">ack</mark>，acknowledgment number，确认号
   * 若 ACK=1，则值是 ACK 发送方期望的下一个序列号
4. data offset，数据偏移。从 TCP segment（段）开始到实际数据的偏移量
5. reserved，保留
6. 九个 1-bit flags，又称 control bits（控制位）
   1. NS，
   2. CWR，congestion window reduced，拥塞窗口减少标志
   3. ECE，ECN-Echo，Explicit Congestion Notification 显式拥塞通知，Echo 回音
      * 若 SYN=1，则表示 TCP peer（对等端）支持 ECN
      * 若 SYN=0，则表示 IP header 中设置了拥塞经历标志的数据包
   4. URG，紧急位。表示 urgent pointer 字段有用
   5. <mark style="color:red;">ACK</mark>，确认位。表示 acknowledgment 字段有用
   6. PSH，推送位。推送功能，要求将 buffered data（缓冲数据）推送给 application
   7. RST，复位。重新建立连接
   8. <mark style="color:purple;">SYN</mark>，synchronize sequence numbers，同步序列编号。用来建立连接请求
      * SYN = 1 时，不能携带数据
   9. <mark style="color:purple;">FIN</mark>，Finish，终止位。用来释放一个连接
      * 来自 sender 的最后一个 packet
7. window size，窗口大小
8. checksum，校验和&#x20;
9. urgent pointer，紧急指针
10. options，选项。该字段的长度由 data offset 字段决定。为了确保 TCP header 在 32-bit 边界上结束，如需要会用 0 来填充

## 6. 真实的网络环境

真实的网络环境会更复杂，如下：

<figure><img src="https://605825044-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVUa7MztTVfoHQxXozBQf%2Fuploads%2FAupde5RpPIdKBK6kZDJf%2Fimage.png?alt=media&#x26;token=bab3282a-f766-47b9-8886-c46a0560c263" alt=""><figcaption></figcaption></figure>

首先，通过网关接入网络，获得静态或动态的 IP 地址。

其次，DNS 域名解析。

第三，<mark style="color:red;">CDN</mark>，内容分发网络。DNS 解析可能会给出 CDN 服务器的 IP 地址，这样拿到的就是 CDN 服务器而不是目标网站的实际地址。CDN 会<mark style="color:green;">缓存</mark>网站的大部分资源，比如图片、CSS 样式表，所以有的 HTTP 请求就不需要再发到最终的服务器，CDN 就可以直接响应我们的请求。对于不能缓存的资源（比如 POST 请求），CDN 无法缓存，就只能从目标网站上获取。于是 HTTP 请求就开始了在互联网上的漫长跋涉，经过无数的路由器、网关、代理，最后到达目的地。

第四，<mark style="color:red;">服务器</mark>。目标网站的服务器对外表现的是一个 IP 地址，但为了能扛住高并发，其内部也是一套复杂的架构。其中有<mark style="color:purple;">负载均衡设备</mark>（比如四层的 LVS 或七层的 Nginx），通常它们也会有自身的<mark style="color:green;">缓存策略</mark>，比如处理一些静态资源。到达<mark style="color:purple;">应用服务器</mark>（比如 Apache、Nginx），其中<mark style="color:green;">缓存服务器</mark>通常有 memory 级缓存（如 Redis）和 disk 级缓存（如 Varnish），它会把最频繁访问的数据缓存几秒或几分钟，其作用和 CDN 类似，只不过是工作在内部网络里。

第五，<mark style="color:red;">数据库服务</mark>，比如 MySQL, PostgreSQL, MongoDB，它也会有自己的<mark style="color:green;">缓存策略</mark>。

最后，HTTP 的响应数据会按原路返回，最终到达我们的设备。它可能是 HTML、JSON、图片或者其它格式的数据，需要浏览器的解析和处理才能显示出来。如果数据里还有超链接指向别的资源，就会重复类似的流程，直到所有的资源都下载完毕。

为了减少响应时间，整个过程中的每一个环节都有<mark style="color:green;">缓存</mark>实现短路操作。虽然现实中的 HTTP 传输过程非常复杂，但理论上仍然可以简化成“请求方-应答方”的两点模型。
