【JavaSE-网络部分11】网络原理-HTTP协议的详细介绍【应用层】

🎯 你正在阅读「网络原理续命手册」系列文章 🎯

🔥 弹简特 个人主页

❄️ 个人专栏直通车:

靠热爱去书写自己,靠勇敢去书写生活!


🌟 博主简介:


文章目录:


一、前言

在上一期,我们介绍了 Fiddler 抓包工具的使用。它的核心作用,就是抓取 HTTP 和 HTTPS 请求。为了更详细地观察 HTTP 的报文格式,我们才引入了这款抓包工具。


二、HHTP协议介绍

本期,我们就来详细介绍 HTTP 协议。

HTTP 是我们日常开发中非常常用的协议。比如浏览器访问网站、手机 APP 访问服务器,使用的都是 HTTP 协议。

HTTP 的特点是一问一答:浏览器访问服务器,服务器再返回响应。


1、什么是HTTP协议

我们使用fiddler这个抓包工具来看一个请求:

同时再看一个响应:

那么,所谓的 HTTP 协议,就是按照特定格式构造的一个字符串。客户端把这个字符串写入 TCP Socket,服务器通过 TCP Socket 把它读出来,这样的字符串就是 HTTP 请求 。反过来,服务器也可以构造另一个特定格式的字符串,写入 TCP Socket,客户端再通过 TCP Socket 把这个字符串读出来,这样的字符串就是 HTTP 响应

HTTP 的核心是传输文本,因此它的名字叫做 超文本传输协议。这里的「超」字,代表我们不仅可以传输文本,还可以传输多媒体、二进制数据等。


2、HTTP响应中会存在乱码

我们看这个响应:

HTTP 传输的是文本,也就是字符串。但我们在响应中有时会看到一些二进制乱码,这是为什么呢?本质上,是 HTTP 服务器对返回的数据进行了压缩------压缩算法会对数据重新计算并编码,保证数据含义相同的同时缩小体积。目的就是为了减少网络带宽。

那么我们怎么解压缩呢?在 Fiddler 上有 Decode 解压缩按钮,通过这个按钮可以看到解压缩之后的数据:


三、HTTP协议报文格式

这里要注意一点:HTTP 是文本协议,它的格式和我们之前学习的 TCP/UDP/IP 协议不同。TCP、UDP、IP 都是约定好哪个字节对应什么含义;而 HTTP 是文本协议,所以 HTTP 按 「字符格式 + 分隔符」 约定含义。

1、请求

再比如:

bash 复制代码
GET https://www.baidu.com/ HTTP/1.1
Host: www.baidu.com
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
sec-ch-ua: "Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: BIDUPSID=1AFD334D29967B82C921F7E72BA3138F; PSTM=1778398131; ZFY=XDp977cCBvTrimC0IbwQSi4V:ADFzrQml08:AT9TN0i30:C; BAIDUID=6A3DCB3C87BA056AB6E9326BCC8A59BC:FG=1; H_PS_PSSID=63142_67861_68165_69007_69295_70116_70622_70812_70803_70547_70504_70965_70973_70995_71018_71032_71042_71049_71057_71064_71074_71098_71149_71144_71139_71188_71199_71195_71193_71209_71202_71207_71206_71197; BD_HOME=1; BD_UPN=12314753; BA_HECTOR=81a52025a124800k01ak21a085050g1l3fq8329; BAIDUID_BFESS=6A3DCB3C87BA056AB6E9326BCC8A59BC:FG=1

HTTP 按照行的形式来组织,不同的行代表不同的作用。这种按行组织的文本格式,我们也称之为行文本

1.1 首行

bash 复制代码
GET https://www.baidu.com/ HTTP/1.1

GET 后面有一个空格,接着 URL。首行包含三个部分:

  1. HTTP 方法 --- 描述这次请求要干什么
  2. URL --- 描述要访问哪个资源
  3. 版本号
    这三个之间是有空格分开的。

1.2 请求头(header)/ 报头

bash 复制代码
Host: www.baidu.com
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
sec-ch-ua: "Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: BIDUPSID=1AFD334D29967B82C921F7E72BA3138F; PSTM=1778398131; ZFY=XDp977cCBvTrimC0IbwQSi4V:ADFzrQml08:AT9TN0i30:C; BAIDUID=6A3DCB3C87BA056AB6E9326BCC8A59BC:FG=1; H_PS_PSSID=63142_67861_68165_69007_69295_70116_70622_70812_70803_70547_70504_70965_70973_70995_71018_71032_71042_71049_71057_71064_71074_71098_71149_71144_71139_71188_71199_71195_71193_71209_71202_71207_71206_71197; BD_HOME=1; BD_UPN=12314753; BA_HECTOR=81a52025a124800k01ak21a085050g1l3fq8329; BAIDUID_BFESS=6A3DCB3C87BA056AB6E9326BCC8A59BC:FG=1

