目录

一、查询参数

什么是查询参数?

查询参数的格式规范

二、路由路径

什么是路由路径?

路由路径与查询参数的关联

三、Web根目录

什么是Web根目录?

四、HTTP表单

表单是什么?

表单核心结构(HTML 代码示例)

表单两大核心属性 (决定HTTP请求)

相关问题

五、Get和Post请求方法

GET

POST

PUT

HEAD

DELETE

OPTIONS 

幂等

六、HTTP的报头header

请求报头 vs 响应报头

1. Host

2. User-Agent

3. Content-Type

5. Connection

6. Referer

7. Location

8. Cookie

10. Session

七、状态码

2xx 成功状态码

4xx 客户端错误状态码

5xx 服务端错误状态码

3xx 重定向状态码

302 临时重定向状态码

301 永久重定向状态码

八、总结


在上一篇文章中,我们从零手写了一个极简的 HTTP 服务器,完整实现了 TCP 网络通信、HTTP 报文解析、动态路由、业务处理的完整过程。但在编码中,我们聚焦于服务器核心框架搭建,对 HTTP 协议中一些细节做了简化处理。本篇文章就对这些细节进行讲解。

一、查询参数

什么是查询参数?

查询参数,是 URL (网址) 里 ? 后面那一串键值对。是 GET 请求专门用来传递业务数据 的一套机制,是 GET 请求传参的唯一方式。

我们先看一个完整带查询参数的 URL:

我们把这个 URL 拆成两部分 :

  1. 前面的 /Search:这是路由路径,作用是告诉服务器 “我要找 Search 这个接口”;这个 Search 接口一定是服务器里注册了的接口,用来定位具体业务函数;
  2. 后面的 ?key=C++&page=1&size=10:这一整段,就是查询参数

查询参数也属于 URI (统一资源标识符) 的一部分,我们之前说过,URL 去掉协议、IP、端口后,剩下的路径 + 查询参数,就是 URI。所以路由路径 /Search 是 URI 的一部分,查询参数  ?key= C++&page=1 也是 URI 的一部分。

查询参数的格式规范

查询参数有严格固定的格式,必须遵守 : 以 ? 开头作为路由路径和参数的分隔符多个参数之间用 & 分隔;每个参数都是 键=值 的格式,键是参数名,值是你要传给服务器的数据

比如上面例子里:

  1. 键是 key,值为 C++(代表搜索关键词是 C++)
  2. 键是 page,值为 1(代表要查第 1 页数据)
  3. 键是 size,值为 10(代表一页返回 10 条结果)

那查询参数在请求报文中是如何体现出来的?

查询参数里的键值对,就是 HTTP 请求报文中请求行里 URI 的一部分,本身就是请求报文的核心组成。

举个例子 :

它在 HTTP GET 请求报文里,是这样体现的:key=C++ 和 page=1 这组键值对,原封不动地存在于请求报文的请求行,是请求报文的一部分,跟着请求行一起被浏览器发送给服务器。

这里我们要区分两个键值对,因为在 HTTP 请求报文里,有两种键值对,一个是查询参数里的键值对,另一个就是请求报头里的键值对

查询参数键值对在请求行里,是 GET 方法的专属,作用是用来请求 URL 的指定资源。

而请求头键值对如下,在请求报头中,是所有请求方法公用的,作用是传递客户端配置信息。


 

二、路由路径

什么是路由路径?

路由路径,就是 URI 中问号 ? 前面、专门用来让服务器匹配接口 / 资源的那部分路径,是服务器 “找哪个业务函数” 的依据,和我们上一篇手写 HTTP 服务器里注册的动态路由对应。

我们还是用之前的举例:

  1. 问号 ? 前面的  /Search —— 这就是路由路径;
  2. 问号 ? 后面的  key=C++&page=1 —— 就是查询参数。

路由路径的核心作用就是用来精准匹配服务器里的 “业务接口”,在我们手写的 HTTP 服务器中,main 函数里写过这样的代码:

这里面的 /Login、/Register、/Search、/api/getprocutlist,就是路由路径。它的本质就是客户端通过 URL 里的路由路径告诉服务器:“我要调用你服务器内部 Search 这个业务函数”;服务器收到请求后,拿着路由路径去自己的路由注册表里找,找到就执行对应的业务逻辑,找不到就返回 404 资源不存在。

当客户端访问 http://127.0.0.1:8080/Search?key=C++ 时,服务器解析请求行,拿到完整 URI /Search?key=C++;先从 URI 里拆分出路由路径 /Search;拿着 /Search 去匹配我们注册的路由,找到并执行 Search 业务函数;再拆分出查询参数 key=C++,在 Search 函数里使用这个业务数据。

