欢迎光临
我们一直在努力

HTTP理解(2)请求行字段解读和状态码初认识

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

1. 一句总纲:上篇讲报文长什么样,这篇讲怎么保证一个字不差地读下来。

2. 请求行三段:只有方法带语义

请求行只有一行:

GET /index.html HTTP/1.1\\r\\n

在这里插入图片描述


3. GET 与 POST:一个拿下来,一个送上去

上篇有一个结论:人上网只干两件事 —— 把远端的东西拿下来,把本地的东西送上去。方法就是这两件事的名字。

方法语义动作本质典型场景
GET 从远端获取内容 read 刷短视频、看网页、下文件、点链接
POST 向远端提交参数 write 注册、登录、发评论、提交作业

判断标准只有一条:这次来,是想让服务器「给出」东西,还是想让它「收下」东西。

  • 浏览器访问自己写的服务器,服务器把磁盘上的文件读出来发回去 —— 这是 GET;
  • 表单里填了账号口令点提交,服务器把正文读出来存下来 —— 这是 POST。

方法不止两个。规范里还有 PUT、DELETE、HEAD、OPTIONS、TRACE、CONNECT,但真实流量里 99% 就是 GET 和 POST。剩下那些,用一句话概括:一部分极少用,一部分一看名字就懂。

3.1 一个容易记混的点:GET 也能带参数

「GET 不能传参」是错的。GET 的参数挂在 URI 的 ? 后面:

GET /s?wd=%E5%BE%AE%E4%BF%A1 HTTP/1.1

区别不在「能不能传」,而在参数放在哪、谁看得见:

参数位置地址栏长度刷新行为
GET URI 里,?k=v&k=v 可见,可收藏可分享 受 URI 长度限制 直接重发
POST 正文里 不可见 由 Content-Length 说明 浏览器会问「要不要重新提交」

百度搜索用的是 GET(关键词挂在 ?wd= 后面),注册登录用的是 POST(口令放在正文里)。前者要能被分享和收藏,后者不能被看见。

3.2 版本演进:数字一变,能用的功能就变

版本特征备注
HTTP/0.9 只有 GET,没有报头 历史包袱
HTTP/1.0 请求-响应一问一答,短连接 讲原理就用这一版
HTTP/1.1 支持长连接 keep-alive 今天的主流
HTTP/2 · 3 多路复用、二进制分帧 暂时不管

上篇讲的「短连接」就是 1.0 的模型:一问一答,答完 close。1.1 的长连接是优化,不是本质。


4. 版本号为什么两边都要写

请求行末尾写 HTTP/1.1,状态行开头也写 HTTP/1.1。同一个版本号写两遍,看起来是冗余,其实是一次协商。

4.1 先承认一个事实:这是两款软件

在这里插入图片描述

浏览器由浏览器厂商写,服务器由自己写(或者由另一家公司写)。两边代码毫无关系,唯一认的东西是那段纯文本的格式。所以:

  • 客户端手里有一个 Request 对象,序列化成文本发出去;
  • 服务端从 socket 里读出一段字节,按同一套规则反序列化回来。

协议是两份独立实现之间唯一的合同。 这一句能解释很多现象:改一个空格、少一个 \\r\\n,对端就听不懂;「明明发了东西,浏览器就是报错」,十有八九是格式差了一点。

4.2 再说版本号为什么不能省

关键前提:客户端的新老版本会同时在线。

在这里插入图片描述

所以两边都要报版本:

  • 客户端自报 → 服务器知道对面是谁;
  • 服务器按版本回流 → v1 客户端只拿到 v1 的功能;
  • 状态行里的版本号 → 服务器告诉客户端「按哪一版回的」。

💡 换一个说法:版本号不是装饰,是降级开关。它让新服务端能同时伺候新老客户端,而不需要客户端一夜之间全部升级。


5. 字节流不认报文边界

到这里还没碰到真正的麻烦。麻烦在于:recv 返回的那一段字节,跟报文没有任何对齐关系。

在这里插入图片描述


6. 想要读全一个请求,则需要三步:

恢复的依据只有两个:空行和 Content-Length。

在这里插入图片描述

6.1 第一步:读到空行,报头就齐了

