> For the complete documentation index, see [llms.txt](https://docs.junyangz.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.junyangz.com/note/what-happens-when-zh_cn.md).

# What happens when zh\_CN

一个古老的面试问题：当你在浏览器中输入 google.com 并且按下回车之后发生了什么？

## 按下"g"键

接下来的内容介绍了物理键盘和系统中断的工作原理，但是有一部分内容却没有涉及。当你按下“g”键，浏览器接收到这个消息之后，会触发自动完成机制。浏览器根据自己的算法，以及你是否处于隐私浏览模式，会在浏览器的地址框下方给出输入建议。大部分算法会优先考虑根据你的搜索历史和书签等内容给出建议。你打算输入 "google.com"，因此给出的建议并不匹配。但是输入过程中仍然有大量的代码在后台运行，你的每一次按键都会使得给出的建议更加准确。甚至有可能在你输入之前，浏览器就将 "google.com" 建议给你。

\===========================

键盘中断

事件传递

解析URL

\===========================

## 检查HSTS列表

* 浏览器检查自带的“预加载 HSTS（HTTP严格传输安全）”列表，这个列表里包含了那些请求浏览器只使用HTTPS进行连接的网站
* 如果网站在这个列表里，浏览器会使用 HTTPS 而不是 HTTP 协议，否则，最初的请求会使用HTTP协议发送
* 注意，一个网站哪怕不在 HSTS 列表里，也可以要求浏览器对自己使用 HSTS 政策进行访问。浏览器向网站发出第一个 HTTP 请求之后，网站会返回浏览器一个响应，请求浏览器只使用 HTTPS 发送请求。然而，就是这第一个 HTTP 请求，却可能会使用户受到 [downgrade attack](https://en.wikipedia.org/wiki/Downgrade_attack) 的威胁，这也是为什么现代浏览器都预置了HSTS 列表。

  > 降级攻击：故意使系统放弃新式、安全性高的工作方式（如加密连接），反而使用为向下兼容而准备的老式、安全性差的工作方式（如明文通讯）。
  >
  > Chrome HSTS列表 <chrome://net-internals/#hsts>

## DNS查询

* 浏览器检查域名是否在缓存当中（要查看 Chrome 当中的缓存， 打开 <chrome://net-internals/#dns>）。
* 如果缓存中没有，就去调用 `gethostbyname` 库函数（操作系统不同函数也不同）进行查询。
* `gethostbyname` 函数在试图进行DNS解析之前首先检查域名是否在本地 Hosts 里，Hosts 的位置 [不同的操作系统有所不同](https://en.wikipedia.org/wiki/Hosts_\(file\)#Location_in_the_file_system)
  * **Linux/Unix**: /etc/hosts
  * **Microsoft Windows**: [%SystemRoot%](https://www.wikiwand.com/en/Environment_variable#Windows)\System32\drivers\etc\hosts
* 如果 `gethostbyname` 没有这个域名的缓存记录，也没有在 `hosts` 里找到，它将会向 DNS 服务器发送一条 DNS 查询请求。DNS 服务器是由网络通信栈提供的，通常是本地路由器或者 ISP 的缓存 DNS 服务器。
* 查询本地 DNS 服务器
* 如果 DNS 服务器和我们的主机在同一个子网内，系统会按照下面的 ARP 过程对 DNS 服务器进行 ARP查询
* 如果 DNS 服务器和我们的主机在不同的子网，系统会按照下面的 ARP 过程对默认网关进行查询

## ARP 过程

要想发送 ARP（地址解析协议）广播，我们需要有一个目标 IP 地址，同时还需要知道用于发送 ARP 广播的接口的 MAC 地址。

* 首先查询 ARP 缓存，如果缓存命中，我们返回结果：目标 IP = MAC

如果缓存没有命中：

* 查看路由表，看看目标 IP 地址是不是在本地路由表中的某个子网内。是的话，使用跟那个子网相连的接口，否则使用与默认网关相连的接口。
* 查询选择的网络接口的 MAC 地址
* 我们发送一个二层（ [OSI 模型](https://en.wikipedia.org/wiki/OSI_model) 中的数据链路层）ARP 请求：

  `ARP Request`:

  ```
  Sender MAC: interface:mac:address:here
  Sender IP: interface.ip.goes.here
  Target MAC: FF:FF:FF:FF:FF:FF (Broadcast)
  Target IP: target.ip.goes.here
  ```

根据连接主机和路由器的硬件类型不同，可以分为以下几种情况：

直连：

* 如果我们和路由器是直接连接的，路由器会返回一个 `ARP Reply` （见下面）。

集线器：

* 如果我们连接到一个集线器，集线器会把 ARP 请求向所有其它端口广播，如果路由器也“连接”在其中，它会返回一个 `ARP Reply` 。

交换机：

* 如果我们连接到了一个交换机，交换机会检查本地 CAM/MAC 表，看看哪个端口有我们要找的那个 MAC 地址，如果没有找到，交换机会向所有其它端口广播这个 ARP 请求。
* 如果交换机的 MAC/CAM 表中有对应的条目，交换机会向有我们想要查询的 MAC 地址的那个端口发送 ARP 请求
* 如果路由器也“连接”在其中，它会返回一个 `ARP Reply`

  `ARP Reply`:

  ```
  Sender MAC: target:mac:address:here
  Sender IP: target.ip.goes.here
  Target MAC: interface:mac:address:here
  Target IP: interface.ip.goes.here
  ```

现在我们有了 DNS 服务器或者默认网关的 IP 地址，我们可以继续 DNS 请求了：

* 使用 53 端口向 DNS 服务器发送 UDP 请求包，如果响应包太大，会使用 TCP 协议

  > DNS 查询响应报文大于 512 字节时。
  >
  > DNS 主、辅助服务器之间，进行区域传送时。（辅服务器从主服务器同步信息的动作）
  >
  > 客户端主动发起TCP的DNS查询请求
* 如果本地/ISP DNS 服务器没有找到结果，它会发送一个递归查询请求，一层一层向高层 DNS 服务器做查询，直到查询到起始授权机构，如果找到会把结果返回。

## 使用套接字

当浏览器得到了目标服务器的 IP 地址，以及 URL 中给出来端口号（http 协议默认端口号是 80， https 默认端口号是 443），它会调用系统库函数 `socket` ，请求一个 TCP流套接字，对应的参数是 `AF_INET/AF_INET6` 和 `SOCK_STREAM` 。

* 这个请求首先被交给传输层，在传输层请求被封装成 TCP segment。目标端口会被加入头部，源端口会在系统内核的动态端口范围内选取（Linux下是ip\_local\_port\_rang, default: 32768 60999)
* TCP segment 被送往网络层，网络层会在其中再加入一个 IP 头部，里面包含了目标服务器的IP地址以及本机的IP地址，把它封装成一个IP packet。
* 这个 TCP packet 接下来会进入链路层，链路层会在封包中加入 frame 头部，里面包含了本地内置网卡的MAC地址以及网关（本地路由器）的 MAC 地址。像前面说的一样，如果内核不知道网关的 MAC 地址，它必须进行 ARP 广播来查询其地址。

到了现在，TCP 封包已经准备好了，可以使用下面的方式进行传输：

* [以太网](http://en.wikipedia.org/wiki/IEEE_802.3)
* [WiFi](https://en.wikipedia.org/wiki/IEEE_802.11)
* [蜂窝数据网络](https://en.wikipedia.org/wiki/Cellular_data_communication_protocol)

对于大部分家庭网络和小型企业网络来说，封包会从本地计算机出发，经过本地网络，再通过调制解调器把数字信号转换成模拟信号，使其适于在电话线路，有线电视光缆和无线电话线路上传输。在传输线路的另一端，是另外一个调制解调器，它把模拟信号转换回数字信号，交由下一个 [网络节点](https://en.wikipedia.org/wiki/Computer_network#Network_nodes) 处理。节点的目标地址和源地址将在后面讨论。

大型企业和比较新的住宅通常使用光纤或直接以太网连接，这种情况下信号一直是数字的，会被直接传到下一个 [网络节点](https://en.wikipedia.org/wiki/Computer_network#Network_nodes) 进行处理。

最终封包会到达管理本地子网的路由器。在那里出发，它会继续经过[自治区域](https://www.wikiwand.com/zh-hans/傳輸層安全性協定)(autonomous system, 缩写 AS)的边界路由器，其他自治区域，最终到达目标服务器。一路上经过的这些路由器会从IP数据报头部里提取出目标地址，并将封包正确地路由到下一个目的地。IP数据报头部 time to live (TTL) 域的值每经过一个路由器就减1，如果封包的TTL变为0，或者路由器由于网络拥堵等原因封包队列满了，那么这个包会被路由器丢弃。

上面的发送和接受过程在[TCP](https://www.wikiwand.com/zh-hans/%E4%BC%A0%E8%BE%93%E6%8E%A7%E5%88%B6%E5%8D%8F%E8%AE%AE)连接期间会发生很多次(SYN, *SYN-ACK*, ACK)：

* 客户端选择一个初始序列号(Initial sequence numbers, ISN)，将设置了 SYN 位的封包发送给服务器端，表明自己要建立连接并设置了初始序列号
* 服务器端接收到 SYN 包，如果它可以建立连接：
  * 服务器端选择它自己的初始序列号
  * 服务器端设置 SYN 位，表明自己选择了一个初始序列号
  * 服务器端把 (客户端ISN + 1) 复制到 ACK 域，并且设置 ACK 位，表明自己接收到了客户端的第一个封包
* 客户端通过发送下面一个封包来确认这次连接：
  * 自己的序列号+1
  * 接收端 ACK+1
  * 设置 ACK 位
* 数据通过下面的方式传输：
  * 当一方发送了N个 Bytes 的数据之后，将自己的 SEQ 序列号也增加N
  * 另一方确认接收到这个数据包（或者一系列数据包）之后，它发送一个 ACK 包，ACK 的值设置为接收到的数据包的最后一个序列号
* 关闭连接时(FIN, ACK -- FIN, ACK)：
  * 要关闭连接的一方发送一个 FIN 包
  * 另一方确认这个 FIN 包，并且发送自己的 FIN 包
  * 要关闭的一方使用 ACK 包来确认接收到了 FIN

## TLS 握手

* 客户端发送一个 `ClientHello` 消息到服务器端，消息中同时包含了它的 [Transport Layer Security (TLS)](https://www.wikiwand.com/zh/%E5%82%B3%E8%BC%B8%E5%B1%A4%E5%AE%89%E5%85%A8%E6%80%A7%E5%8D%94%E5%AE%9A) 版本，可用的加密算法和压缩算法。
* 服务器端向客户端返回一个 `ServerHello` 消息，消息中包含了服务器端的TLS版本，服务器所选择的加密和压缩算法，以及数字证书认证机构（Certificate Authority，缩写 CA）签发的服务器公开证书，证书中包含了公钥。客户端会使用这个公钥加密接下来的握手过程，直到协商生成一个新的对称密钥
* 客户端根据自己的信任CA列表，验证服务器端的证书是否可信。如果认为可信，客户端会生成一串伪随机数，使用服务器的公钥加密它。这串随机数会被用于生成新的对称密钥
* 服务器端使用自己的私钥解密上面提到的随机数，然后使用这串随机数生成自己的对称主密钥
* 客户端发送一个 `Finished` 消息给服务器端，使用对称密钥加密这次通讯的一个散列值
* 服务器端生成自己的 hash 值，然后解密客户端发送来的信息，检查这两个值是否对应。如果对应，就向客户端发送一个 `Finished` 消息，也使用协商好的对称密钥加密
* 从现在开始，接下来整个 TLS 会话都使用对称秘钥进行加密，传输应用层（HTTP）内容

## HTTP 协议

如果浏览器是 Google 出品的，它不会使用 HTTP 协议来获取页面信息，而是会与服务器端发送请求，商讨使用 [SPDY](https://www.wikiwand.com/zh/SPDY) 协议(HTTP/2前身)。

如果浏览器使用 HTTP 协议而不支持 SPDY 协议，它会向服务器发送这样的一个请求:

```
GET / HTTP/1.1
Host: google.com
Connection: close
[其他头部]
```

“其他头部”包含了一系列的由冒号分割开的键值对，它们的格式符合HTTP协议标准，它们之间由一个换行符分割开来。（这里我们假设浏览器没有违反HTTP协议标准的bug，同时假设浏览器使用 `HTTP/1.1` 协议，不然的话头部可能不包含 `Host` 字段，同时 `GET` 请求中的版本号会变成 `HTTP/1.0` 或者 `HTTP/0.9` 。）

HTTP/1.1 定义了“关闭连接”的选项 "close"，发送者使用这个选项指示这次连接在响应结束之后会断开。例如：

> Connection:close

不支持持久连接的 HTTP/1.1 应用必须在每条消息中都包含 "close" 选项。

在发送完这些请求和头部之后，浏览器发送一个换行符，表示要发送的内容已经结束了。

服务器端返回一个响应码，指示这次请求的状态，响应的形式是这样的:

```
200 OK
[响应头部]
```

然后是一个换行，接下来有效载荷(payload)，也就是 `www.google.com` 的HTML内容。服务器下面可能会关闭连接，如果客户端请求保持连接的话，服务器端会保持连接打开，以供之后的请求重用。

如果浏览器发送的HTTP头部包含了足够多的信息（例如包含了 Etag 头部），以至于服务器可以判断出，浏览器缓存的文件版本自从上次获取之后没有再更改过，服务器可能会返回这样的响应:

```
304 Not Modified
[响应头部]
```

这个响应没有有效载荷，浏览器会从自己的缓存中取出想要的内容。

在解析完 HTML 之后，浏览器和客户端会重复上面的过程，直到HTML页面引入的所有资源（图片，CSS，favicon.ico等等）全部都获取完毕，区别只是头部的 `GET / HTTP/1.1` 会变成 `GET /$(相对www.google.com的URL) HTTP/1.1` 。

如果HTML引入了 `www.google.com` 域名之外的资源，浏览器会回到上面解析域名那一步，按照下面的步骤往下一步一步执行，请求中的 `Host` 头部会变成另外的域名。

## HTTP 服务器请求处理

HTTPD(HTTP Daemon)在服务器端处理请求/响应。最常见的 HTTPD 有 Linux 上常用的 Apache 和 nginx，以及 Windows 上的 IIS。

* HTTPD 接收请求
* 服务器把请求拆分为以下几个参数：
  * HTTP 请求方法(`GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, 或者 `TRACE`)。直接在地址栏中输入 URL 这种情况下，使用的是 GET 方法
  * 域名：google.com
  * 请求路径/页面：/ (我们没有请求google.com下的指定的页面，因此 / 是默认的路径)
* 服务器验证其上已经配置了 google.com 的虚拟主机
* 服务器验证 google.com 接受 GET 方法
* 服务器验证该用户可以使用 GET 方法(根据 IP 地址，身份信息等)
* 如果服务器安装了 URL 重写模块（例如 Apache 的 mod\_rewrite 和 IIS 的 URL Rewrite），服务器会尝试匹配重写规则，如果匹配上的话，服务器会按照规则重写这个请求
* 服务器根据请求信息获取相应的响应内容，这种情况下由于访问路径是 "/" ,会访问首页文件（你可以重写这个规则，但是这个是最常用的）。
* 服务器会使用指定的处理程序分析处理这个文件，假如 Google 使用 PHP，服务器会使用 PHP 解析 index 文件，并捕获输出，把 PHP 的输出结果返回给请求者

## 浏览器背后的故事

当服务器提供了资源之后（HTML，CSS，JS，图片等），浏览器会执行下面的操作：

* 解析 —— HTML，CSS，JS
* 渲染 —— 构建 DOM 树 -> 渲染 -> 布局 -> 绘制

## Reference

* [**what-happens-when-zh\_CN**](https://github.com/skyline75489/what-happens-when-zh_CN)
