后端第一课:输入 URL 后发生了什么?我用手写 Socket 把全链路跑通了

👋 我是 Yulia,一线 AI 应用开发工程师:4 年工程经验,在职做大模型应用落地。

只写一件事------把最新 AI 技术讲成人话 + 给你能直接跑通的代码 (全部本机实测,不搬运不注水)。

有用就点个 关注,持续更新大模型应用的前沿实战。
📚 本文是《后端开发从入门到靠谱》系列第 1 篇。整个系列共 20 篇,用 Python + FastAPI 串起一条「学生 → 在职程序员」的进阶路线,每篇的代码我都本机实测。关注我,不错过后续 19 篇。

"从输入 URL 到页面显示,中间发生了什么?"------这是后端面试的经典开场题,也是每个后端工程师绕不开的第一课。

但网上 90% 的文章有个通病:要么从浏览器渲染 视角讲(DOM 树、CSSOM、布局绘制),要么是纯背诵的"面试标准答案",读完你仍然没见过一次真实的报文长什么样。作为后端,你真正该关心的是网络链路:你的接口为什么慢、HTTP 报文怎么构造、服务器收到请求后做了什么。

所以这篇文章换个玩法:不用浏览器,我直接用 Python 的 socket 手写一个 HTTP 客户端,把 DNS、TCP、HTTP 的每个环节都真实跑一遍,让你看到每个阶段的原始数据。看完你会发现:所谓"神奇的互联网请求",底层就是几行文本。

读完你会拿到:

  • 一次请求的完整链路全景图:DNS → TCP → HTTP → 服务器处理 → 响应
  • DNS 解析的缓存层级与真实 nslookup 输出
  • TCP 三次握手为什么是三次不是两次(防失效连接的核心逻辑)
  • HTTP 报文逐字段拆解------用 3 行 Python 代替浏览器发请求
  • FastAPI 服务器收到报文后发生了什么(含本机实测日志)
  • 面试 99 分答案的答题框架(后端视角组织)

Web 服务器 DNS 服务器 浏览器/客户端 Web 服务器 DNS 服务器 浏览器/客户端 #mermaid-svg-6MDUHqbT1YJRsP0k{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6MDUHqbT1YJRsP0k .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6MDUHqbT1YJRsP0k .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6MDUHqbT1YJRsP0k .error-icon{fill:#552222;}#mermaid-svg-6MDUHqbT1YJRsP0k .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6MDUHqbT1YJRsP0k .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6MDUHqbT1YJRsP0k .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6MDUHqbT1YJRsP0k .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6MDUHqbT1YJRsP0k .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6MDUHqbT1YJRsP0k .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6MDUHqbT1YJRsP0k .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6MDUHqbT1YJRsP0k .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6MDUHqbT1YJRsP0k .marker.cross{stroke:#333333;}#mermaid-svg-6MDUHqbT1YJRsP0k svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6MDUHqbT1YJRsP0k p{margin:0;}#mermaid-svg-6MDUHqbT1YJRsP0k .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6MDUHqbT1YJRsP0k text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-6MDUHqbT1YJRsP0k .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6MDUHqbT1YJRsP0k .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-6MDUHqbT1YJRsP0k .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-6MDUHqbT1YJRsP0k .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-6MDUHqbT1YJRsP0k #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-6MDUHqbT1YJRsP0k .sequenceNumber{fill:white;}#mermaid-svg-6MDUHqbT1YJRsP0k #sequencenumber{fill:#333;}#mermaid-svg-6MDUHqbT1YJRsP0k #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-6MDUHqbT1YJRsP0k .messageText{fill:#333;stroke:none;}#mermaid-svg-6MDUHqbT1YJRsP0k .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6MDUHqbT1YJRsP0k .labelText,#mermaid-svg-6MDUHqbT1YJRsP0k .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-6MDUHqbT1YJRsP0k .loopText,#mermaid-svg-6MDUHqbT1YJRsP0k .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-6MDUHqbT1YJRsP0k .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6MDUHqbT1YJRsP0k .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-6MDUHqbT1YJRsP0k .noteText,#mermaid-svg-6MDUHqbT1YJRsP0k .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-6MDUHqbT1YJRsP0k .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6MDUHqbT1YJRsP0k .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6MDUHqbT1YJRsP0k .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6MDUHqbT1YJRsP0k .actorPopupMenu{position:absolute;}#mermaid-svg-6MDUHqbT1YJRsP0k .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-6MDUHqbT1YJRsP0k .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6MDUHqbT1YJRsP0k .actor-man circle,#mermaid-svg-6MDUHqbT1YJRsP0k line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-6MDUHqbT1YJRsP0k :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 2. TCP 三次握手建立连接 5. 连接关闭或 keep-alive 复用 1. 域名 example.com 的 IP 是? 104.20.23.154(可能命中各级缓存) SYN(我想连接,seq=x) SYN+ACK(收到,seq=y,ack=x+1) ACK(确认,ack=y+1)连接建立 3. HTTP 请求报文 GET / HTTP/1.1 ... 4. HTTP 响应报文 200 OK + HTML