路由路径与查询参数的关联

从本质上来说,查询参数和普通函数传入的参数是完全等价的,服务器解析完成后可以直接提取使用。在 GET 请求中,路由路径的作用是帮服务器匹配对应的业务接口,相当于确定了要调用的函数名称,而查询参数就是为这个接口传递的具体入参,客户端通过 URL 中问号后的键值对,将业务所需的数据传递给服务器。服务器解析请求报文时,会先从 URI 中拆分出路由路径完成接口匹配,再单独解析出查询参数的键值对,然后在业务函数内部就可以直接通过参数名提取对应的值,直接参与后续的业务逻辑处理,就像调用普通 C++ 函数时传递参数一样自然。需要区分的是,GET 请求的参数通过查询参数承载在 URI 中,而 POST 请求的参数会放在请求体里,但对于后端业务函数而言,两者最终都是被解析为可用的参数,只是数据承载的报文位置不同。

三、Web根目录

什么是Web根目录?

Web 根目录本质就是网站的根目录,在我们手写 HTTP 服务器的项目里,我们创建的 wwwroot 文件夹,就是服务器中定义的 Web 根目录。Web 根目录是服务器存放所有静态资源的最顶层文件夹,相当于服务器对外展示文件的 “大门”,客户端通过浏览器访问时,所有静态文件的请求都会从这个目录下查找资源。

举个例子: 当客户端发起 GET 请求访问 http://127.0.0.1:8080/test.txt 时,服务器解析出 URI 里的文件路径 /test.txt,会自动拼接上 Web 根目录 wwwroot,最终定位到 wwwroot/test.txt 这个文件,再将文件内容读取并返回给客户端。

换句话说,Web 根目录就是整个网站文件层级的最高层,是绝对的顶层入口,服务器找静态文件时,一定是从这个根目录开始,顺着 URL 中访问的路径一层一层向下查找。结合我们的上面的代码来看,我们设置的 wwwroot 就是这个最高层级的根目录,所有静态资源都必须放在这个文件夹里,不能超出它的范围。

当访问 http://127.0.0.1:8080/index.html 时,服务器会以 wwwroot 为起点,直接查找根目录下的 index.html 文件;如果访问的是 http://127.0.0.1:8080/img/1.jpg,服务器就会从 wwwroot 这个顶层目录开始,先进入里面的 img 子文件夹,再在里面寻找 1.jpg。整个查找逻辑是严格从上到下的层级匹配,根目录就是整个文件查找逻辑的起点,也是最高权限的目录,下面所有的文件夹、文件都是它的子层级,服务器不会去根目录之外的位置寻找资源,这样既规范了网站的文件结构,也保证了访问的安全性。

四、HTTP表单

表单是什么?

表单是浏览器原生提供的数据提交工具,专门用来收集用户输入(账号、密码、搜索内容等),并自动将数据打包成 HTTP 请求发给服务器。我们日常登录、注册、提交问卷,底层都是表单在工作。

表单核心结构(HTML 代码示例)

我们先看一下表单的结构:

表单有 3 个核心组成:

  1. 提交地址(action):数据发给哪个接口,注册接口? 登录接口?;
  2. 提交方式(method):用 GET 还是 POST;
  3. 表单元素(input、button):收集用户输入,靠 name 生成参数键。

表单两大核心属性 (决定HTTP请求)

一个标准的 HTML 表单,有两个最核心的属性,直接决定了浏览器会生成什么样的 HTTP 请求,分别是 action 和 method。

1. action:提交目标地址

action 属性用来指定数据要提交到哪个服务器地址,也就是我们之前讲的路由路径,比如 action="/Login",就代表用户提交的数据要发送给服务器的 Login 接口;

2. method:提交方式(2 种)

method 属性用来指定提交数据时使用哪种 HTTP 请求方法,只有两个可选值,分别是 GET 和 POST,这也是 GET 和 POST 最核心、最本质的区别来源。

① 当表单的 method 设置为 GET 时,浏览器会把用户填写的所有表单数据,全部拼接在 action 指定的 URL 后面,以查询参数的形式发送。举个例子,一个登录表单 action="/Login",method= "GET",用户输入账号 zhangsan、密码 123456,浏览器就会生成这样一个 URL:/Login?username=zhangsan&password=123456,如下,然后用 GET 请求发送给服务器。

但此时的缺点就是 :

  • 数据明文在地址栏,不安全;
  • 受 URL 长度限制,不能传大量数据;
  • 无请求体,服务器解析简单。