他本质上是键值对结构,每一行都是一个键值对,键和值之间使用冒号空格分开。

注意:请求头这里边的键值对是标准约定好的,我们程序员是不能随便改的

是我们发明HTTP这些的大佬约定好你这请求头里面的键是哪些键,值对应的是哪些值?代表的含义是什么?它是约定好的,那这些键值对分别是啥意思?我们后面会挑一些来说。

1.3 空行

这里空行的效果起到一个结束标志的作用,它是用来标识你这个请求头header到哪里结束的,那它是至关重要的,我们说header是键值对结构,那么里面有多少个键值对我们并不知道,所以呢此时我们通过这个结束标志来表示这些键值对的结束。

正文/载荷

对于请求来说,有些请求带有正文,而有些请求是不带正文的,正文就是所谓的请求体。

不带正文的:

带正文的:

2、响应

2.1 首行

首行包含三个部分:

  1. 版本号
  2. 状态码
  3. 状态码描述

2.2 响应头

响应头(Header)也是键值对结构,每一行就是一个键值对,键和值之间用「冒号 + 空格」分开。键值对的含义也是标准规定好的。

2.3 空行

空行是响应头的结束标记。

2.4 响应正文body/载荷

这里边的响应正文也是可以空的,不过此处我们的响应中它的响应是一个HTML。


四、HTTP请求各个部分的详细介绍

1、HTTP请求中的URL

首先,URL 并不是 HTTP 的一部分,它是独立于 HTTP 存在的。URL 会给很多协议提供支持。

URL 是统一资源定位符 ,描述了一个资源在网络中的位置。

1.2 协议名称

代表我当前的协议去给哪个用的。

1.3 登录信息

我们现在已经不会把用户名和密码放在url中了,因为如果我们是普通用户,他不熟悉这里边的格式规则,很可能就会写错。

1.4 服务器地址

这里边我们可以写IP地址,也可以写域名,只不过我们现在大部分情况下都是写域名的。

1.5 服务器端口号

咱们通过浏览器输入url访问HTTP服务器,此时我们就得知道对方服务器的端口是多少。

咱们80端口号是给HTTP预留的,所以:

那么对于HTTP来说,如果你服务器的端口确实是80的话,那么我们写url的时候呢是可以省略端口号的,此时呢由浏览器自动给我们填充这个80端口号。

如果你的服务器端口号它不是80的话,比如你的服务端口号是8080,那么此时你就得自己显示的把端口写为8080。

补充:那么与之对应啊,如果你用的是HTTPS协议,然后呢你的服务器的端口确实是443的话,那么此时我们在写url的时候呢是可以不用写443了,由浏览自动给我们填充443端口】

1.6 带层次的文件路径

带层次的文件路径表示你要访问这个程序管理的哪个资源?

我们服务器这边组织资源的时候,也是按照目录结构的形式来组织的。

当然有可能和文件系统的结构不一致,这具体呢取决于服务器代码的实现。

1.7 查询字符串

查询字符串一般是携带参数,对访问指定资源进行补充说明。它一般是键值对格式,而且此处的键值对是我们程序员自定义的

比如:

这里包含若干个键值对,键值对之间用 & 符号分割,键和值之间使用 = 分割。

你也发现了,此处查询字符串中的键值对含义我们是不知道的,因为它是对应程序员自定义的,外人是不知道的。

以后写代码也会重点关注查询字符串这部分,因为实现不同的功能需求时,就需要设置不同的路径、设计不同的查询字符串。

1.8 片段标识符

片段标识符用来区分访问页面的哪个部分。现在不是很常见,一般文档类网站才能见到,比如 Vue 官网。


2、URL ecode

URL 里如果带有特殊字符,就需要进行转义。比如搜索「C++」,URL 里不会直接显示 c++,而是显示 c%2B%2B

平时打开网页、搜索内容时,你大概率见过这种奇怪的地址:

搜索 C++,URL 里不会直接显示 c++,而是变成了 c%2B%2B

这就是 URL 编码(URL encode),也叫 URL 转义。

2.1 为什么要做 URL 编码?