一、先拆 URL:你到底在向谁发请求

在浏览器输入 http://example.com/orders?page=1,回车之前,先把它拆成五块:

组成 本例 含义
协议 http:// 传输规则(HTTP/HTTPS),默认端口 80/443
域名 example.com 人类可读的主机名,需要翻译成 IP
端口 (省略=80) 服务器上监听的服务入口,像大楼的门牌号
路径 /orders 请求服务器上的哪个资源
查询参数 ?page=1 附加给资源处理的参数

后端视角的第一个重点:域名不是地址,IP 才是。网络层的寻址全靠 IP,域名只是给人类看的"通讯录名字"。所以第一件事是查通讯录------这就是 DNS。

二、DNS 解析:全球最大分布式数据库的查找过程

DNS(Domain Name System)本质是一个分布在全球的键值数据库 :key 是域名,value 是 IP。它的设计核心是分层 + 缓存------因为全世界的域名解析请求每秒上亿次,任何单点都扛不住。

查找顺序(缓存优先,逐级上溯):
#mermaid-svg-tL0nvQdGX60GQC3l{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tL0nvQdGX60GQC3l .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tL0nvQdGX60GQC3l .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tL0nvQdGX60GQC3l .error-icon{fill:#552222;}#mermaid-svg-tL0nvQdGX60GQC3l .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tL0nvQdGX60GQC3l .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tL0nvQdGX60GQC3l .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tL0nvQdGX60GQC3l .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tL0nvQdGX60GQC3l .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tL0nvQdGX60GQC3l .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tL0nvQdGX60GQC3l .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tL0nvQdGX60GQC3l .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tL0nvQdGX60GQC3l .marker.cross{stroke:#333333;}#mermaid-svg-tL0nvQdGX60GQC3l svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tL0nvQdGX60GQC3l p{margin:0;}#mermaid-svg-tL0nvQdGX60GQC3l .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tL0nvQdGX60GQC3l .cluster-label text{fill:#333;}#mermaid-svg-tL0nvQdGX60GQC3l .cluster-label span{color:#333;}#mermaid-svg-tL0nvQdGX60GQC3l .cluster-label span p{background-color:transparent;}#mermaid-svg-tL0nvQdGX60GQC3l .label text,#mermaid-svg-tL0nvQdGX60GQC3l span{fill:#333;color:#333;}#mermaid-svg-tL0nvQdGX60GQC3l .node rect,#mermaid-svg-tL0nvQdGX60GQC3l .node circle,#mermaid-svg-tL0nvQdGX60GQC3l .node ellipse,#mermaid-svg-tL0nvQdGX60GQC3l .node polygon,#mermaid-svg-tL0nvQdGX60GQC3l .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tL0nvQdGX60GQC3l .rough-node .label text,#mermaid-svg-tL0nvQdGX60GQC3l .node .label text,#mermaid-svg-tL0nvQdGX60GQC3l .image-shape .label,#mermaid-svg-tL0nvQdGX60GQC3l .icon-shape .label{text-anchor:middle;}#mermaid-svg-tL0nvQdGX60GQC3l .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tL0nvQdGX60GQC3l .rough-node .label,#mermaid-svg-tL0nvQdGX60GQC3l .node .label,#mermaid-svg-tL0nvQdGX60GQC3l .image-shape .label,#mermaid-svg-tL0nvQdGX60GQC3l .icon-shape .label{text-align:center;}#mermaid-svg-tL0nvQdGX60GQC3l .node.clickable{cursor:pointer;}#mermaid-svg-tL0nvQdGX60GQC3l .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tL0nvQdGX60GQC3l .arrowheadPath{fill:#333333;}#mermaid-svg-tL0nvQdGX60GQC3l .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tL0nvQdGX60GQC3l .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tL0nvQdGX60GQC3l .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tL0nvQdGX60GQC3l .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tL0nvQdGX60GQC3l .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tL0nvQdGX60GQC3l .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tL0nvQdGX60GQC3l .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tL0nvQdGX60GQC3l .cluster text{fill:#333;}#mermaid-svg-tL0nvQdGX60GQC3l .cluster span{color:#333;}#mermaid-svg-tL0nvQdGX60GQC3l div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tL0nvQdGX60GQC3l .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tL0nvQdGX60GQC3l rect.text{fill:none;stroke-width:0;}#mermaid-svg-tL0nvQdGX60GQC3l .icon-shape,#mermaid-svg-tL0nvQdGX60GQC3l .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tL0nvQdGX60GQC3l .icon-shape p,#mermaid-svg-tL0nvQdGX60GQC3l .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tL0nvQdGX60GQC3l .icon-shape .label rect,#mermaid-svg-tL0nvQdGX60GQC3l .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tL0nvQdGX60GQC3l .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tL0nvQdGX60GQC3l .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tL0nvQdGX60GQC3l :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 有
无
有
无
浏览器输入域名
浏览器 DNS 缓存有吗
直接用缓存的 IP
操作系统缓存 hosts 文件有吗
本地 DNS 服务器

