引言
当我们在浏览器地址栏输入一个网址,按下回车键,到页面完全展示在我们面前,这短短的几秒钟内,到底发生了什么?作为一名前端开发者,理解这个过程不仅能帮助我们更好地优化应用性能,还能在遇到问题时快速定位症结所在。今天,就让我们一起深入探索这段奇妙的旅程。
从输入网址到页面显示:完整流程解析
1. URL解析与输入处理
当用户在地址栏输入内容时,浏览器会智能地判断输入的是搜索关键词还是网址。如果是关键词且不是完整的URL,浏览器会使用默认搜索引擎进行搜索。
2. DNS解析
当输入URL地址后,浏览器需要知道该地址对应的目标服务器的IP,才能建立连接,这个过程称为DNS解析,其主要查询过程如下:
- 浏览器缓存查找
- 操作系统缓存查找
- 本地hosts文件查找
- 路由器缓存查找
- ISP(互联网服务提供商)DNS服务器查询:
- ⾸先在本地的域名服务器中查找
- 如果没找到,则去根域名服务器查找
- 根域名没有,再去com顶级域名服务器查找
- ⼤致过程:.-> .com. ->baidu.com. -> www.baidu.com
3. TCP连接建立(三次握手)
TCP连接建立后面的章节会专门介绍。
4. TLS/SSL协商(HTTPS情况)
如果使用HTTPS,在TCP连接建立后还会进行TLS握手,协商加密算法、交换证书、生成会话密钥。
5. HTTP请求发送
浏览器构造HTTP请求报文,包含:
- 请求行(方法、路径、协议版本)
- 请求头(User-Agent、Accept、Cookie等)
- 请求体(POST、PUT等方法)
6. 服务器处理与响应
服务器接收请求后,经过后端处理(Java示例):
@RestController
public class UserController {
@GetMapping("/api/user")
public ResponseEntity<User> getUser(@RequestParam String id) {
try {
User user = userService.findById(id);
return ResponseEntity.ok(user);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
7. 浏览器渲染
浏览器接收到响应后开始渲染:
- 解析HTML构建DOM树
- 解析CSS构建CSSOM树
- 合并生成渲染树
- 布局计算元素位置
- 绘制页面像素
- 合成显示
从发送请求到接收响应:中间件视角
请求流程
- 应用层:浏览器构建HTTP请求
- 传输层:TCP将数据分割成数据段,添加端口信息
- 网络层:IP协议添加源和目标IP地址
- 链路层:添加MAC地址,通过物理设备发送
响应流程
- 链路层:接收数据帧,检查MAC地址
- 网络层:IP协议检查目标IP,处理分片重组
- 传输层:TCP检查端口,处理数据排序、确认和重传
- 应用层:浏览器接收HTTP响应数据
HTTP/TCP连接的建立与断开
三次握手详解
三次握手过程
第一次握手:客户端向服务器发送SYN报文
- 客户端随机生成一个初始序列号seq=x
- 设置SYN标志位为1,表示请求建立连接
- 客户端进入SYN_SENT状态,等待服务器确认
第二次握手:服务器收到SYN报文后
- 服务器随机生成自己的初始序列号seq=y
- 确认号ack=x+1,表示期望收到客户端下一个报文
- 设置SYN和ACK标志位为1
- 服务器进入SYN_RCVD状态,等待客户端确认
第三次握手:客户端收到服务器的SYN-ACK报文后
- 确认号ack=y+1,表示收到服务器的序列号
- 设置ACK标志位为1
- 客户端进入ESTABLISHED状态
- 服务器收到这个ACK报文后,也进入ESTABLISHED状态
三次握手图示
客户端 服务器
|--------SYN, seq=x-------------->|
|<-----SYN, ACK, seq=y, ack=x+1---|
|--------ACK, ack=y+1------------>|
思考题:如果只有两次握手会发生什么?
假设只有两次握手,即客户端发送SYN后,服务器回复SYN-ACK就认为连接已建立,这会导致以下问题:
问题1:已失效的连接请求造成资源浪费
// 场景模拟:网络延迟导致的重传
场景:客户端发送SYN-1(seq=100)
↓ 网络延迟(SYN-1卡在网络中)
客户端超时重传SYN-2(seq=200)
↓
服务器收到SYN-2,回复SYN-ACK(ack=201)
连接建立成功,数据传输完成,连接关闭
↓
此时SYN-1终于到达服务器
服务器以为这是新连接请求,回复SYN-ACK(ack=101)
服务器资源被白白占用,等待永远不会来的ACK
问题2:无法防止历史连接的初始化
服务器无法验证客户端发送的SYN是否过期,盲目分配资源;同时服务器在二次握手时,会认为连接建立,但此时客户端可能根本没想连接。
四次挥手详解
四次挥手过程
第一次挥手:客户端发送FIN报文
- 客户端随机生成一个序列号seq=u(通常是之前已传送数据的最后一个字节的序号加1)
- 设置FIN标志位为1,表示请求断开连接
- 客户端进入FIN_WAIT_1状态,等待服务器的确认
- 此时客户端不能再发送数据,但可以接收数据
第二次挥手:服务器发送ACK报文
- 服务器收到客户端的FIN报文后,发送确认报文
- 确认号ack=u+1,表示已收到客户端的FIN报文
- 服务器自己的序列号seq=v(服务器正常发送数据时的序列号)
- 设置ACK标志位为1
- 服务器进入CLOSE_WAIT状态(等待关闭状态)
- 客户端收到确认后进入FIN_WAIT_2状态
- 此时连接处于半关闭状态:客户端到服务器的方向关闭,但服务器到客户端的方向仍可传输数据
第三次挥手:服务器发送FIN报文
- 服务器处理完剩余数据后,发送FIN报文准备关闭连接
- 序列号seq=w(如果第二次挥手后有发送数据,则w=v+已发送数据长度;否则w=v)
- 确认号仍为ack=u+1(确认客户端的FIN)
- 设置FIN和ACK标志位为1
- 服务器进入LAST_ACK状态,等待客户端的最终确认
- 此时服务器表示自己也没有数据要发送了
第四次挥手:客户端发送ACK报文
- 客户端收到服务器的FIN报文后,发送确认报文
- 确认号ack=w+1,表示已收到服务器的FIN报文
- 自己的序列号seq=u+1(对应第一次挥手的序列号)
- 设置ACK标志位为1
- 客户端进入TIME_WAIT状态,等待2MSL(Maximum Segment Lifetime,最大报文段生存时间)后才进入CLOSED状态
- 服务器收到确认后立即进入CLOSED状态
- 2MSL过后,客户端自动进入CLOSED状态
四次挥手图示
客户端 服务器
|--------FIN, seq=u---------->| (客户端主动关闭)
|<-------ACK, ack=u+1---------| (服务器确认)
|<-------FIN, seq=v-----------| (服务器也准备关闭)
|--------ACK, ack=v+1-------->| (客户端确认)
四次挥手关键点说明
为什么是四次而不是三次?
- 因为TCP连接是全双工的,每个方向都需要单独关闭
- 服务器收到客户端的FIN时,可能还有数据要发送,所以先回复ACK,等数据发完再发FIN
TIME_WAIT状态的作用
- 确保最后一个ACK能到达服务器(如果ACK丢失,服务器会重发FIN)
- 等待2MSL时间,让旧连接的所有报文都从网络中消失,避免影响新连接
2MSL的含义
- MSL:报文最大生存时间(通常30秒到2分钟)
- 2MSL:保证至少一次报文往返的时间
- 确保最后的ACK能被对方收到,如果丢失还有重传的机会
可手动取消的浏览器请求
XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.send();
// 手动取消
xhr.abort();
Fetch API
const controller = new AbortController();
const signal = controller.signal;
fetch('/api/data', { signal })
.then(response => response.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已取消');
}
});
// 手动取消
controller.abort();
Axios
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/api/data', {
cancelToken: source.token
}).catch(thrown => {
if (axios.isCancel(thrown)) {
console.log('请求已取消:', thrown.message);
}
});
// 手动取消
source.cancel('用户取消了请求');
结语
从输入网址到页面显示,这看似简单的过程背后,蕴含着网络通信的诸多智慧。理解这些底层原理,不仅能帮助我们写出更可靠的代码,还能在性能优化和问题排查时事半功倍。
对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!