JavaWeb 复习讲义
原始讲义
第二部分 协议与容器:请求是怎么到达 Java 代码的

第4章 HTTP 协议与响应状态码

难度:★★☆☆☆ | 前置知识:第1~3章(HTML、CSS、JavaScript) | 本章解决:看懂浏览器与服务器之间到底传了什么,并能凭一个状态码判断问题出在客户端、服务器还是缓存

本章导读

4.1 HTTP 概述与一次请求的完整旅程

4.1.1 HTTP 是什么

概念:HyperText Transfer Protocol,超文本传输协议,规定了浏览器和服务器之间数据传输的规则。

这句话里每个词都值得停一下。

"超文本"(HyperText) 指的是"文本里还能链到别的文本",也就是第 1 章讲的超链接。HTTP 诞生时只用来传 HTML 这种超文本文档,后来把图片、CSS、JavaScript、JSON、视频全都装了进去,名字却留了下来。所以今天你看到 Content-Type: image/jpeg 时不要奇怪:它传的早就不是"文本"了,但协议名不改。

"传输协议" 说明它是一份约定,不是一段程序。约定的是"报文长什么样、谁先说话、说完了怎么表示结束",而不是"数据怎么在网线里变成电信号"。后者是 TCP 的事。

"浏览器和服务器之间" 圈定了它的作用范围:HTTP 是应用层协议,只为这一次"我要一个资源"和"给你一个资源"服务。它不管路由、不管数据库、不管页面长得好不好看。

浏览器与 Web 服务器之间有两个方向的数据流:浏览器发出的是请求数据,服务器返回的是响应数据。用字符画表示这个关系:

浏览器(客户端)  ──────  请求(请求数据)  ──────▶  Web 服务器
                 ◀──────  响应(响应数据)  ──────

两个箭头永远不会同时出现。HTTP 是"一问一答"的:没有请求就没有响应,服务器不会主动找浏览器说话。第 5 章的 Tomcat 之所以叫"Web 服务器",正是因为它唯一的本职工作就是解析 HTTP 请求、生成 HTTP 响应。

【补充】

HTTP 目前有三个常用版本:HTTP/1.0、HTTP/1.1、HTTP/2。考试与课程实验默认都是 HTTP/1.1,本章所有报文示例也都是 1.1。三个版本最直观的差别是连接复用:1.0 默认一次请求一条 TCP 连接,用完就断;1.1 默认 keep-alive,一条连接上可以连续跑多个请求;2.0 更进一步,一条连接上并行跑多个请求,并且把请求头压缩成二进制。你在开发者工具里看到的 HTTP/1.1 就是这套规则的版本号。

4.1.2 HTTP 协议的三个特点

HTTP 协议的特点可以归纳为三条,这三条是本章后面所有内容的基础。

1. 基于 TCP 协议:面向连接,安全

HTTP 自己不管"数据包会不会丢、顺序对不对",这些活全部交给下层的 TCP。所以一次 HTTP 通信的真实顺序是:先建立 TCP 连接,再在上面发 HTTP 报文,用完再关(或复用)。

2. 基于请求-响应模型的:一次请求对应一次响应

浏览器不发请求,服务器就一直等着;服务器回了响应,这一次交互就结束了。请求和响应是严格配对的,不会出现"一个请求两个响应",也不会出现"没有请求却来了响应"。

3. HTTP 协议是无状态的协议:对于事务处理没有记忆能力。每次请求-响应都是独立的。

这是三条里最容易被误解的一条。服务器处理完第一个请求,就把这个客户端忘得干干净净;第二个请求进来,它完全不知道"这还是刚才那个人"。

【补充】

"基于 TCP"落到实处的三件事,都可以在代码里看到影子。第一是端口:HTTP 默认 80,HTTPS 默认 443,Tomcat 默认 8080,所以本地访问要写 localhost:8080,不写端口浏览器就按 80 去找,自然找不到。第二是三次握手:客户端发 SYN,服务器回 SYN+ACK,客户端再回 ACK,三次之后连接才建立,HTTP 报文才开始发。第三是连接复用:HTTP/1.1 默认 Connection: keep-alive,一次握手之后这条连接可以接着跑后面的 CSS、JS、图片请求,省掉反复握手的时间。

【易错点】 "无状态"不等于"不能登录",也不等于"服务器不记任何东西"。准确说法是:协议层不记忆。第 7 章的 Session 机制是服务器自己在内存里建一张表,用 JSESSIONID 这个 Cookie 当钥匙,把多次请求认成同一个人。协议一个字都没改,是应用层在 HTTP 上面额外搬了一个标识。就像物流公司的运单号:物流公司并不"记得你",是你自己每次寄件都报同一个单号。

【对比】 "无状态"和"无连接"是两个概念,很容易被混成一句。无连接说的是连接层面的做法:HTTP/1.0 一次请求一条 TCP 连接,用完就断,所以叫无连接;HTTP/1.1 默认持久连接,这条已经不成立了。无状态说的是记忆层面:不管连接复用多少次,服务器都不会因为"这条连接刚才来过"就记住你的身份。1.1 时代是"有连接、无状态"。

【考点】 为什么说无状态是优点?因为服务器不必为每个客户端保存上下文,任何一个请求都可以交给集群里任意一台机器处理,加机器就能扩容;反过来,正因为不保存上下文,才需要 Cookie 和 Session 把状态"外挂"出来。答题时这两句一前一后,正好解释第 7 章存在的理由。

4.1.3 一次请求的完整旅程

前面三节是"定义",这一节把它串成一条线。请把下面这条线当成一章的目录来记:你在地址栏敲下回车之后,到看见页面为止,中间发生了八件事。

第一步:地址栏回车,浏览器解析 URL。

你在地址栏输入 http://www.itcast.cn/brand/findAll?name=华为&status=1,浏览器做的第一件事是把这一串字符拆成零件,此时它还没有发出请求:

http://www.itcast.cn:80/brand/findAll?name=华为&status=1#top
└─┬──┘  └────┬─────┘└┬┘└──────┬──────┘└────────┬────────┘└┬─┘
 协议       主机名   端口   资源路径          查询串      锚点

