当在浏览器键入URL后会发生哪些事情(网络篇)?

2019/10/11 36

在上篇文章 当在浏览器键入URL后会发生哪些事情(浏览器篇)? 中,仅仅讲解了开始和结尾浏览器发生了什么,而对网络通信并没有说明,这篇文章主要是讲解请求如何到达服务端。

DNS 域名解析

上文 当在浏览器键入URL后会发生哪些事情(浏览器篇)? 提到过,浏览器会分析出用户输入的网址,如果是 http/https 的协议,浏览器就能分析出 Host、Port、Path 等信息,继而进行网络请求,如果 Host 是域名而不是 IP 地址,则要进行域名解析,得到对应的 IP 地址进行请求。

首先,计算机会去找本地 host 配置文件,检查是否有响应的域名,如果有,则用配置中的 IP 地址,如果没有,再去本地 DNS 服务器中查询,如果仍然没要找到,需要采用递归或迭代查询的方式一次向根域名服务器、顶级域名服务器、权威域名服务器发起查询请求,知道找到一个或一组 IP 地址,返回给浏览器。一般情况下本地 DNS 地址由 ISP(Internet Service Provider,互联网服务提供商)通过 DHCP 协议动态分派,我们仍可以手动把它修改为公共 DNS,比如 Google 提供的 8.8.8.8,国内的 114.114.114.114,它们分布在不同的地理位置上,借助 Anycast 技术,将请求路由由离用户最近的 DNS 服务器上。为了让 DNS 解析更加精确,客户端还需在请求包里带上自己的源 IP 地址,否则类似 GSLB 的 DNS 服务器不能精准判断离用户最近的 DNS 服务器。

DNS 查询过程

建立 TCP 连接

经过一番周折后,终于拿到了服务器 IP 了,下一步自然是连接到服务器。HTTP 协议最终还是通过 TCP 协议进行传输数据的,所以也会进行三次握手。

TCP 握手过程

详细连接过程

  1. 在连接建立之前,服务器必须做好接受连接的准备,通过调用 socket、bind、listen 和 accept 四个函数来完成绑定公网 IP、监听 443 端口和接受请求的任务。
  2. 客户端通过 socket 和 connect 两个函数来主动打开连接,给服务器发送带有 SYN 标志位的分组,随机生成一个初始序列号 x,以及附带 MSS(Maximum Segment Size,最大段大小)等额外信息。为了避免在网络层被 IP 协议分片使得出现丢失错误的概率增加,及达到最佳的传输效果,MSS 的值一般为以太网 MTU(Maximum Transmission Unit,最大传输单元)的值减去 IP 头部和 TCP 头部大小,等于 1460 字节。
  3. 服务器必须确认收到客户端的分组,发送带有 SYN+ACK 标志位的分组,随机生成一个初始序列号 y,确认号为 x+1,以及附带 MSS 等额外信息。当一端收到另外一端的 MSS 值时,会根据两者的 MSS 取最小值来决定随后的 TCP 最大报文段大小。
  4. 客户端确认收到服务器的分组,发送带有 ACK 标志位的分组,确认号为 y+1,从而建立 TCP 连接。
  5. 如果客户端此前未与服务器建立会话,那么双方需要进行一次完整的 TLS 四次握手。客户端首先向服务器发送 Client Hello 报文,包含一个随机数、TLS 协议版本、按优先级排列的加密套件列表。
  6. 服务器向客户端发送 Server Hello 报文,包含一个新的随机数、TLS 协议版本、经过选择后的一个加密套件。
  7. 服务器向客户端发送 Certificate 报文,包含服务器 X.509 证书链,其中,第一个为主证书,中间证书按照顺序跟在主证书之后,而根 CA 证书通常内置在操作系统或浏览器中,无需服务器发送。
  8. 如果密钥交换选择 DH 算法,服务器会向客户端发送 Server Key Exchange 报文,包含密钥交换所需的 DH 参数;如果密钥交换选择 RSA 算法,则跳过这一步。
  9. 服务器向客户端发送 Server Hello Done 报文,表明已经发送完所有握手消息。
  10. 客户端向服务器发送 Client Key Exchange 报文,如果密钥交换选择 RSA 算法,由客户端生成预主密钥,使用服务器证书中的公钥对其加密,包含在报文中,服务器只需使用自己的私钥解密就可以取出预主密钥;如果密钥交换选择 DH 算法,客户端会在报文中包含自己的 DH 参数,之后双方都根据 DH 算法计算出相同的预主密钥。需要注意的是,密钥交换的只是预主密钥,这个值还需进一步加工,结合客户端和服务器两个随机数种子,双方使用 PRF(pseudorandom function,伪随机函数)生成相同的主密钥。
  11. 客户端向服务器发送 Change Cipher Spec 报文,表明已经生成主密钥,在随后的传输过程都使用这个主密钥对消息进行对称加密。
  12. 客户端向服务器发送 Finished 报文,这条消息是经过加密的,因此在 Wireshark 中显示的是 Encrypted Handshake Message。如果服务器能解密出报文内容,则说明双方生成的主密钥是一致的。
  13. 服务器向客户端发送 New Session Ticket 报文,而这个 Session Ticket 只有服务器才能解密,客户端把它保存下来,在以后的 TLS 重新握手过程中带上它进行快速会话恢复,减少往返延迟。
  14. 服务器向客户端发送 Change Cipher Spec 报文,同样表明已经生成主密钥,在随后的传输过程都使用这个主密钥对消息进行对称加密。
  15. 服务器向客户端发送 Finished 报文,如果客户端能解密出报文内容,则说明双方生成的主密钥是一致的。至此,完成所有握手协商。