空行是协议写死的分隔符。报头是一行一行的(Key: Value\\r\\n),每一行都以 \\r\\n 结尾;当出现一个「什么都没有的行」,也就是连续两个 \\r\\n,报头结束。

所以第一步的判据非常干脆:在收到的字节里找 \\r\\n\\r\\n,找到就说明报头完整了。

为什么这一步一定可靠?因为报头里不可能出现空行 —— 空行是保留的边界标记。其他行都有内容,唯独分隔符这一行什么都没有。

这一步的产出:一份完整的报头(请求行 + 所有 Key: Value)。正文完整不完整,还不知道。

6.2 第二步:反序列化报头,取出 Content-Length

报头是一堆 Key: Value,其中最要紧的一个是:

Content-Length: 36

它声明的是有效载荷(正文)的字节数。三种情况:

情况含义
有这个字段,值 > 0 后面要跟这么多字节的正文
有这个字段,值 = 0 有正文段,但是空的
没有这个字段 没有正文

6.3 第三步:从剩余字节里取 N 个字节

拿到 N 之后,剩下的事情就是数数:

  • 剩余字节 不足 N 个 → 返回,等下一次 recv 把余下的拼上来;
  • 剩余字节 够 N 个 → 截出前 N 个字节当正文,多出来的留给下一轮。

三步走完,一份完整报文到手。多出来的部分不能丢 —— 它已经是下一个请求的开头了。

6.4 这套做法并不新

自定义协议的计算器里就是这么干的:先找 \\r\\n,切出报头,取出长度,再读正文。

不同的只有一点:计算器的报头只有一行(一个数字),所以「找 \\r\\n」和「找空行」是一回事;HTTP 的报头有十几行,于是多了一步「先找到连续两个 \\r\\n」。

✔ 一个足够强的表述:任何「报头 + 变长正文」的协议,靠的都是「固定分隔符切头 + 长度字段定尾」这两件事。 HTTP、自定义协议、Redis 协议、MQTT,全是这个套路。


7. 一份真实的 POST 报文

理论讲完,看一份抓下来的真货。

POST /companyLogin.do HTTP/1.1
Host: job.xjtu.edu.cn
Connection: keep-alive
Content-Length: 36
Cache-Control: max-age=0
Origin: http://job.xjtu.edu.cn
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) …
Accept: text/html,application/xhtml+xml,application/xml;q=0.9
Referer: http://job.xjtu.edu.cn/companyLogin.do
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=D6287A58…; Hm_lvt_783e83ce…

username=hgtz2222&password=2222222222

整份报文 15 行:1 行请求行 + 13 行报头 + 1 个空行 + 1 行正文。

值得留意的几件事:

  • 报头占了九成篇幅。 真正要传的数据(用户名口令)只有最后一行,前面全是元信息。应用层协议的典型形态就是这样:元信息的量远超有效载荷。
  • Content-Length: 36 是发送方数出来填进去的,不是接收方估算的。所以接收方只要照着 36 去数,一定不多不少 —— 这是整个「读全报文」方案能成立的根本原因。
  • 正文是 username=…&password=…,Content-Type 声明它是 application/x-www-form-urlencoded,也就是「用 & 连接的键值对」。
  • 口令是明文。HTTP 本身不加密,加密要靠 TLS(那是 https 的事)。

8. 响应方向同理:Content-Length 是双向的

请求要读全,响应也要读全。所以对称地:

应答一旦携带数据,就应该在响应报头里带上 Content-Length: XXX。

现状是:今天的浏览器非常宽容,不带也能把网页渲染出来。但规范上要求带,原因和请求方向一样 —— 接收方没有别的办法知道正文到哪结束。