URL 并不是随便写的字符串,它有一套固定语法,里面有些字符是"有特殊身份"的,比如:

  • : 用在协议后面,比如 http:
  • / 用来分割域名、路径等部分
  • ? 表示后面是查询参数(query string)
  • # 表示页面内的锚点片段

这些是 URL 自带的保留字符,浏览器和服务器就是靠它们来识别 URL 结构的。

而问题就出在:

URL 里的查询参数 (? 后面的内容)是用户/程序员自己填的,可能包含中文、符号、空格、+& 等内容。

如果这些自定义内容里的字符,和 URL 本身的保留字符撞车、冲突,浏览器和服务器就会"读不懂"这个 URL,直接解析出错。

所以:

为了不让自定义内容破坏 URL 结构,就必须把特殊字符、中文等进行转义------这就是 URL encode。

不止特殊符号,中文也一样要转义

很多浏览器为了好看,地址栏显示的是正常中文,但真正发请求、抓包时,全是编码后的内容。

2.2 + 为什么会变成 %2B?URL 编码原理是什么?

URL 编码的规则非常简单:

  1. 把要转义的字符,转换成对应的二进制字节
  2. 把每个字节用两位十六进制数表示
  3. 在前面加上一个 %

+ 对应的 ASCII 码十六进制就是 2B,所以编码后就是 %2B

同理,其他特殊字符、中文,都会按这个规则变成 %XX 的形式。

只要查询参数里有:

  • 特殊符号(+&=、空格等)
  • 中文、日文等非英文字符
    必须编码,否则请求大概率失败。

2.3 实际开发中怎么用?

我们不用自己手动算二进制、转十六进制,语言和框架都提供现成方法:

  • 前端 JS 构造请求时,用内置函数做 URL encode【转码】
  • 后端 Java 拿到参数后,需要 URL decode(解码)

如果你用的是 Spring / SpringMVC 这套框架:

解码已经被框架自动处理好了 ,我们在 Controller 里直接用 @RequestParam 取值,拿到的就是正常中文/原文,不用自己写解码逻辑。


简单总结:

URL 编码就是给特殊字符、中文穿一层"安全外套",避免它们破坏 URL 结构,让浏览器和服务器能正确识别、正常传输。


五、请求首行

1、请求首行中的方法(method)

方法描述了我们这次请求要干什么

其中 GET 和 POST 是我们最常用的请求。虽然从语义上来说它们感觉不同,但实际上我们开发的时候并不会严格按语义来区分------约定是理想的,实际实现时我们可以自行灵活掌握。

一般获取 HTML、CSS、JS 等操作使用 GET 请求;登录、上传文件则使用 POST 请求。

1.1 GET请求

GET 请求用于获取资源。

GET 请求通常情况下没有请求体 Body。理论上我们可以给 GET 加上 Body,但实际一般没有这样使用。

GET 请求如果需要给服务器传递数据,通常是通过查询字符串。

1.2 POST

POST 请求相对 GET 少一点,它是给服务器投递一些东西。

POST 和 GET 请求最大的区别就是:POST 带有正文。

我们常用的场景:

1)登录

对于登录,请求带有正文 Body,格式和 Query String 很相似:

对于 POST 来说,Body 的格式有很多种。上面这种格式有一个很长的名字,叫做 application/x-www-form-urlencoded ,就是 form 表单格式。我们的登录页面就是一个典型的 form 表单:

所以登录的时候是一个POST请求。

2)上传

对于上传,请求带有正文 Body,正文中保存了当前上传数据的内容。如果上传的是图片,图片本身是二进制的,但我们会通过特殊方式进行转码(比如 Base64 编码把二进制转为文本)。当然,Body 也可以直接填二进制数据,具体得看你的需求。实际开发中,文件上传更常用的是 multipart/form-data 格式。

如图所示,我们也是一个键值对的方式,值是图片的数据。一张图片本身是二进制格式的数据,但图中的数据并不是二进制,是因为二进制数据在 Body 中被进行了 Base64 转码------用四个字节的文本表示三个字节的二进制数据(将我们的二进制数据转变为文本的形式传输)。

我们上传操作也是一个POST请求。


1.3 PUT 和 DELETE

在实际开发中,我们会遇到一种 RESTful 风格的 API 设计。你写一个 HTTP 服务器给别人调用,服务器能支持哪些 HTTP 请求、返回哪些 HTTP 响应,这我们称之为 HTTP 的 API。RESTful 是当前比较流行的 HTTP API 设计风格。

RESTful 通过不同的方法明确语义:

  1. 使用 GET 实现查询
  2. 使用 POST 实现新增
  3. 使用 PUT 实现修改
  4. 使用 DELETE 实现删除