发送 HTTP 请求

建立起安全的加密信道后,浏览器开始发送 HTTP 请求,一个请求报文由请求行、请求头、空行、实体(Get 请求没有)组成。请求头由通用首部、请求首部、实体首部、扩展首部组成。其中,通用首部表示无论是请求报文还是响应报文都可以使用,比如 Date;请求首部表示只有在请求报文中才有意义,分为 Accept 首部、条件请求首部、安全请求首部和代理请求首部这四类;实体首部作用于实体内容,分为内容首部和缓存首部这两类;扩展首部表示用户自定义的首部,通过 X- 前缀来添加。另外需要注意的是,HTTP 请求头是不区分大小写的,它基于 ASCII 进行编码,而实体可以基于其它编码方式,由 Content-Type 决定。

返回 HTTP 响应

服务器接受并处理完请求,返回 HTTP 响应,一个响应报文格式基本等同于请求报文,由响应行、响应头、空行、实体组成。区别于请求头,响应头有自己的响应首部集,比如 Vary、Set-Cookie,其它的通用首部、实体首部、扩展首部则共用。此外,浏览器和服务器必须保证 HTTP 的传输顺序,各自维护的队列中请求/响应顺序必须一一对应,否则会出现乱序而出错的情况。

维持连接

完成一次 HTTP 请求后,服务器并不是马上断开与客户端的连接。在 HTTP/1.1 中,Connection: keep-alive 是默认启用的,表示持久连接,以便处理不久后到来的新请求,无需重新建立连接而增加慢启动开销,提高网络的吞吐能力。在反向代理软件 Nginx 中,持久连接超时时间默认值为 75 秒,如果 75 秒内没有新到达的请求,则断开与客户端的连接。同时,浏览器每隔 45 秒会向服务器发送 TCP keep-alive 探测包,来判断 TCP 连接状况,如果没有收到 ACK 应答,则主动断开与服务器的连接。注意,HTTP keep-alive 和 TCP keep-alive 虽然都是一种保活机制,但是它们完全不相同,一个作用于应用层,一个作用于传输层。

断开连接

  1. 服务器向客户端发送 Alert 报文,类型为 Close Notify,通知客户端不再发送数据,即将关闭连接,同样,这条报文也是经过加密处理的。
  2. 服务器通过调用 close 函数主动关闭连接,向客户端发送带有 FIN 标志位的分组,序列号为 m。
  3. 客户端确认收到该分组,向服务器发送带有 ACK 标志位的分组,确认号为 m+1。
  4. 客户端发送完所有数据后,向服务器发送带有 FIN 标志位的分组,序列号为 n。
  5. 服务器确认收到该分组,向客户端发送带有 ACK 标志位的分组,序列号为 n+1。客户端收到确认分组后,立即进入 CLOSED 状态;同时,服务器等待 2 个 MSL(Maximum Segment Lifetime,最大报文生存时间) 的时间后,进入 CLOSED 状态。

到此为止,所有的数据都已经返回给客户端浏览器了,接下来浏览器还需要进行解析处理。那么浏览器是如何进行后续处理的呢?在上篇文章 当在浏览器键入URL后会发生哪些事情(浏览器篇)? 中由详细介绍

参考文献:知乎专栏

评论