当表单的 method 设置为 POST 时,浏览器的处理逻辑会完全不同。它不会把数据拼接到 请求行中的 URI 里,而是会把所有表单数据打包,放在 HTTP 请求报文的请求体中发送。同样是登录表单,method="POST",浏览器会把 username=zhangsan&password=123456 这段数据放在请求正文里,同时会自动在请求头里添加 Content-Type 字段,告诉服务器请求体里的数据是什么格式,还会自动添加 Content-Length 字段,告诉服务器请求体有多少字节。
此时数据藏在请求体,地址栏看不见;数据量无上限;服务器必须解析请求体(长连接时要拆包)。

表单数据怎么变成键值对

表单里每个输入框必须有 name 属性:

  • name = 参数的键;
  • 用户输入 = 参数的值;


此时就会生成键值对:username=zhangsan&password=123456。
不管 GET 还是 POST,最终传给服务器的都是同一套键值对,只是存放位置不同。

相关问题

1. 我们登录、注册填账号密码,是不是就是在 “填表单”?

是的。我们在网页里看到的账号框、密码框、登录按钮,这一整块东西,在前端代码里,就是一个完整的 HTML 表单 (<form>)。我们填进去的账号密码,就是填进了这个表单里。

2. 表单是不是属于前端 HTML?

是的。表单是浏览器(前端)的东西,写在 HTML 里。服务器根本不管表单长什么样,服务器只负责接收表单提交后发过来的数据。

现代前端框架(Vue/React)里,原生 <form> 用得少了,大多是用 JS 手动收集数据、发请求,但底层逻辑还是一样的。

3. 那 action 和 method 是干嘛的?

action="/Login":告诉浏览器:“把我收集到的账号密码,发给服务器的 /Login 接口”
method="POST":告诉浏览器:“用 POST 方式发,把数据藏在请求体里,不要拼在网址上”

4. 表单是怎么变成请求报文的?在我们的印象中难道不是只有网址能变成请求报文吗?

不对,不是只有我们手动输入的网址才会变成请求报文。只要浏览器要和服务器通信,就一定会生成 HTTP 请求报文。表单提交,本质就是浏览器和服务器通信,是浏览器自动帮我们发起了一次请求。

我们在填写账号密码的整个过程是这样的:

(1). 前端 (浏览器) 页面上有一个 HTML 表单,写死了 action 和 method。
(2). 我们往表单里填账号密码,点 “登录” 按钮。
(3). 浏览器自动做三件事:

  1. 读取我们填的账号密码,打包成键值对;
  2. 按照 method 规定的方式(GET/POST),生成标准的 HTTP 请求报文;
  3. 按照 action 指定的地址,把这个报文发给服务器。

所以:网址是我们手动触发浏览器发请求;表单是点按钮,浏览器自动发请求。本质完全一样,都是浏览器生成请求报文发给服务器。

5. 那我们一般情况下默认是哪个请求方法?

我们在写 HTML 表单时,如果不写 method 属性,浏览器默认就会使用 GET 方法 提交表单。这是浏览器的原生默认行为,只要我们省略了 method,它就自动走 GET 请求。也就是说,默认情况下,我们填写的账号密码等表单数据,会被浏览器拼接到 URL 后面变成查询参数,明文暴露在地址栏里。正因为 GET 有安全隐患、传参量有限,所以实际开发中,凡是涉及登录、注册、提交隐私数据的表单,我们都会主动把 method 明确设置为 POST,强制让浏览器把数据放到请求体里传输,这也是为什么我们平时接触到的登录注册功能,几乎全是 POST 提交。简单说,表单默认是 GET,但业务开发里 POST 才是标准选择。

6. 那如果默认是 GET 的话,那不就暴露隐私了吗,换句话说我们是如何主动改为 POST 的呢?

首先对于普通人而言,根本不知道 GET 和 POST 是什么,他们只是在网页里填账号、输密码、点登录,对底层的请求方式一无所知。如果表单默认是 GET,那用户输入的账号密码就会直接拼在地址栏里变成明文,不仅自己能看见,还会留在浏览器历史记录里,这确实有极大的隐私泄露风险。但普通人完全不需要操心这件事,因为修改提交方式这件事,根本不是用户来做,而是开发网站的后端和前端程序员来做。程序员在写网页的登录注册页面时,会提前在 HTML 表单代码里,主动加上 method="POST" 这行代码,直接强制浏览器必须用 POST 方式提交,从根源上就避开了 GET 的安全隐患。用户在前端页面上完全感知不到这个设置,他们填完信息点提交,浏览器就会按照程序员提前写好的规则,自动把账号密码放进请求体里发送,不会暴露在地址栏。简单来说,GET 是表单的浏览器默认规则,但程序员在开发时会根据业务场景主动修改这个规则,涉及隐私、敏感数据的场景,程序员一定会手动把 method 改成 POST,普通人只管正常使用,安全防护是网站开发层面提前做好的设计,不需要用户懂任何底层原理。