如 114.114.114.114
根域名服务器 告诉你去 .com 的服务器
.com 顶级域服务器 告诉你去 example.com 的
权威 DNS 服务器 返回最终 IP

为什么分这么多层? 底层逻辑是"谁的数据谁管":根服务器只认识顶级域(.com/.cn/.org),顶级域服务器只认识自己下面的二级域,example.com 的 IP 只有它自己的权威服务器说了算。分层让每台服务器只存一小块数据,也方便域名主自主修改解析。全球根服务器只有 13 个 IP(背后是几百台机器组成的任播集群)------13 个 IP 就能服务全世界的根查询,靠的就是"只问一层,剩下的交出去"。

本机实测------用 nslookup 查询 example.com 的真实过程:

复制代码
> nslookup example.com
服务器:  public1.114dns.com          ← 我本机的 DNS 服务器(运营商)
Address:  114.114.114.114

名称:    example.com
Addresses:  2606:4700:10::6814:179a   ← 2 个 IPv6
            2606:4700:10::ac42:93f3
            172.66.147.243             ← 2 个 IPv4(Cloudflare CDN)
            104.20.23.154

一个域名解析出 4 个 IP------这叫 DNS 负载均衡:CDN 厂商(这里是 Cloudflare)给不同地区返回不同 IP,把流量分散到不同机房。你问"为什么域名能扛住全球访问",这就是答案的第一层。

三、TCP 三次握手:为什么偏偏是三次