2、GET 和 POST 的区别

首先,GET 是更常见的请求,POST 相对少一点。

它们的核心区别主要体现在:

  • GET 给服务器携带的数据,通常放在 URL 的查询字符串(Query String)中
  • POST 给服务器携带的数据,通常放在 Body 中

那么这里会有一个经典问题:让你说这两者之间的区别。

首先抛出一个核心结论:GET 和 POST 没有本质区别,主要的区别还是体现在使用习惯上。 也就是说,使用 GET 的时候,绝大部分情况下都能替换成 POST;使用 POST 的时候,也基本能替换成 GET。

  1. 语义区别:GET 表示从服务器获取数据,POST 表示往服务器投递数据。这是设计 HTTP 的那些大佬们的设计初心,但在实际开发中,很多时候我们并不遵守这一点,是混着用的。

  2. 数据传递位置不同:GET 的数据放到 URL 查询字符串中,POST 数据放到请求体 Body 中。但也有特殊情况------GET 中也可以有 Body(只不过极其罕见);POST 中也可以有查询字符串 Query String(相对常见一些)。


网络上有一些资料也谈到了一些区别,但它们是有点争议的。

eg1:POST 比 GET 更安全?

理由是以登录场景为例:如果用 GET 请求,用户名和密码就放到了 URL 的查询字符串中,会显示在浏览器地址栏里,别人看你的屏幕就知道密码了。

但这里说的「安全」太勉强了。我们谈的安全,防范的是网络黑客,而不是防范别人窥屏。况且安全不安全,看的是你是否对密码进行了加密------如果传的是明文,无论 GET 还是 POST 都谈不上安全。即使使用 POST 把密码放在 Body 中,如果传的是明文,黑客也会通过抓包工具抓到你的密码。

eg2:GET 传输的数据量比较小,POST 传输的数据量很大?

依据是说 GET 以查询字符串的形式传数据,而 URL 有长度限制,查询字符串太长超过上限就无法正确传输。

首先,HTTP 标准文档中并没有谈到上限。其次,这句话放在以前 IE 浏览器年代是成立的------IE6 对 URL 长度有限制,传太多数据可能被截断。但现在主流浏览器和服务器早就没有这样的限制了。

eg3:GET 只能传文本数据,POST 只能传二进制数据?

其实 GET 如果需要 URL 携带二进制,完全可以通过 URL encode 来转。

eg4:GET 请求一般是幂等的,POST 请求一般不需要幂等。

幂等的意思是:同样的操作重复执行多次,得到的结果与执行一次相同,且不会产生额外的副作用。

可以记两个关键词:重复稳定

  • 幂等 :输入相同,多次执行,结果始终一样没有副作用

    • 例:读取一篇固定文章 GET /article/123,只要文章没改,今天查和明天查,正文都一样。
    • 例:查字典里「HTTP」这个词,查 1 次和查 10 次,释义相同。
  • 不幂等 :输入相同,多次执行,结果可能不同 ,或每次都有新的副作用

    • 响应会变:两次都是 GET /ads,但广告内容和点击量已经变了(见下图)。
    • 副作用叠加:两次都是 POST /pay 转账 100 元,不小心点两次,就会扣 200 元。

HTTP 标准文档建议把 GET 设计成幂等的,POST 则不必强求幂等;但实践中,像实时广告、库存、点赞数这类接口,GET 返回的数据也会随时间变化,并没有严格做到「相同输入、稳定相同结果」。

上图就是典型的不幂等场景:

  • 相同的输入(请求) :两次都是 GET /ads
  • 不稳定的结果(响应):广告从 A/B 变成了 C/D,点击量从 1024 变成了 2048

正因为响应会实时变化,我们才需要反复请求来更新广告数据;如果每次结果都一模一样,也就不存在「实时更新」的需求了。


六、HTTP中的报头(header)

我们说报头中它的形式是键值对的结构,那我们得了解一些比较常见的键值对。

1、HOST

Host 描述了我们要访问的服务器的 IP(域名)和端口号。

如图所示想说明一件事:IP和端口号这两个信息在url里面也已经有了,但是呢在host里面也有,这是为什么呢?

  • URL 是给客户端用的 :浏览器从 URL 里读出 www.example.com:8080,知道该连哪台服务器。
  • Host 是给服务器用的 :请求到达服务器后,请求行里通常只有路径(如 GET /index.html),服务器要靠 Host 头判断「这次请求该交给哪个站点处理」。
  • 虚拟主机:同一台机器上可能托管多个网站(商城、博客等),Host 不同,返回的网站就不同。HTTP/1.1 因此规定 Host 是必填请求头;服务器也可以用它校验:这个请求是不是发给自己的。