拆出来的五个零件后面各有各的用处:协议决定用 80 还是 443、要不要加密;主机名要拿去查 IP;端口决定敲服务器的哪扇门;资源路径决定要哪个资源;查询串是 GET 请求的参数。锚点(#top)不会发给服务器,它只留在浏览器里负责滚动定位:这是判断"参数到底有没有发出去"的第一个分水岭。

第二步:DNS 解析,把域名换成 IP。

网络只认 IP,不认域名。浏览器按"浏览器 DNS 缓存 → 操作系统 hosts 文件 → 本地 DNS 服务器 → 逐级递归查询"的顺序找,直到拿到一个形如 42.186.77.21 的地址。这一步失败的表现是"网页打不开、提示找不到服务器",而不是"页面报错"。

第三步:TCP 三次握手,建立连接。

拿到 IP 和端口,客户端发起 TCP 连接:SYN → SYN+ACK → ACK。三次握手成功,一条可靠的管道才铺好。到这一步为止,一个字节的 HTTP 都还没发出去。

第四步:浏览器组装并发送请求报文。

现在才轮到 HTTP 出场。浏览器按 4.2 节的格式拼出一段文本:

GET /brand/findAll?name=华为&status=1 HTTP/1.1
Host: www.itcast.cn
User-Agent: Mozilla/5.0 Chrome/91.0.4472.106
Accept: text/html,application/xhtml+xml,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.9
Accept-Encoding: gzip, deflate

注意最后那个空行,它是"请求头到此结束"的分隔符,后面如果有内容就是请求体。GET 请求通常没有体,所以报文到空行就结束了。

第五步:服务器解析报文并处理。

请求到达服务器,第 5 章的 Tomcat 接住它:先解析请求行拿到路径 /brand/findAll,再按 urlPattern 找到对应的 Servlet,把请求头、查询串、请求体统统封装进第 6 章的 Request 对象,调用 service() 方法。Servlet 里可能还要用第 10 章的 JDBC 去数据库查品牌列表,查完把结果写进 Response。

第六步:服务器回响应报文。

服务器按 4.3 节的格式拼出另一段文本:

HTTP/1.1 200 OK
Content-Type: text/html;charset=UTF-8
Content-Length: 1024

<html>……</html>

同样是"起始行 + 若干头 + 空行 + 正文"的结构,只不过起始行从请求行变成了响应行,里面多了那个三位数的状态码。

第七步:浏览器解析并渲染。

浏览器拿到 HTML,开始第 3 章讲的那套流程:解析标签构建 DOM 树,遇到 <link> 就去下载 CSS(第 2 章),遇到 <script> 就暂停解析去下载并执行 JS(第 3 章),遇到 <img> 就再发一个请求取图片。关键点在这里:CSS、JS、图片每一个都是一次独立的新请求,走的是完整的第二步到第六步。一个普通页面的开发者工具里躺着二十几条请求,原因就在这里。

也正是这一步解释了"无状态"为什么是问题:这二十几个请求在服务器眼里是二十几个陌生人,它不知道它们属于同一个登录用户。要让它们串起来,只能靠第 7 章的 Cookie/Session 额外带一个身份标识。

第八步:连接收尾。

如果响应头里是 Connection: keep-alive,这条 TCP 连接先留着,后面的请求接着用;否则四次挥手断开。浏览器自己也有连接池上限(同域名通常 6 条),超出的请求要排队,这就是开发者工具里 Queued 或 Stalled 那一小段耗时的来源。

【补充】

为什么开发者工具的 Network 面板里看不到 DNS 和 TCP?因为它只记录 HTTP 层的事件。想看这两段,要把请求点开、切到 Timing 面板:Queued 是排队等连接,Stalled 是等待可用连接,DNS Lookup 是查域名,Initial connection 是三次握手,SSL 是 TLS 握手,Request sent 是发出请求,Waiting (TTFB) 是等服务器第一个字节,Content Download 是下载响应体。这条时间轴和上面的八步是严格对应的。

【易错点】 "页面打不开"和"页面报错"是两类完全不同的故障,排错入口也不同。DNS 失败、TCP 连不上(比如 Tomcat 没启动、端口写错)时,浏览器给的是"无法访问此网站 / ERR_CONNECTION_REFUSED",Network 面板里根本不会有这条请求;而 404、500 是请求已经到达服务器、服务器明确回了话,Network 面板里一定有记录,还带状态码。先看有没有记录,能省掉一半排查时间。

【考点】 八步的顺序是高频考点:解析 URL → DNS → TCP 三次握手 → 发请求报文 → 服务器处理 → 回响应报文 → 浏览器渲染(并继续发子请求)→ 关闭或复用连接。特别要记住:DNS 在 TCP 之前,TCP 在 HTTP 之前;HTTP 报文是在连接建立之后才发的。

【对比】 HTTP 与 TCP 的关系可以这样说:TCP 负责"把字节可靠地送到",HTTP 负责"这些字节怎么解释"。所以 TCP 层出问题表现为超时、连接重置,HTTP 层出问题表现为状态码不对、报文格式不对。看到 ERR_CONNECTION_RESET 不要去找状态码,那是 TCP 层的事。

4.2 请求消息结构与常见请求头

4.2.1 请求数据的三个部分

请求数据分为 3 部分:

1. 请求行:请求数据的第一行。其中 GET 表示请求方式,/ 表示请求资源路径,HTTP/1.1 表示协议版本。

2. 请求头:第二行开始,格式为 key:value 形式。

3. 请求体:POST 请求的最后一部分,存放请求参数。

先看一个 GET 请求的实际样子:

GET / HTTP/1.1
Host: www.itcast.cn
Connection: keep-alive
User-Agent: Mozilla/5.0 Chrome/91.0.4472.106
...

第一行是请求行,从第二行起全是请求头,最后那个 ... 表示后面还有若干请求头(Accept、Accept-Language、Accept-Encoding、Cookie 等)。整个报文到此结束,没有请求体,因为这是 GET。

再看同一个网站上的 POST 请求:

POST / HTTP/1.1
Host: www.itcast.cn
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 Chrome/91.0.4472.106

username=superbaby&password=123456

对比一下就能看出区别:请求行里的方法从 GET 变成了 POST;请求头多了一个描述缓存策略的 Cache-Control 和一个表示"请把不安全的请求升级为 https"的 Upgrade-Insecure-Requests;最关键的是请求头后面有一个空行,空行之后跟着 username=superbaby&password=123456,这就是请求体。

把两个报文的每一行归到三个部分里,是这样的:

报文位置属于哪一部分内容作用
第 1 行请求行GET / HTTP/1.1说明用什么方式、要哪个资源、按哪个版本
第 2 行 ~ 空行前请求头Host: www.itcast.cn 等描述这次请求的附加信息
空行之后请求体username=superbaby&password=123456只有 POST 才有,存放提交的数据
【补充】

报文里每一行都以 CRLF(回车换行,\r\n)结尾,请求头与请求体之间那个"空行"就是一个孤零零的 CRLF。这个空行起语法分隔符的作用,不是为了排版好看:服务器读到连续两个 CRLF 就知道"头结束了,下面开始是正文"。少了它,服务器会把请求体当成请求头继续解析,然后报 400。第 5 章如果自己用 Socket 写一个简易服务器,这个空行是必须手写的。

【易错点】 请求头里的冒号是半角冒号,Host: www.itcast.cn,冒号后面通常有一个空格。写成中文全角冒号 Host:www.itcast.cn 时,服务器解析不出键名,整条请求可能被判为 400。抄报文的时候一定要用英文输入法。

4.2.2 请求行的三个要素

请求行虽然只有一行,却包含三个信息,考试经常拆开问。

组成部分示例含义
请求方式GET这次请求想对资源做什么(取、交、删……)
请求资源路径/brand/findAll?name=华为&status=1要访问的资源,? 后面是查询串
协议版本HTTP/1.1本次通信用哪一版 HTTP 规则

请求方式:课程里主要用 GET 和 POST 两种,第 6 章还会见到 HEAD、PUT、DELETE、OPTIONS。方法名必须大写,服务器是按大写匹配的。

请求资源路径:这里是"路径 + 查询串"。? 之前是路径,? 之后是"键=值&键=值"形式的查询串,& 用来分隔多个参数。注意路径里不含域名,也不含 http:// 这类协议前缀。

协议版本:固定写 HTTP/1.1。它告诉服务器"我按 1.1 的规则发报文的,你也可以按 1.1 的规则回我",比如 1.1 默认长连接,1.0 默认短连接。

【补充】

请求行里为什么看不到域名?因为域名放在 Host 请求头里。这样设计是为了实现虚拟主机,并非疏漏:一台服务器(一个 IP)上可以挂几十个网站,服务器收到请求后靠 Host 里的域名判断你要访问哪一个站。所以 Host 是 HTTP/1.1 中唯一一个必填的请求头,缺了它服务器直接回 400。你本地用 localhost:8080 访问时,Host 就是 localhost:8080。

【易错点】 查询串属于请求行,不属于请求体。GET /brand/findAll?name=华为 HTTP/1.1 这一整行里,name=华为 是请求行的一部分。这个细节在第 7 章讲中文乱码时会用到:GET 的参数在 URL 里,URL 的编码由 URIEncoding 决定;POST 的参数在请求体里,编码由 request.setCharacterEncoding() 决定。两者解法不同,原因就在"参数到底在请求行还是在请求体"。

【考点】 一个请求的 URL 是 http://localhost:8080/web-demo/user/login?username=tom,请说出:请求路径是 /web-demo/user/login,查询串是 username=tom,web-demo 是项目名(contextPath),/user/login 是 Servlet 映射的路径。第 5 章配置 urlPattern 时要填的正是 /user/login 这一段,不含项目名。

4.2.3 常见的 HTTP 请求头

五个最常见的请求头如下,考试默写和实际排查都用得上:

请求头含义
Host表示请求的主机名
User-Agent浏览器版本,例如 Chrome 浏览器的标识类似 Mozilla/5.0 ... Chrome/79,IE 浏览器的标识类似 Mozilla/5.0 (Windows NT ...) like Gecko
Accept表示浏览器能接收的资源类型,如 text/*,image/* 或者 */* 表示所有
Accept-Language表示浏览器偏好的语言,服务器可以据此返回不同语言的网页
Accept-Encoding表示浏览器可以支持的压缩类型,例如 gzip, deflate 等

逐个补充一句用途:

【补充】

POST 请求还会多一个关键请求头 Content-Type,用来说明请求体是什么格式。最常见的三种:application/x-www-form-urlencoded(表单默认格式,内容就是 username=superbaby&password=123456 这种键值对)、multipart/form-data(带文件上传的表单,第 6 章文件上传会用到)、application/json(前后端分离项目里传 JSON)。服务器解析请求体之前必须先看这个头,没有它就只能靠猜,这也是"POST 参数怎么都拿不到"的常见原因之一。

【补充】 Connection: keep-alive 的意思是"这次响应之后先别断 TCP 连接,我下一个请求还走这条"。HTTP/1.1 默认就是这个行为,所以前面两个示例里都带着它。与之相对的是 Connection: close,表示"回完这一次就断开"。

【易错点】 User-Agent 是客户端自己填的,可以任意伪造,一条 curl -H "User-Agent: 任意内容" 就能改掉。所以用它做"只有手机能访问""只有自家 App 能调"这类判断,只能挡住普通用户,挡不住恶意请求。安全校验要靠第 7 章的身份认证和第 9 章的过滤器。

【考点】 Accept、Accept-Language、Accept-Encoding 三个词长得很像,考的就是能不能分清:Accept 管类型,Accept-Language 管语言,Accept-Encoding 管压缩。三者都是浏览器告诉服务器"我这边能接受什么",服务器据此做内容协商,但服务器完全可以不理。

4.2.4 GET 请求到底有没有请求体

一句常见的结论是"GET 请求没有请求体"。这句话作为工程结论完全正确,但如果你去翻 HTTP 规范,会发现它没有明文禁止,于是一个很常见的追问就来了:GET 能不能带请求体?

【补充】

规范(RFC 7231)的原话大意是:GET 的请求体没有定义语义,服务器应当忽略它。翻译成三句话:第一,协议没有明文禁止,你可以发;第二,没有任何标准规定这个体该怎么解释;第三,服务器可以合法地装作没看见。落到实际:

  • 浏览器端的表单、fetch、XMLHttpRequest 在 GET 时都不会带请求体;
  • Tomcat 收到带体的 GET,也不会把它当表单参数解析,request.getParameter() 拿不到;
  • 所以工程上"GET 没有请求体、参数只放查询串"是铁律。

面试时标准答法是:"协议允许但无语义,工程上一律不放。"

【易错点】 有人用 fetch 发 method: 'GET' 却带了一个 body,然后奇怪后端为什么收不到参数。后端 API 并没有写错,原因在协议层不认这个体。要么改成 POST,要么把参数拼进查询串。

【对比】 "GET 有请求体"和"POST 有查询串"是两件对称的事,都属"能做但没意义"。POST /user/login?from=app HTTP/1.1 语法完全合法,查询串也会被服务器解析,但没人这么写。判断依据是"约定俗成的参数位置在哪里",不在于"能不能"。这才是那两条区别要表达的意思。

4.3 响应消息结构与常见响应头

4.3.1 响应数据的三个部分

请求是"浏览器说、服务器听",响应反过来,但结构几乎对称。

响应数据分为 3 部分:

1. 响应行:响应数据的第一行。其中 HTTP/1.1 表示协议版本,200 表示响应状态码,OK 表示状态码描述。

2. 响应头:第二行开始,格式为 key:value 形式。

3. 响应体:最后一部分,存放响应数据。

一个真实的响应长这样:

HTTP/1.1 200 OK
Server: Tengine
Content-Type: text/html
Transfer-Encoding: chunked

<html>
<head>
<title></title>
</head>
<body></body>
</html>

逐行对照:

报文位置属于哪一部分内容作用
第 1 行响应行HTTP/1.1 200 OK协议版本、状态码、状态码描述
第 2 行 ~ 空行前响应头Server、Content-Type、Transfer-Encoding描述这次响应的附加信息
空行之后响应体<html>……</html>真正要显示给用户的内容

和请求报文相比,只有第一行不同:请求行开头是"方法 + 路径 + 版本",响应行开头是"版本 + 状态码 + 描述"。这个差别值得单独记一句:请求行以"要什么"开头,响应行以"结果如何"开头。

【易错点】

响应头和响应体之间必须有一个空行。自己用 Socket 手写响应时忘了这个空行,浏览器会把后面的 HTML 当成响应头的一部分去解析,页面上什么也不显示,Network 面板里却显示 200,这是最费时间的一类 bug,因为状态码看起来完全正常。

【补充】 Transfer-Encoding: chunked 表示"响应体是分块发送的,总长度事先不知道"。动态页面(Servlet、JSP 边算边输出)没法事先知道要输出多少字节,就用分块:每一块前面标好本块长度,最后用一个长度为 0 的块表示结束。它和 Content-Length 是互斥的:响应头里出现 Transfer-Encoding 就不会再出现 Content-Length。静态文件服务器知道文件大小,通常直接给 Content-Length,你下载文件时看到的进度条就是靠它算出来的。

4.3.2 常见的 HTTP 响应头

四个最常见的响应头如下:

响应头含义
Content-Type表示该响应内容的类型,例如 text/html,image/jpeg
Content-Length表示该响应内容的长度(字节数)
Content-Encoding表示该响应压缩算法,例如 gzip
Cache-Control指示客户端应如何缓存,例如 max-age=300 表示可以最多缓存 300 秒

这四个头各自解决一个问题:

【补充】

Content-Type 里除了类型,还带着字符集,完整写法是 text/html;charset=UTF-8。这一件事在第 6 章会变成一个 API:response.setContentType("text/html;charset=UTF-8"),一行代码同时干两件事:告诉浏览器"按 HTML 渲染"和"按 UTF-8 解码"。只写 response.setCharacterEncoding("UTF-8") 而不设 Content-Type,浏览器可能仍按默认编码(很多环境是 ISO-8859-1)解码,中文照样是乱码。响应乱码九成以上是这两件事没配对。

【易错点】 Content-Type: text/plain 与 text/html 的差别不是"好不好看"。前者浏览器原样显示源码,后者才渲染成页面。调试接口时更常见:后端返回 JSON,却把 Content-Type 写成 text/html,前端 fetch 拿到的仍是字符串,某些库会因类型不符直接抛解析错误。正确写法是 application/json;charset=UTF-8。

【对比】 Content-Length 和 Transfer-Encoding 是两条互斥的路:前者"先报总长度,再发内容",浏览器心里有数;后者"一边发一边报每块多长",浏览器收到长度为 0 的块才知道结束。同时出现属于非法报文,服务器一般会以 Transfer-Encoding 为准,或者直接回 400。

【补充】 Cache-Control: max-age=300 属于强缓存:300 秒内浏览器再需要这个资源,连请求都不发,直接用本地副本,开发者工具里 Size 一栏显示 (disk cache) 或 (memory cache),状态码仍然显示 200,但根本没有走网络。这跟 304 不是一回事:304 是"问过服务器之后才用本地副本"。两者在 4.6.2 节放在一起对照。

4.3.3 状态码和原因短语从哪来

响应行 HTTP/1.1 200 OK 里的 200 和 OK 是两个不同的东西,很多人以为它们必须配套。

【考点】

浏览器只认三位数字,OK 只是给人看的原因短语。服务器完全可以回 HTTP/1.1 200 一切正常,浏览器照样当成功处理。但数字不能乱写:写 200 就是成功语义,写 404 就是资源不存在语义,浏览器的行为(是否跳转、是否用缓存、是否显示错误页)完全由数字决定。

【补充】 第 6 章的 response.setStatus(404) 改的就是响应行中间那个数字,而 response.sendError(404, "商品不存在") 除了改数字,还会清空已写出的内容、让容器接过去生成一个标准错误页。两者的区别在 4.6.5 节展开。

4.4 GET 与 POST 的全面对比

这一节解决一个从第 1 章拖到现在的老问题:写 <form> 时 method 到底该填什么。

教材给出的两条区别是:

区别一:GET 请求请求参数在请求行中,没有请求体。POST 请求请求参数在请求体中。

区别二:GET 请求请求参数大小有限制,POST 没有。

把两条区别画成报文,一眼就能看出来:

GET /brand/findAll?name=华为&status=1 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 Chrome/91.0.4472.106

这是 GET:参数全在请求行的查询串里,请求头之后报文就结束了,没有请求体。

POST /brand/add HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 38
User-Agent: Mozilla/5.0 Chrome/91.0.4472.106

name=华为&status=1&description=新品

这是 POST:请求行只有路径 /brand/add,一个参数都看不到,参数全在空行之后的请求体里;而且多了一个 Content-Type 说明体的格式,多了一个 Content-Length 说明体有多长。两个报文放在一起看,区别就是"参数在不在那一行"与"有没有空行之后的体"。

把两条区别展开成一张完整的对比表:

对比项GETPOST
参数位置请求行(URL 的查询串)请求体
报文结构请求行 + 请求头 + 空行,无体请求行 + 请求头 + 空行 + 请求体
大小限制有,受浏览器与服务器限制没有 URL 那种硬性上限
是否出现在地址栏是否
是否进浏览器历史、可收藏是否
是否被浏览器缓存默认可以缓存默认不缓存
幂等性幂等(重复发结果一样)不幂等(重复发可能重复下单)
典型用途查询、搜索、分页、跳转新增、修改、删除、登录
中文乱码的处理改 URL 编码(URIEncoding)request.setCharacterEncoding("UTF-8")

以下五段补充,是针对"区别二"和上表里最容易被误解的几格展开的。

【补充】

"GET 请求参数大小有限制"这句话,限制并不来自 HTTP 规范。规范对 URL 长度没有任何规定,限制来自三处实现:

  1. 浏览器:老版本 IE 的 URL 上限约 2083 个字符;Chrome 地址栏可以显示很长的 URL,但实际发出前也会截断。
  2. 服务器:Tomcat 的 maxHttpHeaderSize 默认 8192 字节(8KB),请求行加所有请求头合计不能超过它,超了直接回 400 或 431;Nginx 的 large_client_header_buffers 默认也是 8KB。
  3. 中间设备:代理、网关、CDN 各有各的上限。

所以准确说法是"GET 的参数受 URL 长度限制,而这个长度上限由浏览器和服务器实现决定"。答题时补上这句,比只答"有长度限制"高出档次。

【易错点】 把"POST 没有大小限制"当真理。Tomcat 对 application/x-www-form-urlencoded 的表单体有 maxPostSize,默认 2MB,超了之后 request.getParameter() 直接返回 null,不报错、不提示,这是"大表单提交后参数全是 null"的经典原因。文件上传走另一条路,由第 6 章 @MultipartConfig 的 maxFileSize、maxRequestSize 控制。所谓"POST 没有限制",只是"没有 URL 那种几 KB 的硬上限",不是说想传多大传多大。

【对比】 GET 与 POST 的本质区别在语义,与"参数放哪"无关:GET 是"取",应当幂等,同一个请求发一百次,服务器状态不变,结果一样;POST 是"交",不幂等,重复提交可能生成一百条订单。参数位置、能否缓存、能否收藏,全都由这个语义差异决定。考试问区别时,先答语义与幂等,再答参数位置和长度,条理立刻清楚。

【易错点】 POST 并不比 GET 安全。两者默认都是明文传输,除非上 HTTPS,否则在同一个网络里用抓包工具都能看到内容;开发者工具的 Payload 面板也一样能把 POST 的请求体看得一清二楚。POST 的优势只是"不出现在地址栏、不进历史记录、不完整写进访问日志",降低了泄露面,它不是加密。安全靠 HTTPS 加密传输、靠第 7 章的身份校验。

【考点】 表单不写 method 属性时默认是 GET(第 1 章 <form> 的默认值)。所以"注册成功后密码出现在地址栏里"这个经典事故,原因就是 <form> 忘了写 method="post"。同理,用 <a> 标签或地址栏直接访问一个只处理 POST 的 Servlet,浏览器发的是 GET,服务器会回 405。

4.5 响应状态码分类

状态码是服务器在响应行里告诉浏览器"这次处理结果如何"的三位数字。它是整章最实用的部分:出问题时,你第一眼看的就是它。

状态码的大类划分如下,这张表要整张记住:第一位数字决定大类,后两位只是序号。

状态码分类说明
1xx响应中——临时状态码,表示请求已经接受,告诉客户端应该继续请求或者如果它已经完成则忽略它
2xx成功——表示请求已经被成功接收,处理已完成
3xx重定向——重定向到其它地方:它让客户端再发起一个请求以完成整个处理。
4xx客户端错误——处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等
5xx服务器端错误——处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP 版本不支持等

状态码大全:https://cloud.tencent.com/developer/chapter/13553

这张表只有五行,但信息量很大,需要拆开理解。

1xx 是"话还没说完"。 服务器先回一个临时状态,意思是"请求我收到了,你继续"。它不表示最终结果,所以你在开发者工具里几乎看不到它。

2xx 是"办成了"。 最常见的 200 属于这一类,含义是"请求已经被成功接收,处理已完成"。

3xx 是"换个地方拿"。 注意它不是错误:服务器好好地收到了请求,只是告诉你"资源不在这儿"或"你手上的就是最新的"。浏览器会自动再发起一个请求(302),或者直接用缓存(304)。

4xx 是"你请求错了"。 责任在客户端:地址写错、没有权限、方法用错、参数不合法。

5xx 是"我处理崩了"。 责任在服务端:代码抛异常、数据库连不上、服务没起来。

【补充】

为什么状态码要设计成"三位数字、第一位分类"?因为这样未知状态码也能被正确归类。如果哪天你看到一个从没见过的 418,不用查文档,看首位是 4 就知道是客户端错误;看到 507 就知道是服务器资源不足。这种"可扩展的分类法"是 HTTP 设计得最漂亮的地方之一。

【补充】 1xx 在实际开发中为什么几乎见不到?因为它在"服务器还在处理、先把头回给你"时才出现,而大多数请求处理得太快,等不到这一步。能见到的两个:一是 100 Continue,客户端有一大坨数据要上传,先问一句"你能收吗",服务器回 100 才开始传;二是 101 Switching Protocols,升级协议,WebSocket 握手成功返回的就是它,之后这条连接就不再走 HTTP 了。

【对比】 3xx 常被误解成"出错"。三类的区别可以用一句比喻说清:3xx 是"请到隔壁窗口办理"(302)或"你这张复印件还有效"(304),4xx 是"你的材料不对",5xx 是"我们窗口的电脑死机了"。前两种服务器都在正常工作。

【考点】 "4xx 和 5xx 谁的责任"这个问题,是排错的分水岭。判断规则只有一条:看服务器有没有能力处理这个请求。能处理但请求本身不合法 → 4xx,改请求(前端);收到合法请求但处理时抛了异常 → 5xx,改代码(后端)。这一条在 4.6.4 节会用 404 和 500 详细展开。

4.6 常见状态码详解与排错场景

常见状态码如下,这张表要完整记住,因为考试很可能直接考其中某一行。

状态码英文描述解释
200OK客户端请求成功,即处理成功,这是我们最想看到的状态码
302Found指示所请求的资源已移动到由Location响应头给定的 URL,浏览器会自动重新访问到这个页面
304Not Modified告诉客户端,你请求的资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向
400Bad Request客户端请求有语法错误,不能被服务器所理解
403Forbidden服务器收到请求,但是拒绝提供服务,比如:没有权限访问相关资源
404Not Found请求资源不存在,一般是URL输入有误,或者网站资源被删除了
428Precondition Required服务器要求有条件的请求,告诉客户端要想访问该资源,必须携带特定的请求头
429Too Many Requests太多请求,可以限制客户端请求某个资源的数量,配合 Retry-After(多长时间后可以请求)响应头一起使用
431 Request Header Fields Too Large请求头太大,服务器不愿意处理请求,因为它的头部字段太大。请求可以在减少请求头域的大小后重新提交。
405Method Not Allowed请求方式有误,比如应该用GET请求方式的资源,用了POST
500Internal Server Error服务器发生不可预期的错误。服务器出异常了,赶紧看日志去吧
503Service Unavailable服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好
511Network Authentication Required客户端需要进行身份验证才能获得网络访问权限

表格之外,再看一张"什么时候会真的遇到它"的对照表,这张表把状态码和你的实际开发场景绑在一起:

状态码真实出现场景在 JavaWeb 里通常由谁产生
200打开任意一个正常页面;接口返回 JSON容器默认;response.getWriter().write()
302未登录访问后台被弹回登录页;下单成功后跳订单列表response.sendRedirect()
304第二次刷新页面,CSS、JS、图片都没变Tomcat 的 DefaultServlet 比对 If-Modified-Since
400把 id=abc 传给一个 int 参数;请求头格式坏掉Tomcat 解析失败;参数绑定失败
403访问 WEB-INF 下的资源;目录禁止列表;权限过滤器拦截容器安全约束;Filter
404URL 打错一个字母;Servlet 映射写错;访问已删除的图片容器找不到资源
428接口要求带 If-Match 才能修改业务代码主动 sendError(428)
429一分钟内连续点登录 20 次被限流网关、拦截器、Filter
431Cookie 攒得太多太大Tomcat 拒绝解析超长请求头
405表单写了 method="post",Servlet 只重写了 doGet容器 HttpServlet.service() 分发
500Servlet 里空指针;SQL 写错;JSP 编译失败业务代码抛出异常
503项目还在启动;连接池耗尽;停机维护容器或网关
511连公司或酒店 Wi-Fi,未认证就上网网络接入设备(认证门户)

下面按大类逐个说清,重点放在"遇到它该做什么"。

4.6.1 2xx:成功

200 OK 是最常见的一个:客户端请求成功,即处理成功,这是我们最想看到的状态码。页面正常打开、接口正常返回数据,都是 200。

同一大类里还有两个值得知道的成员:

【补充】

201 Created 表示"资源创建成功",通常在新增接口里返回,并配合 Location 响应头告诉客户端新资源的地址;204 No Content 表示"处理成功,但没有响应体",用在删除成功、修改成功这类"不需要回数据"的场景。课程实验里这两个用得少,看到时知道是成功即可。

200 也可能"假成功"。 有些后端把所有业务错误都包在 200 里返回,比如 {"code": 500, "msg": "库存不足"}。这时 HTTP 层是 200,业务层是失败。

【易错点】

排查问题时不能只看状态码。看到 200 就以为万事大吉,结果响应体里是一段错误 JSON,这是前后端联调时最常见的误判。正确做法是:状态码看"通信层",响应体看"业务层",两层都要看。

4.6.2 3xx:重定向与缓存

3xx 里的两个主角是 302 和 304,它们都属于"重定向",但行为完全不同,是本节的绝对重点。

302 Found:指示所请求的资源已移动到由 Location 响应头给定的 URL,浏览器会自动重新访问到这个页面。

拆开看,302 的响应报文长这样:

HTTP/1.1 302 Found
Location: http://localhost:8080/web-demo/login.jsp
Content-Length: 0

浏览器看到 302 和 Location,就自动再发一个请求到 Location 里的地址,用户看到的效果是"页面自己跳过去了",地址栏也变了。整个过程中用户不需要点任何东西。

【补充】

302 在第 6 章会变成一个 API。它的产生方式有两种,本质完全一样:

// 写法一:手动拼,能看清 302 的本质
response.setStatus(302);
response.setHeader("Location", request.getContextPath() + "/login.jsp");

// 写法二:一行搞定,内部就是上面两行
response.sendRedirect(request.getContextPath() + "/login.jsp");

sendRedirect() 做的事就是"设置状态码 302 + 设置 Location 头 + 清空缓冲区",所以你只要记住 setStatus(302) 加 Location 这两个要素,就理解了重定向的全部机制。第 6 章会把 sendRedirect 和 forward 放在一起对比,到时候回来看这一节会非常顺。

【易错点】 302 之后原来 request 域里的数据就没了。因为重定向是两次独立的请求:第一次请求里存进 request 的数据,随第一次请求结束而销毁;浏览器发起的第二次请求是一个全新的请求,自然拿不到。这就是第 6 章"重定向不能共享 request 域数据、转发可以"的根本原因。想跨重定向传数据,只能用第 7 章的 Session 或者拼在 URL 参数里。

304 Not Modified:告诉客户端,你请求的资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向。

304 的出现必须有两个前提,缺一不可:

  1. 浏览器本地已经有这个资源的副本,并且副本上记着上次服务器给的"版本标记"(Last-Modified 时间或 ETag 指纹)。
  2. 浏览器这次带了条件请求头:If-Modified-Since: 上次的时间 或 If-None-Match: 上次的 ETag。

流程是这样的:

第一次请求:
  浏览器 ──▶ GET /css/style.css
  服务器 ◀── 200 OK
              Last-Modified: Mon, 02 Nov 2026 02:00:00 GMT
              Cache-Control: max-age=0
  (浏览器把文件和这个时间一起存进缓存)

第二次请求(刷新页面):
  浏览器 ──▶ GET /css/style.css
              If-Modified-Since: Mon, 02 Nov 2026 02:00:00 GMT
  服务器 ◀── 304 Not Modified
              (没有响应体)
  (浏览器直接用本地副本)
【补充】

304 属于协商缓存:浏览器先问一句"变了吗",服务器答"没变",于是浏览器用本地副本,省掉的是响应体的传输,请求和响应头还是要走一趟网络。这和 4.3.2 提到的强缓存(Cache-Control: max-age=300)不同:强缓存期间浏览器连问都不问,直接开本地文件。所以两种缓存的本质区别是"要不要发这次请求"。

在 Tomcat 里,静态资源(HTML、CSS、JS、图片)的 304 是 DefaultServlet 自动处理的:它读文件的最后修改时间,和请求头里的 If-Modified-Since 比对,没变就回 304。你不需要写一行代码。而动态资源(Servlet 输出)默认没有这个机制,每次都是 200,要自己做缓存得手动设置 Last-Modified/ETag。

【对比】 302 和 304 都是 3xx,但行为几乎相反,这张表必须记住:

对比项302 Found304 Not Modified
触发者服务器主动要求跳转浏览器带条件头来询问
是否再发新请求是,自动请求 Location否,直接用本地缓存
有没有响应体通常为空(Content-Length: 0)一定为空
地址栏是否变化会变成新地址不变
用户是否感知能看到页面跳转完全无感,只是加载变快
典型来源response.sendRedirect()容器对静态资源的缓存比对
缓存影响默认不缓存(临时跳转)本身就是缓存机制的产物

【补充】 302 之外还有 301 Moved Permanently(永久重定向)。区别是浏览器会不会记住这次跳转:301 会被缓存,下次访问旧地址时浏览器直接跳新地址,根本不再问服务器;302 是临时的,每次都老老实实先请求旧地址,收到 302 再跳。所以"网站换域名"用 301(省一次请求,也利于搜索引擎收录新域名),"未登录跳登录页"必须用 302:如果用 301,用户登录之后再访问还会被永久跳到登录页,怎么都进不去。这类错误真实存在,务必记牢。

【易错点】 在开发者工具里勾了 Disable cache 之后,永远看不到 304。因为此时浏览器不再发 If-Modified-Since/If-None-Match,服务器没有比对依据,只能回 200 全量内容。想复现 304,把这个勾去掉,正常刷新两次。

4.6.3 4xx:客户端错误

4xx 的特征是:服务器好好的,是请求本身有问题。这一组里 404 最常见,403、405、400 也常遇到。

400 Bad Request:客户端请求有语法错误,不能被服务器所理解。

真实场景:请求头格式坏掉(比如全角冒号)、请求行里的空格不对、Content-Length 和实际请求体长度对不上、把 id=abc 传给一个需要整数的参数。看到 400,先检查"请求本身长得对不对",而不是去查业务代码。

403 Forbidden:服务器收到请求,但是拒绝提供服务,比如:没有权限访问相关资源。

真实场景:直接访问 WEB-INF 目录下的 JSP(这个目录对外是禁止访问的,第 8 章会讲)、Tomcat 的目录列表功能被关闭后访问一个没有欢迎页的目录、权限过滤器拦下了当前用户。

【补充】

403 和"没登录"是两回事。没登录通常返回 302 跳转到登录页(sendRedirect),或者返回 401 Unauthorized(要求身份认证)。403 的含义是"我知道你是谁,但你没资格"。这三个状态码经常被混用,实际项目里按语义区分:401 是"请先证明你是谁",403 是"证明过了,但不许进",302 是"请去登录页"。

404 Not Found:请求资源不存在,一般是 URL 输入有误,或者网站资源被删除了。

真实场景:地址栏少打一个字母、Servlet 的 @WebServlet("/user") 写成 /users 而请求的是 /user、项目名(contextPath)写错、图片路径写成了绝对路径 /img/a.png 而实际在 /web-demo/img/a.png。404 的责任在"请求方或映射配置",服务器没有任何问题。

405 Method Not Allowed:请求方式有误,比如应该用 GET 请求方式的资源,用了 POST。

真实场景:Servlet 里只重写了 doPost(),结果你用浏览器地址栏直接访问(浏览器只会发 GET),容器就会回 405。反过来,只重写了 doGet(),而表单写了 method="post",同样是 405。

【对比】

404 和 405 最容易混。404 是"这个地址没有东西",405 是"这个地址有东西,但你不该用这个方法敲门"。判断方法很简单:把请求方式从 POST 改成 GET(或反过来)再试一次,如果变成 200,那就是 405;如果还是 404,那就是路径问题。

428 Precondition Required:服务器要求有条件的请求,告诉客户端要想访问该资源,必须携带特定的请求头。

真实场景:接口要求客户端先带 If-Match(版本号)才能修改,防止两个人同时改同一条数据互相覆盖。课程项目里少见,属于"乐观锁"的实现手段。

429 Too Many Requests:太多请求,可以限制客户端请求某个资源的数量,配合 Retry-After(多长时间后可以请求)响应头一起使用。

真实场景:登录接口"一分钟最多试 5 次",第 6 次返回 429;秒杀活动限流;爬虫被挡。响应里通常带 Retry-After: 60,告诉客户端 60 秒后再来。

431 Request Header Fields Too Large:请求头太大,服务器不愿意处理请求,因为它的头部字段太大。请求可以在减少请求头域的大小后重新提交。

真实场景:Cookie 攒了几百个(第 7 章 Session 用多了、同一域名下不同路径各存一份),或者某个自定义请求头带了一整段 JSON,超过 Tomcat 的 maxHttpHeaderSize(默认 8KB)。

【易错点】

431 是最难查的一个,因为浏览器只显示"请求头过大",看不出是哪个头。排查办法:开发者工具里看 Request Headers 的整体大小,重点怀疑 Cookie;清理该域名下的 Cookie 再试。第 7 章讲 Session 时,如果发现"登录状态莫名丢失",也要怀疑是不是 Cookie 太大触发了 431。

4.6.4 5xx:服务器错误

5xx 的特征是:请求本身没问题,是服务器处理时崩了。这一组里 500 是主角。

500 Internal Server Error:服务器发生不可预期的错误。服务器出异常了,赶紧看日志去吧。

真实场景:Servlet 里 request.getParameter("id") 拿到 null 直接 Integer.parseInt() 抛空指针;SQL 语句写错(第 10 章,SQLSyntaxErrorException);JSP 里写错 Java 代码导致编译失败(第 8 章);Class.forName("com.mysql.jdbc.Driver") 的驱动没导入,抛 ClassNotFoundException。500 的责任在服务端代码,改请求是没用的。

503 Service Unavailable:服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好。

真实场景:项目刚启动、Spring 容器还在初始化,这时请求会 503;数据库连接池被占满;服务器停机维护。

511 Network Authentication Required:客户端需要进行身份验证才能获得网络访问权限。

真实场景:连公司或酒店的 Wi-Fi,还没在认证页面登录就打开网页。它由网络接入设备返回,跟你的 JavaWeb 项目基本无关,知道含义即可。

【补充】

404 与 500 的排查思路完全不同,这是本章最该带走的一条实用结论,因为它们的责任方不一样。

404:先问"东西在哪"。 排查顺序:

  1. URL 对不对:大小写(Linux 服务器区分大小写)、端口(8080 写成 80)、项目名(contextPath)是否拼错;
  2. 映射对不对:@WebServlet("/user/*") 与实际请求路径是否匹配,web.xml 里的 <url-pattern> 是不是写成了 /user 而请求的是 /user/;
  3. 资源是否真被部署:去 Tomcat 的 webapps 或 IDEA 的 out/artifacts 目录里看看那个文件在不在,很多时候是"源文件有、编译产物没有"。

500:先问"为什么炸"。 排查顺序:

  1. 看控制台第一行异常:java.lang.NullPointerException 后面跟着的 at com.itheima.web.UserServlet.doGet(UserServlet.java:42),行号就是答案;
  2. 看异常类型定位方向:NullPointerException 查空值、SQLException 查第 10 章的 SQL 与连接、ClassNotFoundException 查依赖、JSP 相关的 JasperException 查第 8 章的页面代码;
  3. 复现并单步调试:500 几乎一定能复现,打断点比猜快。

总结:404 改地址或改映射,500 改代码;404 先看前端,500 先看日志。

【考点】 "页面报 404 / 500,你先查什么?"这道题的满分答法分两层:先说责任划分(404 是请求方或映射配置的问题,500 是服务器代码抛异常),再说具体动作(404 核对 URL、映射、部署产物;500 看服务器日志第一条异常栈的行号)。

4.6.5 状态码与第 6 章 Request/Response 的对应

到这里,状态码还只是"看懂",这一节把它变成"能写出来"。第 6 章的 Response 对象提供了三种改状态码的方式,对应三类完全不同的需求。

需求使用的 API产生的状态码说明
正常输出内容response.getWriter().write(...)200默认就是 200,不用管
自定义状态码response.setStatus(201)任意只改响应行的数字,不影响响应体
未登录跳转response.sendRedirect(url)302内部设置 302 和 Location
资源不存在response.sendError(404, "商品不存在")404清空缓冲区,容器生成错误页
服务器异常抛异常,或 sendError(500)500容器接管并显示错误页
静态资源未修改不用写,容器自动比对304DefaultServlet 处理 If-Modified-Since

三者的差别用代码看得最清楚:

// 1. setStatus:只改数字,已经写出的内容照常发送
response.setStatus(201);
response.getWriter().write("{\"msg\":\"创建成功\"}");

// 2. sendError:丢弃已写内容,让容器生成一个标准错误页
response.sendError(404, "商品不存在");

// 3. sendRedirect:302 + Location,让浏览器再发一次请求
response.sendRedirect(request.getContextPath() + "/login.jsp");
【考点】

三个方法的核心区别各有一条:

  • setStatus(int) 只改状态码,不影响响应体,也不清空缓冲区;
  • sendError(int, String) 会清空缓冲区、终止后续输出,由容器生成错误页,适合"这条路走不通了";
  • sendRedirect(String) 等于 setStatus(302) + setHeader("Location", url),同样清空缓冲区,并且会让浏览器再发一次请求,所以第 6 章讲"重定向不能共享 request 域数据"。

【补充】 为什么 302 要放在第 6 章重点讲?因为它是唯一一个"服务器不返回内容、只返回指令"的常见响应。理解 302 的关键是理解"浏览器收到 302 之后会自己再发一个请求",而这一步在第 6 章的 forward(服务器内部转发,浏览器完全不知道)里是不存在的。把这两个放在一起对比,第 6 章那节就通了。

【易错点】 sendError 之后再调 getWriter().write() 是无效的,因为响应已经提交给容器了;sendRedirect 之后同理。如果代码里出现"跳转了但页面还输出了半截内容",多半是在 sendRedirect 之后又写了东西,或者前面已经 flush() 过缓冲区导致重定向失败。

4.7 用浏览器开发者工具看 HTTP

前面六节都是"读文档",这一节是"上手验证"。所有现代浏览器都一样:按 F12(或右键"检查"),切到 Network(网络) 面板。

第一次使用要先做两件事:

  1. 勾上 Preserve log(保留日志):否则页面一跳转,之前的请求记录全没了。排查 302 跳转链路时必须勾。
  2. 需要看缓存行为时,取消勾选 Disable cache(停用缓存):勾着它永远看不到 304。

然后刷新页面,你会看到一张请求列表:

Name                  Status   Type          Initiator        Size        Time
index.html            200      document      Other            1.2 kB      35 ms
style.css             200      stylesheet    index.html       3.4 kB      12 ms
main.js               200      script        index.html       8.1 kB      18 ms
logo.png              304      png           index.html       0 B          3 ms
brand/findAll         200      xhr           main.js          642 B       26 ms

这张表就对应 4.1.3 的"第七步":index.html 是第一张多米诺骨牌,style.css、main.js、logo.png 都是它引出来的子请求,brand/findAll 是 main.js 里用 Ajax 发的接口请求。Initiator 一列就是"谁发起了这个请求",顺着它能画出整棵请求树。

点开任意一条请求,右侧会出现几个页签:

页签看什么对应本章哪一节
GeneralRequest URL、Request Method、Status Code、Remote Address4.1.3、4.2.2、4.5
Response HeadersContent-Type、Content-Length、Cache-Control 等4.3.2
Request HeadersHost、User-Agent、Accept、Cookie 等4.2.3
PayloadQuery String Parameters 或 Form Data4.2.4、4.4
Response服务器返回的原始响应体4.3.1
TimingDNS、TCP、TTFB、下载各阶段耗时4.1.3

排查时的三步固定动作:

  1. 先看 Status Code:200 往下走,4xx 查请求,5xx 查日志。
  2. 再看 Request Method 和 Payload:确认参数到底在 Query String(GET)还是 Form Data(POST),对不对得上后端的接收方式。
  3. 最后看 Response Headers 里的 Content-Type:确认 charset 和实际编码一致,排除乱码。
【补充】

Size 一栏的显示值能直接告诉你缓存有没有生效:显示具体字节数,说明真从网络传了;显示 (disk cache) 或 (memory cache),说明走的是强缓存,根本没发请求;显示 304 加 0 B 左右,说明走的是协商缓存,问了一次但没传正文。三种情况对应三种完全不同的机制,这是判断"为什么改了 CSS 页面没变"的最快方法。

【易错点】 "改了样式刷新没效果"这个高频问题,八成出在缓存,不在代码。顺序应该是:先确认文件保存了 → 再按 Ctrl+F5 强刷(绕过缓存)→ 再确认 Network 里那条 CSS 的状态码是 200 而不是 304/(disk cache)。养成看状态码的习惯,能省掉大量"重启项目试试"的无效操作。

【考点】 怎么一眼判断一个请求是 GET 还是 POST?看 Payload 面板:出现 Query String Parameters 是 GET(参数在 URL 里),出现 Form Data 是 POST 且 Content-Type 为 application/x-www-form-urlencoded。另外 GET 的请求体区域根本不显示,因为它没有请求体。

【补充】 Network 面板看不到 DNS 和 TCP 的细节,但是 Timing 页签把它们拆开了:Queued(排队等连接)、Stalled(等待可用连接)、DNS Lookup(查域名)、Initial connection(三次握手)、SSL(TLS 握手)、Request sent(发出请求)、Waiting (TTFB)(等服务器第一个字节)、Content Download(下载响应体)。经验判断:TTFB 特别大说明服务器处理慢,多半是第 10 章的 SQL 慢查询;Content Download 特别大说明响应体太大,该考虑分页或压缩;Initial connection 特别大说明网络或服务器连接数有问题,跟代码无关。

本章小结

本章讲了两件事:HTTP 报文长什么样,以及状态码在说什么。用一张表收口:

主题结论下一章在哪用
HTTP 概念超文本传输协议,规定浏览器与服务器之间数据传输的规则第 5 章 Tomcat 的本职就是解析它
三个特点基于 TCP、请求-响应模型、无状态第 7 章 Cookie/Session 就是为了弥补无状态
请求报文请求行 + 请求头 + 空行 + 请求体第 6 章 Request 对象逐字段封装它
响应报文响应行 + 响应头 + 空行 + 响应体第 6 章 Response 对象逐字段生成它
GET / POST参数位置不同、长度限制不同,本质是语义与幂等性不同第 6 章取参数时按方法分情况处理
状态码分类首位定大类:1xx 响应中、2xx 成功、3xx 重定向、4xx 客户端错、5xx 服务端错4.6 与第 6 章
302服务器让浏览器自动再发一次请求到 Location第 6 章 sendRedirect()
304协商缓存,浏览器带条件头问、服务器答"没变"第 9 章过滤器里做缓存控制
404 / 500前者查地址与映射,后者查日志与代码整个实验课的排错主线

三条要带走的判断:

自测

基础题

  1. 写出 HTTP 请求报文的三个组成部分,并说明 GET 与 POST 在报文结构上的差别。
  2. HTTP/1.1 200 OK 这一行由哪三部分组成?浏览器判断成功与否依据的是哪一部分?
  3. 说出 Host、User-Agent、Accept、Accept-Language、Accept-Encoding 五个请求头的作用。
  4. 状态码 200、302、304、404、500 分别属于哪个大类?各举一个真实场景。
  5. 按本章的表述写出 GET 与 POST 的两条区别。
  6. 响应头 Content-Type: text/html;charset=UTF-8 里的两个信息分别起什么作用?

进阶题

  1. 看报文写结果。浏览器发出如下请求:
    GET /web-demo/user/list HTTP/1.1
    Host: localhost:8080

    而 UserServlet 的映射是 @WebServlet("/user/list"),且只重写了 doPost() 方法。请问服务器返回什么状态码?为什么?如果把请求方式改成 POST,结果又如何?
  2. 找错。下面的代码想实现"未登录就跳到登录页":
    response.setStatus(302);
    response.getWriter().write("请先登录");

    它能不能完成跳转?缺了什么?正确写法是什么?
  3. 为什么在开发者工具里勾选 Disable cache 之后就再也看不到 304?想复现 304 应该怎么做?
  4. 同一个项目里,访问 /web-demo/index.html 返回 404,访问 /web-demo/user/list 返回 500。请分别说出你的第一步排查动作,并说明这两个状态码在"责任划分"上的区别。

参考答案

  1. 请求行、请求头、请求体。GET 只有请求行和请求头,参数放在请求行的查询串里,没有请求体;POST 三部分齐全,参数放在请求体中,并带 Content-Type 说明体的格式。
  2. 协议版本(HTTP/1.1)、响应状态码(200)、状态码描述(OK)。浏览器只依据三位数字,OK 只是给人看的描述。
  3. Host 请求的主机名(HTTP/1.1 必填);User-Agent 浏览器版本标识;Accept 浏览器能接收的资源类型;Accept-Language 浏览器偏好的语言;Accept-Encoding 浏览器支持的压缩类型。
  4. 200 属于 2xx 成功,场景:页面正常打开。302 属于 3xx 重定向,场景:未登录跳登录页。304 属于 3xx,场景:刷新页面时 CSS 未改动,用本地缓存。404 属于 4xx 客户端错误,场景:URL 或 Servlet 映射写错。500 属于 5xx 服务器端错误,场景:Servlet 里空指针异常。
  5. 一是 GET 请求参数在请求行中,没有请求体,POST 请求参数在请求体中;二是 GET 请求参数大小有限制,POST 没有。
  6. text/html 告诉浏览器按 HTML 渲染,charset=UTF-8 告诉浏览器按 UTF-8 解码字节,两者缺一都可能乱码或显示成源码。
  7. 返回 405 Method Not Allowed。路径能匹配到 Servlet,但浏览器发的是 GET,而该 Servlet 只重写了 doPost(),HttpServlet.service() 找不到 doGet() 就回 405。改成 POST 后能进入 doPost(),正常返回 200。若改为 GET 后仍然 404,才说明是映射或路径问题。
  8. 不能跳转。302 只是状态码,浏览器还需要 Location 响应头才知道跳去哪;而且 getWriter().write() 在 302 之后写内容没有意义。正确写法:response.sendRedirect(request.getContextPath() + "/login.jsp");,它等价于 setStatus(302) + setHeader("Location", url)。
  9. 勾选 Disable cache 后,浏览器不再发送 If-Modified-Since / If-None-Match 条件请求头,服务器没有比对依据,只能返回 200 全量内容。复现方法:取消勾选,正常刷新一次(拿到 200 并缓存),再刷新一次即出现 304。
  10. 404 的第一步是核对 URL 与映射:地址拼写、大小写、项目名、@WebServlet 的路径,以及编译产物里是否真有这个文件,责任在请求方或映射配置。500 的第一步是看服务器控制台日志的第一条异常及其行号,责任在服务端代码。区别在于:404 是"东西不在你说的位置",500 是"东西在,但我处理时崩了"。