拿到 IP 后,客户端要建立 TCP 连接。TCP 是可靠的字节流协议 ,可靠性依赖双方同步"序列号"(每个字节编号,用来确认收到、重排乱序、检测丢失)。握手的目的:双方确认彼此的收发能力 + 交换初始序列号。
服务器 客户端 服务器 客户端 #mermaid-svg-lGCajcNZcb1B992H{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lGCajcNZcb1B992H .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lGCajcNZcb1B992H .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lGCajcNZcb1B992H .error-icon{fill:#552222;}#mermaid-svg-lGCajcNZcb1B992H .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lGCajcNZcb1B992H .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lGCajcNZcb1B992H .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lGCajcNZcb1B992H .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lGCajcNZcb1B992H .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lGCajcNZcb1B992H .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lGCajcNZcb1B992H .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lGCajcNZcb1B992H .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lGCajcNZcb1B992H .marker.cross{stroke:#333333;}#mermaid-svg-lGCajcNZcb1B992H svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lGCajcNZcb1B992H p{margin:0;}#mermaid-svg-lGCajcNZcb1B992H .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lGCajcNZcb1B992H text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-lGCajcNZcb1B992H .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lGCajcNZcb1B992H .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-lGCajcNZcb1B992H .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-lGCajcNZcb1B992H .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-lGCajcNZcb1B992H #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-lGCajcNZcb1B992H .sequenceNumber{fill:white;}#mermaid-svg-lGCajcNZcb1B992H #sequencenumber{fill:#333;}#mermaid-svg-lGCajcNZcb1B992H #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-lGCajcNZcb1B992H .messageText{fill:#333;stroke:none;}#mermaid-svg-lGCajcNZcb1B992H .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lGCajcNZcb1B992H .labelText,#mermaid-svg-lGCajcNZcb1B992H .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-lGCajcNZcb1B992H .loopText,#mermaid-svg-lGCajcNZcb1B992H .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-lGCajcNZcb1B992H .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lGCajcNZcb1B992H .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-lGCajcNZcb1B992H .noteText,#mermaid-svg-lGCajcNZcb1B992H .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-lGCajcNZcb1B992H .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lGCajcNZcb1B992H .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lGCajcNZcb1B992H .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lGCajcNZcb1B992H .actorPopupMenu{position:absolute;}#mermaid-svg-lGCajcNZcb1B992H .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-lGCajcNZcb1B992H .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lGCajcNZcb1B992H .actor-man circle,#mermaid-svg-lGCajcNZcb1B992H line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-lGCajcNZcb1B992H :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 随机生成初始序列号 x 随机生成初始序列号 y 双方都知道:对方能收也能发 ✅ ① SYN=1, seq=x 「我想连接,我的起始序号是 x」 ② SYN=1, ACK=1, seq=y, ack=x+1 「收到,我的起始序号是 y,下一个该收 x+1」 ③ ACK=1, seq=x+1, ack=y+1 「确认,连接建立」

面试必问:为什么是三次,两次不行吗? 两次的问题在防失效连接 。想象一个网络差的场景:客户端发了一个 SYN,堵在网络上迟迟没到;客户端等不及,重发了一个新 SYN,这次正常握手、正常通信、正常关闭。此时,第一个迟到的旧 SYN 才慢悠悠到达服务器 ------如果两次握手就能建立连接,服务器会以为这是新请求,回个 SYN+ACK 就傻等数据;客户端却一脸懵:我没发过连接请求啊。旧 SYN 造成的"幽灵连接"就这样白白占用服务器资源。三次握手的关键在第三步 ACK:客户端收到服务器的 SYN+ACK 后,只有真正想连接才会回 ACK,服务器收到 ACK 才算数------迟到的旧 SYN 等不到这个 ACK(客户端收到一个陌生的 SYN+ACK 会直接回 RST 拒绝),幽灵连接自然被消灭。

用一句话记:一次握手发请求,两次握手服务器确认但客户端没确认,三次握手双方都确认了。

(想亲眼看到握手状态?开两个终端:一个用 python -m http.server 8010 起个服务,另一个用 netstat -an | grep 8010 观察连接从 SYN_SENT → ESTABLISHED 的瞬间。后端第二课讲 Linux 命令时会展开。)

四、HTTP 报文:3 行 Python 代替浏览器发请求