它的作用就是我们在后续服务器收到数据之后,可以针对host来进行校验,确认当前这个请求是我这个服务器要收到请求。

注意了,我们在HTTP协议之中,我们在传输的时候可能会涉及到加密,而我们的url部分的内容并不会被加密,我们被加密的是header和booty部分,同时呢也意味着header中的host也会被拿去加密,这样的好处就是服务器收到请求之后,它可以验证url中的内容和我们所加密host的内容是否一致,从而用于校验。


2、Content-length和content-type

HTTP 报文的标准结构是:首行 → Header → 空行 → Body 。其中 Body 是可选的------GET 请求通常没有 Body,POST 登录、上传文件等则会在 Body 里携带数据。

这两个 Header 是给谁看的?

Header 随报文一起发出去,规则很简单:由发送方填写,给接收方看

发送方自己当然知道 Body 有多长、是什么格式,但接收方不知道 ,所以必须在 Header 里交代清楚。这不是给发送方「提醒自己」用的,而是告诉对方该怎么读 Body

方向 发送方(谁填写) 接收方(谁读取、谁使用)
请求 客户端(浏览器 / App) 服务器
响应 服务器 客户端(浏览器 / App)

当报文带有 Body 时,接收方必须解决两个不同的问题:

问题 由谁回答 作用
Body 读多少字节? Content-Length(或 chunked) 接收方据此在 TCP 字节流上切出完整报文
Body 是什么格式? Content-Type 接收方据此决定如何解析 Body

什么时候需要这两个 Header?

  • 没有 Body (典型如 GET /index.html):读到空行就代表 Header 结束,整个请求/响应读完,一般不需要 Content-Length 和 Content-Type。
  • 有 BodyContent-Type 一般需要 ,告诉对方 Body 的格式;Body 长度也需要明确 ,常见两种方式:
    1. Content-Length: N --- 直接声明 Body 占 N 个字节,读够 N 字节即读完。
    2. Transfer-Encoding: chunked --- 分块传输,逐块读取,遇到长度为 0 的块表示结束(此时不用 Content-Length)。

记忆:Content-Length 管「读多少」Content-Type 管「怎么解析」 ------都是发送方写、接收方读

2.1 为什么需要Content-length?

Content-Length 描述的是 Body 的数据长度。这里涉及一个关键要点:HTTP/1.x 在传输层是基于 TCP 实现的。HTTP 协议就是将字符串构造成约定的格式(首行 → 报头 → 空行 → Body 正文)。而对于 TCP 来说,一个连接上可以发多个请求,此时服务器收到数据时就得区分:从哪里到哪里是一个完整的 HTTP 请求?

其实也很好办,就 3 种情况(见下图):

  1. 没有 Body 的请求:读到空行就代表 Header 结束,整个报文读完。
  2. 有 Body + Content-Length:先读首行和 Header 到空行,再按 Content-Length 的值读取固定字节数。
  3. 有 Body + chunked 分块 :使用 Transfer-Encoding: chunked,逐块读取直到结束块,此时不需要 Content-Length。

2.2 为什么需要content-type?

Body 本质上是一串字节 。同样是字节,可能是 JSON、HTML 页面、表单键值对、PNG 图片......格式不同,解析方式完全不同。Content-Type 就是告诉接收方「请按这种格式来解读 Body」。

Content-Type 在请求响应中都会出现:

请求中 --- 客户端告诉服务器 Body 是什么格式:

响应中 --- 服务器告诉浏览器 Body 是什么格式:

总结:

Content-Length 和 Content-Type 在请求和响应中都会出现,规律一致:谁发报文,谁填写;谁收报文,谁读取


3、User-Agent(简称 UA)仅仅是请求头中的属性

User-Agent 仅仅是请求头中的属性,它描述的是用户使用什么样的设备来上网。

下面是一个典型的 Chrome 浏览器 UA。HTTP Header 是键值对,键名 固定为 User-Agent是冒号后面那一整段字符串:

复制代码
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36

那这样的一个东西有什么作用呢?