反过来看就更清楚:浏览器解析响应时,靠的就是

  • 读到空行 → 报头结束;
  • 取 Content-Length → 正文长度;
  • 截取这么多字节 → 正文。
  • 如果正文后面还有内容(比如长连接上的下一个响应),不带长度就彻底分不清边界。

    ✖ 一个常见的错觉:以为「TCP 关闭连接」就代表正文结束。短连接下勉强成立,长连接下完全不成立。


    9. 报头的四把钥匙

    报头字段有几十个,真正需要理解的只有四个:Host、User-Agent、Accept-Encoding、Referer。剩下的(Cookie、Connection、Location)留到下一篇。

    9.1 Host:资源在哪台机器的哪个端口

    Host: 8.137.19.140:8081

    它记的是「被请求资源所在的主机和端口」。对一台普通的、自己就是那台服务器的程序来说,这个字段看看就好 —— 反正请求就是发给自己的。

    但对代理服务器,这个字段是命门:

    • 收到请求,看 Host;
    • Host 指向的不是自己 → 不处理,转发给那台机器;
    • 拿到应答复,再转回给客户端。

    代理服务器自己不当老板,只当中间人。 这一条在 §16 还会展开。

    9.2 User-Agent:客户端是谁

    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) …

    它装的是客户端信息:操作系统、浏览器内核、版本号。手机访问时,这里就会变成 Android 或 iOS 的标识。

    它的用途能用一个场景说明白:在百度搜「微信」,页面会推一个「安全下载」按钮。Windows 上点下去是 .exe,手机上点下去是安卓包或 iOS 跳转。

    服务器凭什么判断客户端是什么设备? 因为搜索本身也是一次 HTTP 请求,请求里带着 User-Agent。服务器读这个字段,就知道该推哪个平台的包。

    它还有第二个用途:判断请求是不是真人发的。后端可以检查 User-Agent 是否像一个正常浏览器,不像就拒绝。这一条直接关系到爬虫能不能活着(§16)。

    9.3 Accept-Encoding:能不能压缩

    Accept-Encoding: gzip, deflate

    意思是「本端能解压这些格式,对端可以压完再发」。

    为什么要压:网页经常几十上百 KB,先压缩再传,用双方的 CPU 算力去对冲网络 IO 的时间。省的是带宽,花的是算力 —— 在带宽贵的年代这是刚需,今天依然默认开着。

    9.4 Referer:从哪个页面跳过来的

    Referer: http://job.xjtu.edu.cn/companyLogin.do

    它回答的是「上一次是从哪个页面点过来的」。这个字段能直接做实验验证:

  • 首页有指向 login.html 和 register.html 的链接;
  • 从首页点进登录页 → 登录页那次请求的 Referer 是首页地址;
  • 在登录页里再点「去注册」→ 注册页那次请求的 Referer 是登录页地址。
  • 结论:浏览器会自动带 Referer,值就是上一个页面的地址。 一次次跳转,就串成一条路径。

    工程上的用途:

    • 分析「用户从哪个页面跳到哪个页面」,画转化漏斗;
    • 多个站点互相导流时,统计「哪个站点导过来的流量最大」;
    • 防盗链:图片只允许本站页面引用,外站引用的 Referer 不对,直接拒绝。

    💡 Referer 这个单词本身是拼错的(正确拼写是 Referrer),但错版已经写进规范,几十年没人改 —— 属于典型的「历史包袱变成标准」。


    10. Content-Type:正文到底是什么

    Content-Length 说的是「正文有多长」,Content-Type 说的是「正文是什么」。

    text/html ← 网页
    text/css ← 样式表
    image/png / image/jpeg ← 图片
    application/x-www-form-urlencoded ← 表单数据

    为什么必须带?有一个很直观的对照:

    之前不带 Content-Type,浏览器照样把 HTML 渲染出来了 —— 于是很容易得出结论「这个字段可有可无」。但只要换成返回图片,问题立刻暴露:不带类型,浏览器不知道这是图片,不会渲染,而是直接当成文件下载。

    所以不是浏览器不需要,是浏览器在替服务器猜。返回的类型一旦多起来,猜不准就是常态。规范要求带,是为了让接收方不用猜。

    ✔ 这也解释了 HTTP 为什么能「什么都能传」:图片、音频、视频、字体、JSON —— 在协议眼里都只是一段字节 + 一个类型标签。协议本身不懂内容,它只负责把「字节」和「它是什么」一起搬过去。

    网页里嵌视频就是同一件事:

    <video src="/video/1.mp4" width="640" height="360" controls></video>

    浏览器拿到网页后,看到 video 标签,再发一次请求去拿那个 .mp4;服务器返回时把 Content-Type 设成 video/mp4,浏览器才知道该调播放器。


    11. 打开一个网页 = 一串请求

    在这里插入图片描述

    11.1 首页是入口,站点是一棵树

    前端和后端通常是一家公司写的,所以站内的链接只会指向自家服务器上的合法文件。用户不会在地址栏里手敲路径 —— 他只会点。

    于是站点的结构天然是一棵树:

    • 根节点是首页(/);
    • 首页里嵌着指向登录页、注册页、图片的链接;
    • 每个链接点下去,就走到一个子节点。

    每个节点对应一次独立的 HTTP 请求。 这一点由浏览器完成,跟后端没关系:

  • 页面里的 <a href="/login.html"> 被点击;
  • 浏览器取出 href,拼成新的访问地址;
  • 浏览器对这个新地址发起一次全新的 HTTP 请求;
  • 拿到新页面,渲染。
  • <a href="https://www.qq.com">…</a> 这种绝对地址,点下去就是跳到别的站点;<a href="/login.html"> 这种相对地址,浏览器自动补成「当前站点的根 + 这个路径」。

    11.2 浏览器还会自己要东西

    除了页面里写的链接,浏览器自己还会发请求:

    资源谁发起的
    index.html 地址栏或链接
    image/1.png 页面里的 <img>
    1.css / 1.js 页面里的 <link> / <script>
    /favicon.ico 浏览器自己,为了标签页上的小图标

    favicon.ico 是一条容易被忽略的请求:它一定从站点根目录取,站点没放这个文件就是 404。


    12. 文件在磁盘上就是一个 char 数组

    在这里插入图片描述

    12.1 看到的「一行一行」是编辑器给的

    文本编辑器把文件显示成一行一行,看起来文件是「二维」的。但底层不是:

    • 换行只是内容里的两个字节 \\r\\n(Windows)或一个字节 \\n(Linux);
    • 磁盘上文件就是一串连续的字节。

    所以文件在内存里就是一个 char 数组。 所谓「第几行」,只是编辑器扫到 \\n 时做的排版。

    这和从 socket 读数据是完全一样的视角:从网络读上来是一维字节数组,从文件读进来也是一维字节数组。只要是一维数组,就需要一个位置记录「读到哪了」,否则下次不知道从哪继续。

    12.2 那个位置叫 pos

    struct file {
    int pos; // 读到第几个字节
    };

    Linux 内核管理一个打开的文件时,属性里就有这么一个「读写位置」。有了它:

    • 读 10 个字节 → pos 变成 10;
    • 再读 10 个 → 从 10 开始,读完 pos 变成 20;
    • 要算文件大小 → 把 pos 移到末尾,此时 pos 的值就是大小。

    12.3 怎么拿文件大小

    两种做法,都要会:

    做法一:fstream + seekg / tellg

    std::ifstream in(file_name, std::ios::binary);
    if (!in.is_open()) return –1;
    in.seekg(0, std::ios::end); // 读写位置移到结尾
    int size = in.tellg(); // 当前位置 = 文件大小
    in.seekg(0, std::ios::beg); // 还原到开头,后面还要读内容
    in.close();
    return size;

    seekg 就是 C 里 fseek 的 C++ 版本,tellg 对应 ftell。以二进制方式打开(std::ios::binary),文本文件和二进制文件都能用。

    做法二:stat 系统调用

    struct stat st;
    if (stat(path.c_str(), &st) == 0) {
    return st.st_size; // 文件大小
    }

    stat 直接把整个文件属性结构体带出来:inode、权限、硬链接数、UID/GID、大小、时间戳。拿大小只是顺手,这个接口的真正价值是「一次拿到全部属性」。

    💡 为什么非要单独算大小?因为 Content-Length 要填一个准确的数字。前面读文件内容时拿到的是「内容」,不是「长度」—— 除非额外调一次 size(),否则只能再数一遍。


    13. 状态码:五类,五种责任

    状态行的格式是:

    HTTP/1.1 200 OK
    ↑ ↑ ↑
    版本 状态码 描述

    描述是给人看的,程序判断只认数字。第一位数字定性,后两位细分。

    13.1 五类总览

    类别名字含义典型码
    1XX 信息性 请求已收到,处理还没完 100 Continue
    2XX 成功 请求正常处理完毕 200 201 204
    3XX 重定向 需要附加操作才能完成 301 302 304 307
    4XX 客户端错误 服务器无法处理这个请求 400 401 403 404
    5XX 服务器错误 服务器自己处理时出错 500 502 503

    13.2 1XX:先回一句「收到了,继续发」

    最典型的场景是大文件上传。传一个几百 MB 的文件,客户端不可能一口气发完,通常先把报头(带 Content-Length)发过去。服务器一看:知道这次要传大东西,但正文还早。于是先回一个 100 Continue:

    请求收到了,接着传,别停。

    这一类的意义就是防止客户端白等。日常很少见,因为除了上传大文件,很少有「处理时间明显长于传输时间」的场景。

    13.3 2XX:成功,但成功的样子不止一种

    码描述场景
    200 OK 一切正常 请求的网页/图片拿到了
    201 Created 新建了资源 发布一篇新文章,服务器入库并生成了新页面
    204 No Content 成功,但没有正文 删除一篇文章,操作成功,没有东西要返回

    204 最容易被误用成 200。区别在于语义:200 的意思是「要的东西在正文里」,204 的意思是「事情办成了,但正文是空的」。程序如果按 200 去读正文,读到空就会以为出错。

    13.4 3XX:换个地方

    重定向的意思是「要找的东西不在这儿(或者不只用这一份),去另一个地址」。

    • 登录后跳回原页面:在 B 站看视频,提示登录 → 点登录 → 登录成功后自动跳回刚才那个视频页。这个「跳回」就是重定向。
    • 换域名:老域名解析到新站,访问老域名自动跳过去。
    • App 互跳:打开某个 App,开屏广告点一下直接跳到淘宝 —— 这是一次重定向。
    • 域名层面的跳转:某些域名解析后指向的不是自己,而是另一台机器的 IP。

    重定向本身还能细分(301 永久、302 临时、304 未修改、307 保持方法),这一篇先不展开。

    13.5 4XX:客户端的责任

    码描述什么情况
    400 Bad Request 坏请求 请求格式不对,服务器解析不了
    401 Unauthorized 需要认证 这个资源要登录才能看
    403 Forbidden 禁止访问 登录了,但权限不够
    404 Not Found 找不到 这个路径下确实没有这个文件

    403 的典型场景是公司内网论坛:新入职的实习生看不了某些版块,即便登录了也一样 —— 权限不够。401 的典型场景是 VIP 视频:点开先弹登录框。

    404 值得单独论证一下归属问题。「访问的页面不存在」,到底怪谁?

    答案是怪客户端。理由:服务器上本来就没有这个文件,请求本身是非法的。类比一下 —— 去找人要一样东西,人家确实没有,不能因此说人家有问题,任何人都有能力边界。

    换个角度看更清楚:只要是在站点内部点链接访问,一般不会 404。 因为站内的链接一定指向自家服务器上的合法文件。真正会 404 的,都是用户在地址栏里自己拼出来的路径 —— 比如在京东后面手敲 /a/b/c.html,那个资源本来就不存在。

    ✔ 由此可以推出一条工程判断:404 的日志条目,多半来自用户手输、旧链接、爬虫猜测,而不是站内正常点击。

    13.6 5XX:服务端的责任

    码描述什么情况
    500 Internal Server Error 内部错误 代码崩了、数据库挂了
    502 Bad Gateway 网关错误 代理没从上游拿到有效响应
    503 Service Unavailable 服务不可用 过载,暂时处理不过来

    503 最好理解:大学选课系统在开放的那一刻,全校同时点,服务器扛不住 —— 页面直接报错,刷新几次偶尔能进去。这就是「过载」。

    500 在今天的代码里对应一个具体场景:TCP 服务器收到新连接后要 fork 一个子进程去处理,如果 fork 失败(进程数到上限),服务器自己就出错了。

    这里有一个漂亮的追问:服务器连子进程都创建失败了,它凭什么还能回一个 500 给客户端?

    答案是:失败的只是「创建子进程」这一步,父进程还活着。父进程手里的监听套接字还在、新连接的套接字也拿到了,所以它完全可以:

  • 自己在那个连接上写一份 500 响应(哪怕只是一张静态错误页);
  • 然后退出。
  • 也就是说,给客户端回错误这件事,不需要「处理请求」的能力,只需要「连接还在」这个条件。 只有当父进程本身也挂掉,才真的什么都回不了 —— 那时候客户端只能看到连接被重置。

    13.7 关于状态码的两个现实

    讲义里特意交代了两件事,都属于「规范之外的世界」:

    第一,状态码在工程实践里写得相当随意。 前后端联调时,很多人不管什么情况都填 200,把真正的错误塞进正文的 JSON 里。原因是浏览器重内容、轻报头:它真正在乎的是正文能不能渲染,状态码对不对很少影响页面显示。

    所以规范是规范,现实是现实。但自己写的时候还是应该认真填 —— 至少 4XX 和 5XX 要分清楚。

    第二,大公司不愿意返回 5XX。 一旦返回 500,等于对外承认「服务器内部出问题了」。这两个信息很有价值:既暴露了自己系统的短板,也可能被人拿来做攻击。所以大多数公司的做法是:内部报错 → 对外返回一个 4XX,甚至返回 200 + 一个「稍后再试」的页面。

    💡 这条能解释一个常见现象:明明网站挂了,抓包看到的却是 200 或 404。状态码首先是产品决策,其次才是技术要求。


    14. 以 404 为载体的异常处理

    理论落到代码,选 404 当样本,因为它最容易复现(随便请求一个不存在的文件就行)。

    14.1 让状态码和描述绑在一起

    状态码和描述永远成对出现(200 OK、404 Not Found),手写容易对不上。所以抽一个接口出来:

    void SetCode(int code) {
    code_ = code;
    switch (code) {
    case 200: desc_ = "OK"; break;
    case 404: desc_ = "Not Found"; break;
    default: desc_ = "Unknown"; break;
    }
    }

    这样只要给一个数字,状态行需要的东西就齐了。以后要加 301、500,就在 switch 里多一个分支。

    14.2 报头也走同一个接口

    响应报头也是一堆 Key: Value,所以同样抽一个:

    void SetHeader(const std::string& key, const std::string& value) {
    headers_[key] = value;
    }

    序列化的时候遍历这个 map,每个键值对拼成 Key: Value\\r\\n,最后补一个 \\r\\n 收尾。这就是「自己写的序列化」 —— 没有第三方库,只有字符串拼接。

    ✖ 一个真实的坑:这个 map 不能写成全局的或者多个响应共用的,否则会出现 §12.3 那种「后写的覆盖前写的」的情况。每条响应各有一份。

    14.3 读不到文件时怎么办

    MakeResponse() 里的流程:

    目标路径 = webroot + uri
    读这个文件
    成功 → SetCode(200),正文 = 文件内容,SetHeader("Content-Length", 文件大小)
    失败 → SetCode(404),目标路径 = webroot + "/404.html",重新读一次

    关键在第 3 步:失败之后不是什么都不返回,而是换一个文件读。 因为协议规定,响应必须有正文(至少得有个能显示的东西),不能让浏览器拿到一片空白。

    所以站点根目录下必须有一个 404.html:

    • 它是「错误页」,不是「错误码」;
    • 报头里写 404 Not Found,正文里放这张网页;
    • 浏览器照样能把这张网页渲染出来,用户看到的是「访问的资源不存在」。

    ✔ 状态码和正文是两件独立的事,可以各换各的。报头说「错了」,正文说「错成什么样」。

    改完之后实测:GET /a/b/c.html HTTP/1.0 → 返回 404 + Content-Length: 1506 + 一张 404 页面。长度对得上,说明换文件之后重新算过大小 —— 这一步如果漏了,就会掉进前面那个「长度不匹配」的坑。

    💡 更规范的做法是用重定向(302)把浏览器导向 /404.html,这样地址栏也会变。但重定向本身要讲一节,这里先用「换正文」的土办法。


    15. 爬虫和代理服务器

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » HTTP理解(2)请求行字段解读和状态码初认识
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址