连接建立后,就该发送 HTTP 请求了。HTTP 报文长得什么样?与其看示意图,不如自己造一个。下面这段代码用 Python 的 socket 直接连上 example.com 的 80 端口,手写请求报文发过去(本机实测输出):

python 复制代码
import socket

# 1. 建立 TCP 连接(内部完成三次握手)
s = socket.create_connection(('example.com', 80), timeout=10)
print('[1] TCP 连接已建立, 本机端口:', s.getsockname())

# 2. 手写 HTTP 请求报文:起始行 + 头部 + 空行
req = 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n'
s.sendall(req.encode())

# 3. 接收响应
data = b''
while True:
    chunk = s.recv(4096)
    if not chunk:
        break
    data += chunk
s.close()
print('[3] 收到响应, 总长度', len(data), '字节')

本机实测输出(前 500 字节):

复制代码
[1] TCP 连接已建立, 本机端口: ('172.16.24.119', 60226)
[3] 收到响应, 总长度 928 字节, 前 500 字节:
HTTP/1.1 200 OK
Date: Wed, 14 Oct 2026 09:30:43 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: close
Server: cloudflare
Last-Modified: Tue, 13 Oct 2026 22:05:48 GMT
Allow: GET, HEAD
Accept-Ranges: bytes
Age: 4
cf-cache-status: HIT
CF-RAY: a4a59123e881d2f7-FRA
alt-svc: h3=":443"; ma=86400

241
<!doctype html><html lang=en><head>...

看懂了这个输出,你就懂了 HTTP 的全部骨架。拆开看:

请求报文 = 起始行 + 头部 + 空行 + 可选报文主体:

复制代码
GET / HTTP/1.1        ← 起始行:方法 + 路径 + 协议版本
Host: example.com     ← 头部:告诉服务器"我要访问哪个域名"
Connection: close     ← 头部:响应完成后关闭连接
                      ← 空行(CRLF):头部结束的标志
  • 起始行 三个词缺一不可:GET(做什么)、/(对哪个资源)、HTTP/1.1(按什么协议理解)。PUT、POST、DELETE 都在这第一位。
  • Host 头为什么必须写:一台服务器(一个 IP)上可能挂着几十个网站(虚拟主机),服务器靠 Host 头区分你找谁。HTTP/1.1 规定 Host 必填------这也是"IP 能通但域名不对"时返回 400 的原因。
  • 空行 不是装饰:服务器解析报文时,看到连续两个 \r\n 才知道"头部结束了,后面是正文"。

响应报文 = 状态行 + 头部 + 空行 + 正文:

  • HTTP/1.1 200 OK:200 是状态码------2xx 成功、3xx 重定向、4xx 客户端错(404 资源不存在)、5xx 服务器错。后端排障第一步永远是看状态码。
  • Transfer-Encoding: chunked:分块传输编码 。服务器响应时不一定预先知道正文总长度,于是把正文切成一块块发送------每块先发一个十六进制数字表示块大小(输出里的 241 = 577 字节),再发内容,最后以 0 块结束。这就是你在输出里看到 241 这个"突兀数字"的原因。
  • Connection: close:我请求里写了它,服务器照做,响应完关闭连接。如果我不写,连接会保持(keep-alive),客户端 recv() 会一直等待直到超时 ------我在实测中就踩了这个坑:同样的代码不带 Connection: close 请求本地服务,直接 TimeoutError。理解这一点,你就理解了"长连接复用省三次握手开销"的底层逻辑。
  • Server: cloudflare:这台服务器前面套着 Cloudflare CDN。cf-cache-status: HIT 说明响应来自 CDN 缓存而不是源站------全世界访问都快的秘密,大半在 CDN 缓存里。

对比一下 HTTP 三个版本的演进(面试高频):

版本 连接方式 关键改进 现状
HTTP/1.1 串行,keep-alive 复用 长连接、Host 头、chunked 最常见(本实验用的就是它)
HTTP/2 单连接多路复用 二进制分帧,一个 TCP 连接并发多个请求 主流网站标配(需 HTTPS)
HTTP/3 基于 UDP(QUIC) 握手更快、弱网更强、避免队头阻塞 正在普及(响应头 alt-svc: h3 就是服务器在邀请你切 HTTP/3)