其实他在以前是非常有用的。在以前我们互联网刚起步的时候,我们最初的浏览器它的功能比较少,刚开始呢它只能显示文本,后来可以带有样式的,那后来呢还可以带有图片,再后来可以有交互的效果,支持了js,然后在最后我们还可以支持多媒体,视频,音频等等。在这样的一个背景下呢,有这样一个问题,我们有的用户的浏览器是旧版本,它只能支持文本,我们有的用户的浏览器是新版本,各种功能都可以支持,那么你后端返回的那些数据呢?返回多少呢?如果你返回多了,那旧版本的浏览器又带不动。因此呢我们的核心思路就是开发网站的时候,可以根据用户设备支持的情况返回不同版本的页面,那这个用户设备的支持情况我们就在UA中体现。

那么现在的浏览器功能都差不太多,那请问这个UA是不是没有用了呢?其实并不是,现在UA另一个用途就是用来区分当前用户使用的是PC端还是移动端。


4、Referer 仅仅是请求头中的属性

Referer 仅仅是请求头中的属性,用于描述当前页面是从哪个页面跳转过来的。

但是呢这个属性不是都会有的,如果是直接输入url或者说直接点击收藏栏,那么打开的页面它是没有这个属性的。

Referer 的作用是什么呢?

它可以帮助我们追踪请求的来源,应用场景,比如广告点击量的计费情况。广告主有自己的网站去推广打广告,这个页面需要到广告平台去投放广告,比如去百度里面投,去搜狗也投,去360也投放。这些渠道共同为广告主的网站导入流量。

那此时就有一个疑问:在总访问量中,有多少次是通过百度过来的,有多少次是通过搜狗过来的,又有多少次是来自360的呢?

我们这样认真计费,不仅仅是为了优化运营策略,同时也是为了和广告平台结账。那么你的广告点击量是多少,点击了多少次,是由谁来统计的呢?

当然是我们双方都会统计,广告平台和广告主都会统计:

  • 对于我们广告平台来说,点击次数是很容易统计的,因为用户点击广告操作的时候,会先给广告平台的计费服务器发送一个请求,所以就方便统计了。

  • 那么广告主又是怎么统计的呢?他是根据收到的请求中 referer 这个属性来统计的,得看请求是从哪来的,根据这个信息,广告主就可以进行统计了。所以呀,referer 这种属性一般是给我们网站的开发者来使用的。

那么针对上述情况,我们会有这样一个疑问:如果广告通过搜狗带来了1万次的点击,那是否有可能把这个数值篡改掉,使数字变小呢?也就是说,会不会有人把请求的 referer 改成别的,导致广告主统计到的数据变少?

答:那么这种情况在以前是非常普遍的,但是后来大家将广告平台从 HTTP 升级为 HTTPS,此时的 HTTPS 就可以有效地对 HTTP 数据报进行加密传输,包括 referer 在内也被加密了。所以直到现在 HTTP 基本就没用了,网络上都是 HTTPS 传输。

5、Cookie【请求响应中都有】重点

如图,观察之后发现:cookie里面的值也是键值对,而且是自定义的,我们一般看不懂。

先给Cookie下一个定论:Cookie本质上是浏览器存储本地数据的一种机制。

我们浏览器打开一个网站都是现场的从服务器获取代码再执行的,一个网页本身是无法进行本地存储的,也就是说不能直接访问硬盘【这个是我们浏览器做出的限制】,浏览器之所以做出这样的限制:禁止网页访问本地硬盘,核心目的就是为了安全。

因为你想一下,如果你打开一个网页,然后网页可以获取到你本地硬盘数据的话,那么就非常之危险了,所以呢我们为浏览器做了这样的一个限制。

那虽然我们希望提高安全性,但是也有一些场就是网页景需要在本地存储一些数据的:举个例子,很多网站是有个夜间模式的,那么当前页面属于的是哪种模式,这种信息保存在浏览器本地就挺好,没必要从服务器来获取,像这种呢我们去本地存储,从而减低服务器的开销。

这样的方案我们使用cookie,那就是说它相当于浏览器就开了一个口子,虽然不能访问硬盘,但是允许你按照键值对的格式来存储数据,也就是说你不能随心所欲的去访问你的硬盘数据,而是只能通过键值对来做,不光是按照键值对存储,而且还把每个网站涉及到的键值对都分别分开存储。


我们通过 Cookie 几个具体的问题来理解 Cookie 的使用场景和效果。

1. Cookie 是干啥用的?

它是浏览器本地存储的机制。

2. Cookie 是从哪来的?

Cookie 是服务器返回响应的时候,通过响应报头写回到浏览器的。服务器的响应中带有特殊的 Header 报头,就是 Set-Cookie,专门用于完成 Cookie 的设置。

比如抓 Gitee 的请求:

大概的逻辑:

也就是说:

3. Cookie 要到哪里去?

本地存储不是目的,真正的目的是后续要发回给服务器。

本地存储的cookie之后呢,接下来给服务器发的请求中都会在header里面带上Cookie的内容。

那这里边就有一个疑问了,我本身cookie就是从服务器来的,那你为什么每次请求你都要带上这个Cookie又要发回给服务器呢?

答:服务器是 "无状态" 的,且要同时为成千上万的客户端服务,根本记不住每个用户是谁。

就拿网页主题设置举例:客户端 A 把主题改成了红色,客户端 B 设置成了蓝色。如果每次请求不带上 Cookie,服务器就分不清当前访问的是哪个客户端,自然也无法展示对应的主题。所以只有客户端每次都把 Cookie 发回服务器,服务器才能凭借 Cookie 识别用户,返回对应的个性化内容。

归根结底,我们的Cookie还是要给服务器用的。

4. Cookie 的典型场景

Cookie 的典型场景就是:用户身份信息的存储。

也就是登录一个网站,一旦登录完成之后,后续访问这个网站的各个页面。那么都能够获取到我们的登录状态,也是基于cookie机制来实现的。

接下来我们会一步一步的描述这过程:

第一步:

如上图所示,我浏览器访问服务器第一次访问不包含身份信息,然后呢浏览器给我返回一个登录页面。于是接下来我们就输入用户名和密码进行登录,此时我一登陆,我就会给服务器发起了一个登录的请求,这个登录请求包含了我的用户名和密码。

此时服务器就会根据我的用户名和密码查询数据库,验证用户是否存在密码是否正确【我们最初注册的时候,设置的密码是按照加密的形式存在数据库的,那么等你登陆传递密码之后,我们拿着传输的密码进行加密后的效果,再和数据库中加密的密码进行相比】。

这里那你可能会想过你的银行卡密码是否会被银行的工作人员获取到呢?其实是不会的,他最多拿到你密码加密后的密文。

第二步:

如上图所示,如果用户名和密码正确,服务器就会返回一个响应登录成功,此时服务器就会创建一个会话session,通过session保存当前用户的登录状态【这个session你也可以理解成他也是保存的是键值对结构,一个服务器可以同时有多个session,每个客户端都对应一个,然后我们的服务器给每个session分配一个唯一的sessionId作为键值key,给每个session分配一个session对象作为value;你可以理解成服务器的内存中有一个哈希表存储了所有的会话数据】。

此时返回登录成功这个请求,它就会通过Set-Cookie 把 sessionId=XXX 写进去,然后浏览器把这个session ID保存到会话中。

第三步:

如上图所示,然后后续浏览器访问服务器的其他页面,都会自动带上这个 sessionId。此时服务器收到请求,就会根据 sessionId 的值去查询哈希表,找到对应的 session 对象,从而进一步知道用户的信息,基于这个完成后续的逻辑。

注意啊,我们的cookie和session可一点都不像,Cookie只存储一个ID,可是session里面存的可就多了。我们的cookie保存了session ID,然后呢cookie也可以保存别的东西,比如用户的主题,这些是和session无关的。

到现在呢我们稍微总结一下我们HTTP中遇到的键值对:


上述我们说的大部分都是http请求相关的内容,接下来我们说的是http响应格式中的内容。


七、http响应

HTTP 响应中的报头 Header 和正文 Body,我们在请求中也谈过了,所以这里不必多说。我们只说首行中的相关问题。

1、http响应首行中的状态码

状态吗,描述的是响应的结果是正确还是出错?如果是出错的话,那出错的问题又是什么?

我们的Java中引入异常来代替状态码,但是http作为比较老的存在,是通过状态码的方案来标识出错误原因。

状态码提出http的大佬们提前定义好的。那有哪些状态码?分别代表什么呢?【状态码有很多,不需要都记住,只需要认识常见的】

200 OK

200 是最常见的状态码,表示成功。

404 Not Found

404 认为是客户端的问题------客户端发来的 URL 中资源不太对。代表访问的资源没找到。所谓的访问资源,就是通过 IP 定位到主机,通过端口进入到程序,通过 path 定位到程序管理的资源。404 是指 IP 没问题、端口没问题,但通过 path 访问资源时,路径中访问的资源在服务器上不存在。

403 Forbidden(译为「被禁止的」)

代表客户端访问的资源被拒绝,也就是没有权限。

500 Internal Server Error

一旦出现 500,一定是后端出现错误。这相当典型------就是你的后端服务器出现了错误异常,服务器逻辑代码中抛出异常,但是你没有 catch 到。

