Prompt:我想理顺互联网是怎么连接的。通过我自己从服务器开始自己搭个人博客的经历,我大体能理清“客户端(浏览器)发送请求(关于请求方法、端口等等)->DNS解析->服务器端Nginx->/var/www/真正的页面、数据、逻辑等等->Nginx->客户端”这样一条线,但我知道这肯定是不准确不完整的(譬如我知道还有像协议、套接字……这些名词)。你能为我完整清楚地讲解一下吗?
一次完整的 Web 访问,比你想象的要多绕几个弯,但抓住一条主线就能串起所有概念。下面我以你访问自己搭建的博客(假设是 https://blog.yourdomain.com)为例,把从浏览器敲回车到页面出现的完整链路讲清楚。
1. 拆解 URL,决定“怎么去”
浏览器先把 https://blog.yourdomain.com/articles/1 拆开:
- 协议:
https→ 用 TLS 加密的 HTTP,目标端口默认 443 - 主机名:
blog.yourdomain.com→ 需要先变成 IP - 路径:
/articles/1→ 请求的具体资源
协议和端口是后续构造请求的基础,但首要问题是:blog.yourdomain.com 的 IP 是什么? 这就进入 DNS。
2. DNS:把名字变成 IP
DNS 不是“查一次就完事”,而是一个层级递归/迭代的过程:
- 浏览器缓存 → 操作系统缓存 → hosts 文件 → 如果都没有,就向本地 DNS 服务器(通常是路由器或 ISP 提供的)发起查询。
- 本地 DNS 服务器从 根域名服务器 问到
.com的顶级域服务器,再问到yourdomain.com的权威 DNS 服务器,最终拿到一条 A 记录(IPv4)或 AAAA 记录(IPv6)。 - 假设得到 IP 为
203.0.113.5,这就是你服务器的公网地址。
DNS 本身跑在 UDP 53 端口(也支持 TCP),对整个流程至关重要的是:现在浏览器知道了服务器的 IP 和端口(443),可以建连接了。
3. 套接字(Socket)登场,建立 TCP 连接
套接字 = IP 地址 + 端口号 + 协议,是操作系统提供给应用程序收发数据的“门把手”。
- 浏览器调用
socket(AF_INET, SOCK_STREAM, 0)创建一个 TCP 套接字。 - 内核自动分配一个 随机源端口(如
54123),源 IP 是你本机的内网 IP(如192.168.1.10)。 随后
connect()到目标203.0.113.5:443。内核开始 TCP 三次握手:- 你 → 服务器:
SYN(序列号 x) - 服务器 → 你:
SYN+ACK(序列号 y,确认 x+1) - 你 → 服务器:
ACK(确认 y+1)
- 你 → 服务器:
连接建立后,这对套接字 192.168.1.10:54123 <-> 203.0.113.5:443 就成为浏览器和服务器之间专属的“双向管道”。
那你的个人电脑怎么出得去公网?——NAT
你的 192.168.x.x 是私有地址。家庭路由器做 SNAT(源地址转换),把你的数据包的源 IP 换成路由器的公网 IP,并分配一个临时端口(如 33333)。服务器看到的实际是 路由器公网IP:33333。路由器记住这个映射,把返回的包再转回给你的内网机器。整个过程对浏览器透明。
4. TLS 握手:在 TCP 上建一条加密隧道
因为用了 https,TCP 连接建立后还不能直接发 HTTP 请求,必须先进行 TLS 握手(简版流程):
- 浏览器发送
ClientHello(支持的加密套件、随机数等)。 - 服务器(Nginx)回复
ServerHello(选定加密套件、随机数) + 证书链(包含域名blog.yourdomain.com的公钥)。 - 浏览器验证证书:域名匹配、由受信任的 CA 签发、未过期。
- 双方通过密钥交换算法(ECDHE 等)协商出 对称会话密钥。
- 后续 HTTP 数据全用这个对称密钥加密,外面看到的都是密文。
至此,一条安全、可靠的 TCP + TLS 字节流 已经就绪。
5. 构造并发送 HTTP 请求
浏览器拼出 HTTP 请求报文(明文,但即将被 TLS 加密):
GET /articles/1 HTTP/1.1
Host: blog.yourdomain.com
User-Agent: Mozilla/5.0 ...
Accept: text/html,...把这个字符串通过之前创建的套接字 write() 进去。操作系统内核做这些事:
- TCP 层:把数据切成合适的段(segment),加上 TCP 头(源/目标端口、序列号、校验和等)。
- IP 层:封装为 IP 包,源 IP(经过 NAT 后为路由器公网 IP),目标 IP
203.0.113.5,协议号 6(TCP)。 - 链路/物理层:查路由表,找到下一跳网关。在以太网中,通过 ARP 获取网关 MAC 地址,封装成帧发送。
这包数据就在网线/Wi-Fi/光纤中,经过多个路由器逐跳转发,最终到达你服务器的网卡。
6. 服务器端接收,内核将数据交给 Nginx
服务器网卡收到帧,内核协议栈逐层剥离:
- 检查 MAC 地址,确认是给自己的。
- 网络层看到目标 IP 是自己,协议是 TCP,递交给传输层。
- TCP 层看到目标端口 443,查找此端口对应的 监听套接字——正是你的 Nginx 进程。
Nginx 内部做了什么?
Nginx 的 master 进程提前创建了一个 监听套接字 绑定在 0.0.0.0:443,并调用 listen()。当三次握手完成、TLS 握手完成后,内核把这个已建立的连接放入 接受队列。Nginx 的 worker 进程通过 epoll/kqueue 等机制获知新连接,调用 accept() 得到一个 新的连接套接字(源端口 33333,与你通信)。之后 Nginx 使用这个套接字读写数据。
TLS 解密后,Nginx 读到了完整的 HTTP 请求:
- 检查
Host头,匹配server_name blog.yourdomain.com的虚拟主机配置。 看到路径
/articles/1,按location规则处理:- 静态文件:如果请求的是
/static/logo.png,Nginx 直接从/var/www/...读取文件内容,构造 200 响应返回。 动态请求:如果后端是 PHP(你提到的 “真正的页面/数据/逻辑”),Nginx 会通过 FastCGI 协议 把请求转发给
php-fpm。php-fpm通常监听一个 Unix Domain Socket(如/run/php-fpm.sock)或 TCP 端口(127.0.0.1:9000)。- 这也是套接字!Nginx 作为客户端,
php-fpm作为服务端,通过这个套接字传输 FastCGI 协议数据。 php-fpm执行你的 PHP 代码(读取数据库、生成 HTML),把结果返回给 Nginx。
- 静态文件:如果请求的是
Nginx 将响应组装成 HTTP 报文:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 5123
...(其他头部)
<html>...真正的页面...</html>将这段数据通过之前与浏览器建立的连接套接字写回,内核负责加密(TLS)、分段、打包,沿原路发回给客户端。
7. 响应回程与浏览器渲染
回程的 IP 包目标地址是你路由器的公网 IP,NAT 设备根据之前记录的映射,把目标地址转回你的内网 IP 192.168.1.10:54123,发给你的电脑。
浏览器从自己的套接字读取到 HTTP 响应,解析状态码、头部、HTML。然后:
- 解析 HTML 过程中,遇到图片、CSS、JS 等外部资源,会发起新的请求(可能复用已有的 TCP 连接,即 HTTP/1.1 Keep-Alive 或 HTTP/2 多路复用,避免重复握手)。
- 渲染引擎逐步构建 DOM、CSSOM,最终呈现页面。
你那条线的“升级版”
你原先的链路:
客户端 → DNS → Nginx → /var/www → Nginx → 客户端
现在可以理解为一条充分展开的版本:
浏览器(URL解析,构造HTTP)
→ DNS(递归/迭代获取IP)
→ 创建套接字,TCP三次握手 + 可能的TLS握手
→ NAT转换(客户端侧)
→ 互联网路由(多跳转发)
→ 服务器内核协议栈(IP/TCP解包)
→ Nginx监听套接字接受连接,解密TLS,解析HTTP
→ 若静态:直接读 /var/www;若动态:通过 FastCGI/uWSGI 等套接字与后端(PHP/Python等)通信
→ Nginx 构造HTTP响应
→ 经内核、网络、NAT原路返回
→ 浏览器解析渲染协议分层是贯穿始终的骨架:
- 应用层:HTTP、DNS、FastCGI
- 传输层:TCP、UDP
- 网络层:IP、路由
- 链路层:以太网、ARP
套接字则是应用层与传输层之间的编程界面,让 Nginx、浏览器等不必操心 TCP 细节,就能“像读写文件一样”收发网络数据。你搭博客时也许没直接碰过 socket 代码,但 Nginx 的 listen 443;、php-fpm 的 listen = /run/php-fpm.sock 本质上都是在配置套接字。
这样,整个“互联网是怎么连接的”就有了一个丰满、准确的图景。