7. 还有问题就是,我们肯定不会在地址栏输入网址呀,一般都是输入文字,然后浏览器会给我们转换成地址栏的网址吗?

1. 我们日常几乎不会手动敲完整网址,我们平时在浏览器顶部的搜索框里输入文字,比如直接搜 “百度”,或者搜一个网站名称,浏览器根本不是直接拿这段文字去发请求,它会先把你输入的普通文字,自动转换成标准的网址(URL),再进一步生成 HTTP 请求报文发给服务器。简单说,我们输入的文字是给浏览器看的,浏览器会把它翻译成服务器能看懂的网址,这个过程完全是浏览器自动完成的,我们感受不到。

2. 具体来说,浏览器分两步处理:第一步是地址转换,如果我们输入的是普通文字,浏览器会默认调用它自带的搜索引擎,把文字解析成对应的目标网址,比如搜 “百度”,浏览器就会自动定位到 “https://www.baidu.com” 这个标准网址;如果我们输入的是一个简单的域名,比如 “baidu.com”,浏览器会自动补全协议,变成完整的网址。第二步是生成报文,拿到转换好的网址后,浏览器会按照我们之前聊的规则,自动拆解网址里的协议、IP、路由路径,生成标准的 GET 请求报文,然后发给对应的服务器。

3. 这和表单提交的逻辑本质是一样的,只是触发方式不同:表单是我们填完信息点按钮,浏览器自动生成 POST 请求报文;我们搜文字是输入关键词,浏览器先转网址,再自动生成 GET 请求报文。两种情况里,我们都不用手动处理网址和报文,浏览器都在背后做了翻译和打包的工作,服务器最终收到的,永远是标准的 HTTP 请求报文,它根本不关心这个请求是我们搜文字触发的,还是点表单触发的。

8. 举一个完整的例子

当我们在浏览器地址栏输入 “西红柿炒鸡蛋怎么做” 这类纯文字搜索词时,浏览器首先会判断输入内容不是标准网址,而是搜索指令,随后会调用默认搜索引擎 (比如百度、谷歌),将我们的搜索词拼接成带查询参数的完整 URL,本质是一次 GET 请求;网址里是可以包含中文这类非英文字符的,但计算机底层只识别 ASCII 字符,所以浏览器会自动对中文进行 urlencode / urldecode 编码,把中文转换成 % XX 格式的编码字符,拼接到搜索 URL 的查询参数中,再生成标准的 HTTP GET 请求报文发给搜索引擎服务器,服务器收到数据后会自动解码还原出中文搜索词,再根据关键词返回对应的搜索结果页面。整个过程里,我们看到的中文是浏览器解码后展示的,网络中实际传输的是编码后的字符,这也是为什么我们肉眼看到的搜索网址里能显示中文,而实际传输时中文会被转换的原因,整个搜索流程本质就是浏览器自动完成拼接 URL、编码、发起 GET 请求的全过程,和我们之前讲的 GET 请求、查询参数、表单提交的底层逻辑完全相通。

五、Get和Post请求方法

GET

GET 是 HTTP 协议里最基础、最常用的请求方法,GET 的本质就是从服务器中「获取 / 查询」资源。简单说,GET 就是 “只读” 请求,我们向服务器要东西,服务器把东西给我们,整个过程不会改变服务器的状态,比如打开网页、搜索内容、加载图片、视频,底层全都是 GET 请求。

结合我们之前的例子,GET 请求的数据传递有一个铁则:所有参数都放在 URL 的查询参数里,也就是请求报文的「请求行」中。不管是我们在浏览器地址栏手动输入网址、表单默认提交,还是搜索关键词,本质都是 GET 请求:浏览器会把我们要传递的参数(比如搜索词、表单数据),拼接在 URL 的问号后面,形成 URL?键1=值1&键2=值2 的格式,而这些中文、特殊符号,浏览器会自动做 URL 编码,服务器收到后再解码还原,这也是 GET 请求最标志性的特征。

基于这个核心特征,GET 请求衍生出几个关键特点:第一,数据明文暴露,因为参数在地址栏里,所有人都能看见,还会留在浏览器历史记录里,所以绝对不能用来传输账号、密码这类敏感隐私数据,这也是为什么登录、注册表单都会手动改成 POST;第二,数据长度有限制,因为浏览器和服务器都会对 URL 的长度做限制,不能传大量数据,只适合传少量、简单的查询参数;第三,天然可缓存、可收藏,浏览器会缓存 GET 请求的结果,我们刷新页面速度更快,也能把这个带参数的网址收藏、分享,比如搜索后的页面网址,分享给别人打开就是一样的结果;第四,没有请求体,GET 请求报文只有请求行、请求头,空一行后直接结束,不需要解析请求体,服务器处理起来非常简单高效。

