HTTP 协议底层 TCP 传输原理——从输入 URL 到看到页面发生了什么?
前言
HTTP(超文本传输协议)是万维网的基石,打开浏览器就离不开它。但 HTTP 位于应用层,真正帮它把数据从服务器搬到浏览器的是传输层的 TCP。
在浏览器输入网址并按下回车的那一瞬间,实际发生的不是"请求-响应"这么简单,而是一条完整的链路:DNS 解析 → TCP 三次握手 → HTTP 请求报文 → HTTP 响应报文 → TCP 四次挥手。
由于涉及大量网络协议,可以非常直观地考察对计算机网络体系的整体把握程度,所以自然成为了各大公司的面试常客。
下面我们就来详细拆解这个网络通信过程。
1. DNS 解析:域名怎么变成 IP
互联网上的每一台主机都有唯一的 IP 地址,但 IP 地址是一串数字,比如 110.242.68.66,没人愿意记住它。
于是有了域名系统(DNS)。你在浏览器输入 baidu.com,浏览器会先向 DNS 服务器查询这个域名对应的 IP 地址,然后用 IP 去建立连接。在浏览器按 F12 打开开发者工具,Network 面板里能看到每个请求对应的真实 IP。
DNS 查询的简化流程:
配合下图直观理解:
浏览器缓存 → 系统缓存 → 本地域名服务器 → 根域名服务器 → 顶级域名服务器 → 权限域名服务器
需要注意的是,DNS 使用的是 UDP 协议,也就是说上面各种请求的转发,都是基于 UDP 这个无连接协议的。
2. TCP 三次握手:为 HTTP 报文铺路
HTTP 选择 TCP 作为传输层协议,是因为 HTTP 要求数据完整、有序到达,而 TCP 的确认应答和重传机制正好满足这个需求。
在 HTTP 报文传输之前,TCP 必须先完成三次握手,确认双方的收发能力都正常:
三次握手之后,双方进入 ESTABLISHED 状态,HTTP 报文就可以开始传输了。
用 Wireshark 抓包观察一次最简单的 HTTP 请求-响应,TCP 协议一共收发了 11 个包,可以归纳为 4 个过程:
除去握手和挥手,真正传输 HTTP 数据的只有 4 个包——浏览器发送请求报文、服务器回复 ACK、服务器发送响应报文、浏览器回复 ACK。
3. HTTP 请求报文:浏览器怎么向服务器要数据
HTTP 请求报文由三部分组成:起始行、头部字段、消息正文。
起始行是请求行,包含三个要素:
- 请求方法:一个动词,GET 获取资源,POST 提交数据,PUT 更新,DELETE 删除
- 请求目标:URI,标记要操作的资源路径
- 协议版本:HTTP/1.1
一个完整的请求行长这样:
GET /index HTTP/1.1
这行指令和你在浏览器地址栏输入 http://127.0.0.1:80/index 是等价的。
头部字段以 key-value 格式描述请求的附加信息,比如客户端接受的编码格式、语言偏好等,这里不展开。消息正文在 GET 请求中通常为空,POST 请求则携带表单数据或 JSON。
下面用 Go net 包发送一个最小的 HTTP GET 请求,直观感受一下:
conn, err := net.Dial("tcp", "localhost:8001")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// 发送 HTTP 请求行
conn.Write([]byte("GET /index HTTP/1.1\\r\\n\\r\\n"))
// 关闭写端,告诉服务器"我发完了"
conn.(*net.TCPConn).CloseWrite()
// 读取服务器响应
buffer := make([]byte, 2048)
length, err := conn.Read(buffer)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(buffer[:length]))
关键点拆解:
- net.Dial(“tcp”, “localhost:8001”) 建立 TCP 连接到服务器的 8001 端口
- conn.Write(…) 把 HTTP 请求行以字节流形式写出去,底层走的就是 TCP
- CloseWrite() 告诉服务器"我发完了",服务器才会开始处理并返回响应
- 响应也是一个字节流,通过 conn.Read() 接收
你在浏览器输入 URL 的过程,本质上就是这段代码在底层做的事。
4. HTTP 响应报文:服务器怎么把页面返回
服务器收到 HTTP 请求后,解析请求行,找到对应的资源,构造响应报文返回。
响应报文同样由三部分组成:起始行(响应行)、头部字段、消息正文。
响应行包含三个要素:
- 协议版本:HTTP/1.1
- 状态码:三位数字,200 表示成功,404 表示资源不存在,500 表示服务器内部错误
- 原因短语:状态码的文字说明,比如 OK、Not Found
一个让开发者最安心的响应行:
HTTP/1.1 200 OK
常见状态码分类:
- 1xx:信息提示,请求已收到,继续处理
- 2xx:成功,200 OK 最常见
- 3xx:重定向,资源换了位置
- 4xx:客户端错误,404 Not Found(路径写错)、400 Bad Request(参数不对)
- 5xx:服务端错误,500 Internal Server Error(代码崩了)
服务器的响应分为静态和动态两种。静态响应直接返回一个 HTML 文件,动态响应需要根据请求参数(比如用户 ID)生成个性化内容。
下面是一个处理静态响应的最小 TCP 服务端代码:
listener, err := net.Listen("tcp", ":8001")
if err != nil {
log.Fatal(err)
}
defer listener.Close()
fmt.Println("启动服务器,监听端口:8001")
for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
go func(c net.Conn) {
defer c.Close()
buffer := make([]byte, 4096)
length, _ := c.Read(buffer)
requestLine := string(buffer[:length])
// 简单解析请求行,提取路径
parts := strings.Fields(requestLine)
path := parts[1]
// 构造响应
body := fmt.Sprintf("<h1>Hello, you requested: %s</h1>", path)
response := fmt.Sprintf(
"HTTP/1.1 200 OK\\r\\n"+
"Content-Length: %d\\r\\n"+
"Content-Type: text/html\\r\\n"+
"\\r\\n%s",
len(body), body)
c.Write([]byte(response))
}(conn)
}
关键点拆解:
- net.Listen(“tcp”, “:8001”) 监听 8001 端口,这个端口就是浏览器里 localhost:8001 指向的目标
- listener.Accept() 阻塞等待客户端连接,每个连接返回一个 Conn 对象
- 用 go 关键字启动 goroutine 处理每个连接,天然支持并发
- 解析请求行,提取出请求路径
- 构造完整的 HTTP 响应报文(状态行 + 头部 + 空行 + 响应体),通过 Conn 写出去
- HTTP 协议不关心文件内容是什么——HTML、图片、JSON,最终都转成字节流通过 TCP 发出去
总结
屏蔽掉底层细节,笼统总结一下上述过程:
应用层:
- 浏览器封装 HTTP 请求报文
- DNS 解析域名获得目标服务器地址
传输层:
- TCP 三次握手建立连接
- 把应用层传过来的 HTTP 请求报文进行分割,并在各个报文上打上标记序号及端口号
- 转发给网络层
网络层:
- 利用 ARP 协议根据 IP 地址获取作为通信目的地的 MAC 地址
- 转发给链路层
服务端在链路层收到数据,按序往上层发送,一直到应用层接收到浏览器发送来的 HTTP 请求报文,然后处理该请求并返回 HTTP 响应报文,浏览器接收到响应报文之后解析渲染界面。最后 TCP 四次挥手断开连接。
网硕互联帮助中心



评论前必须登录!
注册