504 Gateway Timeout(Gateway 译为「网关」)

504 也是服务器出现问题,通常是服务器压力太大,或者上游服务响应超时,导致网关等待超时。

302 Found

302 代表临时重定向。重定向就是你访问服务器 A,然后服务器 A 会告诉你,让你去找 B 吧。

再如:重定向流程 --- 域名升级👇

如上图所示:很多网站可能会出现域名升级的情况。你以前用的那个老的域名去访问某个网站,但是后面他升级了之后呢,我们会把服务器配置成从定向。然后当你访问那个旧的域名的时候,服务器会返回一个新域名给你进行访问,然后浏览器自动去访问新域名,我们普通用户是感觉不到。

同时我们还有一个典型的例子:就是登录页面,登录页面是一个典型的302场景,我们登录成功之后通过302跳转到首页,如下图所示:

301 Moved Permanently

301是也是重定向的效果,但它的重定向效果是永久生效的,对比我们上述的302,我们302重定向的效果是临时生效。他会影响到浏览器的缓存,如果是用301重定向,浏览器就会记住旧的域名和新域名的对应关系,后续你访问旧的域名,浏览器就直接给新域名发请求。如果你使用的是302,浏览器不能缓存对应关系,我们得到的数据就是最新的,所以我们一般使用最多的是302,可能这个描述我们并不清楚,那么我们接下来就仔细描述一下:

301 和 302 看起来都是「让你去另一个地址」,但给浏览器的语气不一样:

301 永久重定向 302 临时重定向
服务器在说什么 「旧地址以后别用了,永久去新地址」 这次先去新地址,下次还来问我」
浏览器会不会记住 会记住旧 → 新的对应关系 一般不长期记住
下次访问旧地址时 浏览器直接请求新地址,可能不再访问旧服务器 浏览器仍会先访问旧服务器,再按最新响应跳转
典型用途 域名彻底更换、永久迁移 登录跳转、临时维护、地址可能还会变
用「搬家」来理解
  • 301 像在门口贴告示:「本店已永久搬到新地址,以后别来这儿了。」浏览器看一次就记住了,下次你输入旧地址,它直接去新地址。
  • 302 像服务员说:「您这次请到隔壁房间。」但没说你以后永远不用来这个房间------下次你来,还得再问一次该去哪。
举个例子:旧域名 old.com → 新域名 new.com

如果是 301:

  1. 第一次访问 old.com → 服务器返回 301,Location: new.com
  2. 浏览器跳转到 new.com,并记住old.com 已经等于 new.com
  3. 第二次 再输入 old.com → 浏览器可能直接new.com 发请求,不再问 old.com 的服务器

如果是 302:

  1. 第一次访问 old.com → 服务器返回 302,Location: new.com
  2. 浏览器跳转到 new.com,但不认为这是永久规则
  3. 第二次 再访问 old.com → 浏览器还是会先 请求 old.com,由服务器决定这次是否还要跳到 new.com(或别的地址)
为什么「302 数据是最新的」?

因为 302 不让浏览器长期缓存「旧地址 = 新地址」这条规则。每次访问旧地址,都会再跟服务器确认一次,所以服务器如果改了跳转目标,浏览器能跟得上。

301 一旦被浏览器记住,旧服务器就算改了配置,用户浏览器里可能还按老映射直接访问新地址,你改起来更麻烦

实际开发怎么选?
  • 用 302(最常见):登录成功跳首页、活动页临时跳转、地址可能还会调整的场景。
  • 用 301 :旧域名确定永远不用了,正式迁到新域名,希望用户和搜索引擎都永久改用新地址。

所以我们日常开发里,302 用得最多;301 多用于「域名彻底搬家」这种一次性、长期不变的变更。


八、写在最后

到这里,HTTP 协议的核心内容就讲完了。我们从 Fiddler 抓包出发,把请求怎么发、响应怎么回、URL 怎么写、Header 里常见字段是什么意思、Cookie 和 Session 怎么配合登录、状态码遇到时该怎么理解,都串了一遍。搞懂 HTTP,以后看接口文档、排查「请求发了但没反应」「登录状态丢了」「页面跳错地址」这类问题,会顺手很多。

如果这篇对你有帮助,欢迎点赞、收藏、关注,你的支持是我继续更新的动力。有问题欢迎在评论区交流,我会尽量回复。

下期预告 :应用层最后一个协议------HTTPS。我们会讲加密传输、证书、和 HTTP 的关系,把网络原理这块收个尾。咱们下期见。