再结合我们手写的 HTTP 服务器,GET 请求的完整工作流程可以完美闭环:客户端发起 GET 请求时,会在请求行里带上完整的 URI(路由路径 + 查询参数),服务器收到报文后,先拆分出路由路径,匹配对应的业务接口,再拆分出查询参数,解析成键值对后直接使用,执行业务逻辑,最后把查询到的资源(HTML 页面、图片、搜索结果)返回给客户端。

最后明确 GET 的使用场景,记住一个核心原则:只要是 “查、看、获取”,不用改服务器数据的操作,全用 GET。比如打开网页、搜索信息、加载静态资源、查询个人信息;反之,只要是提交隐私数据、上传文件、修改服务器数据(注册、登录、发布内容),绝对不用 GET,必须用 POST。

POST

POST 是 HTTP 协议里专门用来向服务器提交数据、修改服务器资源状态的请求方法,和 GET 完全相反:GET 是 “从服务器拿东西”,POST 是 “往服务器送东西”。我们日常登录账号、注册信息、发布评论、提交表单、上传文件,底层全是 POST 请求。

POST 最核心的特征,和 GET 有本质区别:它的参数不放在 URL 里,而是藏在 HTTP 报文的「请求体」中。我们之前讲表单时提到,只要把表单 method="POST",浏览器就不会把账号密码拼在地址栏,而是打包放在请求头后面的请求体里传输。正因为数据藏在请求体,不在地址栏暴露,所以 POST 天生适合传输账号、密码、身份证这类敏感隐私数据,这也是所有登录、注册功能必用 POST 的根本原因。

基于这个核心特征,POST 衍生出和 GET 完全对立的关键特点:第一,数据隐私安全,请求体里的内容不会显示在地址栏,也不会留在浏览器历史记录里,第三方无法通过网址直接获取参数;第二,无数据长度限制,请求体理论上可以承载任意大小的数据,既能传简单的账号密码,也能传大体积的图片、文件、视频;第三,不可缓存、不可直接收藏,POST 是提交操作,每次提交都可能改变服务器数据,浏览器不会缓存 POST 结果,也无法直接收藏 POST 提交后的页面,刷新页面时浏览器还会提示 “是否重新提交表单”;第四,必须解析请求体,POST 报文结构是请求行 + 请求头 + 空行 + 请求体,服务器必须读取请求体内容、解析数据格式,相比 GET,处理逻辑更复杂,我们手写服务器时遇到的粘包、拆包问题,本质就是 POST 请求体带来的。

结合我们手写的 HTTP 服务器和表单场景,POST 的完整工作流程清晰闭环:前端表单设置 action 和 method="POST",用户填完账号密码点击提交,浏览器收集表单数据、打包成键值对放入请求体,自动添加 Content-Type(告诉服务器请求体数据格式)和 Content-Length(告诉服务器请求体大小)两个关键请求头,生成完整 POST 报文发给服务器;服务器收到后,先解析请求行匹配路由接口,再读取请求体、解析键值对,执行业务逻辑(比如验证账号密码),最后返回响应结果。

明确 POST 的使用场景,记住核心原则:只要是 “提交、修改、新增、上传”,会改变服务器数据状态的操作,全用 POST。比如登录、注册、发布动态、提交问卷、上传文件;反之,纯查询、浏览、获取资源的操作,一律用 GET。


HTTP 里除了我们重点讲的 GET 和 POST,还有 PUT、HEAD、DELETE、OPTIONS 这些请求方法,虽然浏览器原生表单不直接支持,但在 RESTful API、前后端交互里很常见:

PUT

PUT 方法的作用是向服务器上传 / 替换一个资源,把请求体里的内容,存到 URL 指定的位置。

HEAD 的作用和 GET 几乎完全一样,但服务器只返回响应头,不返回响应体

DELETE

DELETE 的作用是请求服务器删除 URL 指定的资源。

OPTIONS 

OPTIONS 的作用就是询问服务器对某个 URL 支持哪些 HTTP 方法,或者用来做跨域预检。

幂等

幂等指的是一个操作无论执行一次还是多次,对服务器产生的最终效果完全一致,不会因重复执行引发额外副作用

