输入URL后究竟发生了什么?
# 先决条件
本文排除特殊条件,将问题简化为:
- 一个 Chrome 浏览器
- 一台 Linux 服务器
- 发起 HTML 请求
- 不考虑任何缓存和优化机制
- 采用 HTTP/1.1 + TLS/1.2 + TCP 协议
# DNS解析过程
首先,浏览器向本地 DNS 服务器发起请求,由于本地 DNS 服务器没有缓存不能直接将域名转换为 IP 地址,需要采用递归或者迭代查询的方式依次向根域名服务器、顶级域名服务器、权威域名服务器发起查询请求,直至找到一个或一组 IP 地址,返回给浏览器。
一般本地 DNS 地址由 ISP(Internet Service Provider,互联网服务提供商)通过 DHCP 协议动态分配,我们仍可以手动把它修改为公共 DNS,比如 Google 提供的 8.8.8.8,国内的 114.114.114.114,它们分布在不同的地理位置上,借助 Anycast 技术,将请求路由到离用户最近的 DNS 服务器上。为了让 DNS 解析更加精确,客户端还需在请求包里带上自己的源 IP 地址,否则类似 GSLB 的 DNS 服务器不能够精准地匹配判断离用户最近的目标 IP 地址。
# HTTP请求过程
通过 DNS 解析拿到服务器 IP 地址后,浏览器再通过系统调用 Socket 接口与服务器 443 端口进行通信,整个过程可以分解为建立连接、发送 HTTP 请求、返回 HTTP 响应、维持连接、释放连接五个部分。
# 建立连接
在连接建立之前,服务器必须做好接受连接的准备,通过调用 socket、bind、listen 和 accept 四个函数来完成绑定公网 IP、监听 443 端口和接受请求的任务。详细过程参考《TCP连接运输管理详解## TCP连接的建立》
倘若没有发生错误,服务器和浏览器之间将会建立起安全的加密信道。
# 发送 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 虽然都是一种保活机制,但是它们完全不相同,一个作用于应用层,一个作用于传输层。
# 释放连接
详细过程参考《TCP连接运输管理详解##TCP连接的释放》
# 浏览器解析过程
浏览器解析HTML是一个复杂的流程,读者可参考[浏览器渲染原理及流程],本文不做详细介绍。