注意上面实测响应里就有一行 alt-svc: h3=":443"; ma=86400------服务器在说:"我支持 HTTP/3,下次可以走 443 端口"。一次实验同时看见三代协议的影子,这就是抓真实报文的价值。

五、服务器端:FastAPI 收到报文后做了什么

报文到了服务器,接下来是后端工程师的主场。用 FastAPI 起一个最小服务(本系列的主线框架),在处理函数里把服务器"看到"的请求信息打印出来:

python 复制代码
from fastapi import FastAPI, Request

app = FastAPI()

@app.get("/orders")
async def get_orders(request: Request):
    # 打印服务器收到的原始请求行和关键头部(真实报文)
    print(f"[收到请求] {request.method} {request.url.path}")
    print(f"  Host: {request.headers.get('host')}")
    print(f"  User-Agent: {request.headers.get('user-agent')}")
    print(f"  Connection: {request.headers.get('connection')}")
    return {"orders": [{"id": 1, "status": "paid"}]}

用 curl 请求它,服务器端真实日志长这样(本机实测):

复制代码
INFO:     Started server process [3652]
INFO:     Uvicorn running on http://127.0.0.1:8012
[收到请求] GET /orders
  Host: 127.0.0.1:8012
  User-Agent: curl/8.18.0
  Connection: None
INFO:     127.0.0.1:53322 - "GET /orders HTTP/1.1" 200 OK

日志背后,服务器对一次请求的处理是一条流水线:

  1. 接收:uvicorn(ASGI 服务器)监听端口,把 TCP 字节流按 HTTP 格式解析成结构化对象;
  2. 路由匹配 :FastAPI 把 GET /orders 和 @app.get("/orders") 装饰器注册的路由表比对,命中后执行你的函数;
  3. 处理:你的函数读参数、查数据库、算结果,返回 Python 字典;
  4. 封装响应 :FastAPI 把字典序列化成 JSON,加上 content-type: application/json 等头部,交给 uvicorn 写回 TCP 连接。

再用手写 socket 客户端请求这个本地服务,看它吐回来的完整响应(本机实测):

复制代码
HTTP/1.1 200 OK
date: Wed, 14 Oct 2026 10:00:49 GMT
server: uvicorn
content-length: 37
content-type: application/json
Connection: close

{"orders":[{"id":1,"status":"paid"}]}

到此,闭环完成:你用 3 行 Python 造请求,服务器按流水线处理,再把结果原路送回------整个互联网上的每一次请求,都是这条链路的重复。

顺带说两个我在实测中遇到的真实事故,都是后端日常会撞上的:

  • 端口冲突 :第一次起服务用了 8000 端口,uvicorn 直接报 Errno 10048:地址已在使用 启动失败------端口是稀缺资源,一个端口同一时刻只能被一个进程监听,生产环境"端口被占"就是这样报出来的;
  • keep-alive 超时 :手写客户端不带 Connection: close 请求本地服务时,recv() 一直等不到连接关闭,直接 TimeoutError------服务器默认保持长连接,客户端必须自己约定何时结束。

六、响应之后:连接是关还是留

响应发完,TCP 连接的去留有讲究。短连接 (HTTP/1.0 默认)每次请求后关闭,代价是下次请求要重新三次握手;长连接 (keep-alive,HTTP/1.1 默认)复用同一条连接发多个请求,省下握手开销。关闭时是"四次挥手"------因为 TCP 是全双工(双向各自独立),双方各发 FIN 各收 ACK,共四个报文。面试记住一句话即可:三次握手建连接,四次挥手断连接,多的一次是因为"收"和"发"要分开关闭。

keep-alive 为什么重要?一个页面往往要请求几十个资源(HTML、CSS、JS、图片),如果每个资源都重新握手,三次握手每次至少 1 个网络往返(RTT),几十个资源光握手就浪费几十个 RTT。长连接把它们压缩到一条连接上串行(HTTP/1.1)或并发(HTTP/2)完成------这就是协议演进的主线:用更少的连接、更少的往返,传输更多的数据。