放到 HTTP 请求里就是:GET、PUT、DELETE、HEAD、OPTIONS 这几种请求,都是幂等的。GET 只是单纯查东西,查多少次服务器里的数据都不会变;PUT 是改或替换某个资源,改一次和反复改一百次,最终都是同一个样子;DELETE 是删东西,删一次东西就没了,再删一百次也没任何变化;HEAD 和 OPTIONS 只是问问服务器信息,啥都不改,自然也不怕重复。

唯独 POST 不是幂等的,它主要用来提交、新增东西,比如注册账号、下单付款,干一次生成一条数据,重复提交就会生成一堆重复数据,如果是付款的话就可能导致重复扣钱,所以这种操作必须额外做好防重复的处理。

六、HTTP的报头header

现在我们来讲解 HTTP 报文里最 “会说话” 的部分 ——报头(Header)。不管是浏览器发给服务器的请求报文,还是服务器返回给浏览器的响应报文,都自带报头,它们就像报文里的 “附加说明”,藏着这次请求 / 响应的关键信息,比如数据格式、跳转指令、身份凭证等,也是前后端交互里最常用的配置项。

请求报头 vs 响应报头

很多人刚学的时候会把两者混为一谈,其实它们的分工完全不同:

  • 请求报头(Request Header):浏览器发给服务器的 “自我介绍 + 附加要求”,比如告诉服务器 “我是谁”“我带的是什么格式的数据”“我能接收什么格式的内容”。
  • 响应报头(Response Header):服务器发给浏览器的 “回复说明”,比如告诉浏览器 “我给你的数据是什么格式”“你接下来该跳转到哪个地址”。

两者都以键: 值的键值对形式存在,中间用冒号分隔,每一行一个键值对,结尾用换行结束。

1. Host

Host 是请求报头专属,并且是 HTTP/1.1 强制必须带的,它是客户端给服务器的主机 + 端口说明,大白话就是 “我要访问的是哪个域名 / IP、哪个端口的服务”。

为什么必须带?因为一个服务器可能同时跑多个网站,光看请求行里的路径,服务器不知道你要访问哪个网站。Host: www.luogu.com.cn 就是告诉服务器:“我要访问的是洛谷这个网站”。比如我们访问https://www.luogu.com.cn:443,请求头里会自动带上 Host: www.luogu.com.cn,服务器收到后,就知道把洛谷的页面返回给我们。


2. User-Agent

User-Agent 是请求报头专属的,它是浏览器的 “自我介绍”,告诉服务器 “我是什么操作系统、什么浏览器、什么版本”。服务器根据它返回适配的页面,比如手机端返回简化版页面,电脑端返回完整版;也常被用来做爬虫识别,服务器看到异常的 User-Agent,就会拒绝访问。

举个例子 : User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0,就是 Chrome 浏览器在 Windows 上的标识。


3. Content-Type

Content-Type 在请求报头和响应报头里都有,它是报文主体的格式说明书,用来告诉对方 “请求报文  / 响应报文里装的是什么类型的数据”,没有它,服务器或浏览器就看不懂报文主体的内容。

在请求报头里 (POST 表单场景) :比如 Content-Type: application/x-www-form-urlencoded,表示请求体里是 key=value 格式的表单


数据,就是登录表单提交时必须带的报头,服务器靠它来解析请求体里的账号密码。

在响应报头里:比如 Content-Type: text/html 表示返回的是网页,image/png 表示返回的是图片,application/json 表示返回的是 JSON 格式数据,浏览器会根据这个头决定怎么处理响应体,比如渲染成网页、直接显示图片,还是交给 JS 解析成数据。


4. Content-Length

Content-Lengt 在请求报头和响应报头中都可以有,它的作用,就是告诉对方 “我这次的报文正文主体(Body) 有多少字节”。

对 POST 请求来说,服务器必须靠它知道请求体要读多少数据,不然就会一直等数据,造成连接挂死;比如我们发一个带表单数据的 POST 请求,浏览器会自动加上 Content-Length: 15,服务器读到 15 个字节后,就知道请求体读完了,不用再等了。

对响应来说,浏览器靠它知道下载进度,比如下载文件时的百分比,就是用 Content-Length 算出来的。


5. Connection

connect 在请求报头和响应报头中都有,它是浏览器和服务器之间的连接约定,用来商量 “这次请求结束后,连接要不要断开”。

Connection: close:传完数据就立刻断开连接,是 HTTP/1.0 的默认规则,每次请求都要重新建立连接,效率较低。

Connection: keep-alive:传完数据后保持连接不断开,后续的请求可以复用这个连接,也就是我们常说的 “长连接”,能减少反复建立连接的开销,HTTP/1.1 默认开启这个模式,也是服务器时需要处理粘包问题的关键原因之一。


6. Referer

Referer 是请求报头专属的,它的作用是告诉服务器 “是从哪个页面跳过来的”。相当于我们从 A 网站点了个链接跳到 B 网站,B 网站的服务器会收到 A 网站的地址,知道我们是从哪来的。

这个报头可以用做当防盗链:比如图片服务器会检查 Referer,如果不是自己的网站,就拒绝返回图片,防止别人盗链用你的资源;也可以用作统计分析:网站可以知道用户是从哪个渠道 (比如百度、抖音) 引流来的。


7. Location

Location 是响应报头专属的,它是服务器给浏览器发的跳转指令,专门和 3xx 重定向状态码(301/302)绑定使用。

比如说服务器在响应报头里写上 Location: /home.html,再配上 302 状态码,浏览器收到后会立刻自动跳转到 /home.html 这个地址,全程不用用户手动操作。再比如说访问洛谷的个人中心,服务器发现我们没登录,就会返回 302 Found 状态码 + Location: /login,浏览器就会自动跳转到登录页面,这就是 Location 的核心作用。


8. Cookie

Cookie 是请求报头专有的它是浏览器存的 “小纸条”,每次请求都会自动带给服务器,用来维持会话状态。

比如我们在登录洛谷后,服务器会给浏览器发一个 Cookie,里面存着我们的登录凭证,下次我们再访问洛谷的其他页面,浏览器会自动把这个 Cookie 塞进请求报头里发给服务器,服务器一看 “哦,是已经登录的用户”,就不用我们再输一遍账号密码了,这也是我们常说的 “会话保持” 的核心。


Set-Cookie 是响应报头专有的,它是服务器给浏览器 “塞小纸条” 的指令,专门用来设置 Cookie。

比如我们登录软件登录成功后,服务器会在响应报头里写 Set-Cookie: user_id=123; path=/,浏览器收到后,就会把这个 Cookie 存起来,后续访问同域名下的页面时,都会自动带上这个 Cookie,实现登录状态的保持。


10. Session

Session 是响应报文专有的,是服务器端的 “会话状态”,和 Cookie 是一对搭档,用来解决 HTTP 协议本身 “无状态” 的问题 —— HTTP 本身不记得我们是谁,每次请求都是独立的,Session 就是服务器用来记住状态的机制。

比如每个用户第一次访问服务器时,服务器会给这个用户创建一个 Session,分配一个唯一的 SessionID。服务器把 SessionID 通过 Set-Cookie 响应报头发给浏览器,浏览器把它存在 Cookie 里。之后每次请求,浏览器都会自动把 Cookie 里的 SessionID 发给服务器,服务器根据这个 ID 读取的状态,就知道 “哦,是这个用户,他已经登录了”。


下面我们可以举个例子来验证 Cookie 的工作原理:

                                 比如我们在刷抖音时,我们在抖音登录成功后,服务器会在响应报头里给浏览器发一个Set-Cookie,里面包含了登录凭证(比如sessionid这类标识)。浏览器收到后,就把这个 Cookie 存在本地,和抖音这个域名绑定。
                                    我们每次访问抖音的页面,浏览器都会自动把这个 Cookie 塞进请求头里发给服务器。服务器拿到 Cookie 里的sessionid,找到对应的 Session,就知道 “这是已登录的用户”,直接返回登录状态的页面,不用我们重复登录。

当我们在浏览器里把抖音的 Cookie 全部删除后,浏览器本地就没有这个登录凭证了。这时我们刷新页面时,浏览器发给服务器的请求头里,没有了 Cookie 字段,服务器找不到我们的 SessionID,就判断我们是未登录状态,直接返回登录页面,让我们重新登录。

Cookie 就是服务器给浏览器的 “登录通行证”,浏览器每次访问都带着它,服务器就认我们;我们把通行证(Cookie)删了,服务器就认不出了,会话自然就断了,这就是 Cookie 保持会话的核心作用。

七、状态码

状态码是服务器写在响应报文第一行(状态行)里的数字,作用就是用一个三位数,告诉浏览器这次请求结果到底怎么样。

2xx 成功状态码

200 OK : 最常见的状态码,代表请求成功,数据正常返回。我们打开一个网页、刷抖音视频,服务器正常返回数据时,状态码就是 200。

204 No Content : 代表请求成功,但服务器没有返回任何响应体,比如我们提交一个表单,服务器处理完但不需要返回页面时会用。

4xx 客户端错误状态码

400 Bad Request : 代表我们发的请求格式不对,服务端看不懂。比如请求体格式错了、参数传错了、URL 有问题,服务器就会返回 400。