七、面试 99 分答案:后端视角的答题框架

回到开头的面试题。普通答案从头背到尾,考官听到第三句就走神。99 分答案按分层 + 重点组织,每个环节一句话说机制、一句话说重点:

  1. URL 解析:拆协议/域名/端口/路径(一句话带过);
  2. DNS 解析:浏览器缓存 → hosts → 本地 DNS → 根/顶级/权威递归查询,CDN 多 IP 负载均衡(重点:为什么分层);
  3. TCP 三次握手:SYN / SYN+ACK / ACK,防失效连接(重点:为什么三次);
  4. HTTP 请求:起始行 + 头部 + 空行,Host 头定位虚拟主机,浏览器条件请求缓存(重点:报文结构);
  5. 服务器处理:Web 框架路由 → 业务逻辑 → 数据查询 → 响应封装(重点:结合你会的框架讲,如 FastAPI 流水线);
  6. 响应返回:状态码、keep-alive、chunked/压缩,浏览器渲染(渲染部分一句"交给渲染引擎构建 DOM"即可,后端不用展开)。

如果面试官打断你问细节,把你实测过的东西讲出来------"我手写 socket 发过请求,chunked 编码每块前有十六进制长度"------这句话一出,你就和其他背答案的候选人拉开了差距。

总结:一张图带走

本机复现清单(建议你亲手跑一遍,十分钟全链路通关):

bash 复制代码
# 1. DNS 解析
nslookup example.com

# 2. 看真实报文
curl -v http://example.com/

# 3. 手写客户端(本文第四节代码保存为 client.py)
python client.py

# 4. 起本地服务看服务器日志
pip install fastapi uvicorn
uvicorn app:app --port 8012
curl http://127.0.0.1:8012/orders

下一课预告 :《Linux 生存手册:后端必会的 20 个命令 + 服务器实战》------会用 netstat 观察连接的 4 种状态、用日志定位问题、在服务器上部署第一个服务。你在哪个环节最想深入?是 DNS 的缓存策略,还是 TCP 的拥塞控制?评论区点题,我按呼声安排后续篇目。有用的话点赞 + 收藏,转发给同样在学后端的朋友。

本文所有命令与代码均于 2026 年 10 月本机实测,输出来自 Windows 11 + Python 3.11 环境。协议细节参考 RFC 9110(HTTP 语义)、RFC 1035(DNS)。文中 CDN 为 Cloudflare,仅为实测对象,无商业推广。


参考来源:

相关推荐
Frag0ut3 小时前
Chrome 2026 AI 能力全解析:从 Auto Browse 到 Skills,浏览器正在变成智能代理
chrome·ai·谷歌浏览器·gemini·智能代理·skills·auto browse
全栈弄潮儿3 小时前
Python实战第7期:字典与集合
后端·python·编程语言
2603_965148114 小时前
电商选品调研:通过关键词搜索API获取市场热销商品,结合商品详情API分析竞品卖点与定价
开发语言·数据库·人工智能·python·信息可视化
xixiaoyunya4 小时前
Python 异步编程入门:用 asyncio 轻松实现并发任务
开发语言·python·php
把我格子衫拿来4 小时前
Dice 很高就够了吗?分割评价中的 IoU、表面距离与 HD95
python·图像分割·模型评估·医学影像·dice
曲径海盗5 小时前
【Python 学习第 31 天】复习:变量与数据类型 等 3 题
python·学习笔记·编程入门
Data_Journal5 小时前
如何绕过 Cloudflare:网页抓取的最佳方法
网络·python
一枕槐安4875 小时前
AG 幻觉率从 24% 压到 4%:我把召回评估做成了流水线
人工智能·ai
我命由我123456 小时前
智能助手通用问答边界
人工智能·python·学习·ai·学习方法·ai编程·智能体