401 Unauthorized : 代表没登录 / 没权限,服务器不认识客户端。我们没带 Cookie/SessionID 访问需要登录的接口,服务器就会返回 401,让你先登录。

403 Forbidden : 代表服务器认识客户端,但客户端没权限访问这个东西。比如我们登录了,但想访问别人的私密文件,服务器就会返回 403,拒绝我们的请求。

404 Not Found : 最经典的状态码,代表我们客户端想要的资源不存在,找不到。比如输错了网址、网页被删除了,服务器就会返回 404。

所以 4xx 错误状态码出错的问题主要是客户端 (浏览器/用户) 身上。

5xx 服务端错误状态码

500 Internal Server Error : 代表服务器崩了,出问题了。比如服务器代码报错、数据库挂了,都会返回 500,属于服务器内部错误。

502 Bad Gateway : 服务器的 “中间人” 挂了。比如 Nginx 反向代理后端服务器时,后端服务器崩了,Nginx 就会返回 502。

503 Service Unavailable : 服务器太忙了 / 正在维护,暂时处理不了。比如服务器流量炸了、正在重启,就会返回 503。

所以 5xx 错误状态码出错的问题主要是服务端身上。

3xx 重定向状态码

3xx 重定向的核心逻辑就是当浏览器向服务器发起请求时,如果服务器要求浏览器跳转,就会返回以 3 开头的状态码,同时在响应头中添加 Location 字段,指定要跳转的目标网址。浏览器收到这个响应后,会识别到状态码和 Location 头,自动向新地址发起第二次请求,最终加载新地址的页面。根据跳转的性质不同,它又分为临时重定向和永久重定向两种:

302 临时重定向状态码

302 状态码代表临时重定向,浏览器不会记录旧地址和新地址的对应关系,每次访问旧地址都要重新向服务器确认跳转目标,典型场景就是用户登录成功后,服务器通过 302 + Location: /home让浏览器跳转到首页;

举个例子 : 假设我们正在用抖音 APP,还没登录就直接点进了 “我的收藏” 页面,网址是 douyin.com/collection。服务器收到请求后,发现我们还没有登录凭证,就会返回 302 临时重定向,并在响应头里加上 Location: douyin.com/login。浏览器看到这个指令,就会立刻跳转到登录页面,让我们先完成登录。这里的 “临时” 体现在:浏览器不会记住 /collection 和 /login 的绑定关系。等我们登录成功后,再访问 douyin.com/collection,服务器就不会再跳转了,而是直接返回我们的收藏列表页面。如果用了 301 永久重定向,浏览器会把 /collection 永久绑定到登录页,就算你登录成功,也永远进不了收藏页面了。

301 永久重定向状态码

301 状态码代表永久重定向,浏览器会将旧地址和新地址永久绑定,更新书签并直接使用新地址访问,搜索引擎也会把旧地址的权重转移到新地址上,这类跳转多用于网站更换域名的场景,比如旧域名 old.com 被设置为 301 重定向到新域名 new.com,后续所有访问旧域名的请求都会被自动导向新域名。

再举个例子 : 假设有一个知名电商网站,旧域名 old-shop.com 已经积累了大量用户和品牌口碑,用户都习惯直接访问这个域名购物。由于业务调整,需要将网站整体迁移到新域名 new-shop.com,但如果直接停用旧域名,用户找不到网站就会流失,搜索引擎收录的页面也会失效。
这时就可以通过 301 永久重定向,将所有对 old-shop.com 的请求自动跳转到 new-shop.com。浏览器会永久记住这个跳转关系,用户输入旧域名就能直接访问新网站;同时搜索引擎也会把旧域名的权重转移到新域名上,既保住了用户流量,也不影响网站的搜索排名。

八、总结

这篇文章深入讲解了HTTP协议的多个核心细节,主要包括:

  1. 查询参数的结构与作用,解析了URL中问号后的键值对格式规范;
  2. 路由路径的匹配机制,说明服务器如何通过路径找到对应业务接口;
  3. Web根目录的概念及其在静态资源访问中的重要性;
  4. 表单提交的工作原理,对比GET和POST方法的本质区别;
  5. HTTP请求方法详解,重点区分GET(获取资源)和POST(提交数据)的使用场景;
  6. 报文头的关键作用,包括Host、Content-Type、Cookie等常见报头字段;
  7. 状态码分类解析,涵盖2xx成功、3xx重定向、4xx客户端错误和5xx服务端错误;
  8. 会话管理机制,说明Cookie和Session如何协同维持登录状态。

文章通过具体示例,展示了这些技术细节在实际Web开发中的应用,为理解HTTP协议提供了全面而深入的视角。

谢谢大家的观看!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