前言
hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
在上面HTTP(一)博客中解析了抓包工具Fiddler的下载和使用,以及如何获取交互的请求和响应报文,这篇博客就会具体解析请求和响应报文格式,也就是在解析HTTP协议的相关内容
比如前端和后端配合完成一个功能,出现bug了,就要确定这个bug是前端的问题,还是后端的问题,也就是通过抓包来分析请求和响应:
1.浏览器发送的请求就是错误的 -> 前端的问题
2.请求正确,响应错了 -> 后端的问题
3.请求正确,响应正确,显示错误 -> 前端的问题
🎆个人主页:洋不写bug的博客
🎆所属专栏:JavaEE学习
🎆铁汁们对于JavaEE的各种常用核心语法,都可以在上面的JavaEE专栏学习,专栏正在持续更新中🏀,有问题可以写在评论区或者私信我哦~
1,URL
请求和响应的报文的格式如下图,接下来解析请求报文的格式

请求行部分为 Method URL Version,就拿获取到的豆包响应来看(如下图),Get就是请求方法(Method),Version就表示版本,也就是最后的HTTP/1.1,首先来解析下URL

URL部分其实就是我们俗称的网址,URL(Uniform Resource Locator)翻译为统一资源定位符。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置已经浏览器应该怎样处理它
URL组成:
- 协议名,例如htt或者https协议
- 登录信息认证(这种认证方式不安全,现在已经基本上淘汰了)
- 服务器地址,这里输入服务器的域名,实际上还是转化为IP来访问服务器
- 服务器端口号,通过端口号区分把请求给哪个应用程序,HTTPS 默认端口是 443,HTTP 默认端口是 80。如果用的是默认端口,URL 里可以省略不写
- 带层次的文件路径,一个机器上的服务器程序可能管理着很多的资源,可能是真实的文件,也可能是"虚拟的","动态生成"的资源,文件路径就是定位这些文件的位置
- 查询字符串用?开头,是键值对的形式,通过=来分割键和值,通过&来分割键值对,通过查询字符串,就可以让客户端给服务器传递一些参数
- 标识网页的某个部分,实现"页面内跳转"的功能,文档类网站就会带有这个,以#开头

?前面的豆包请求的首行部分,查询字符串如下所示,用?开头,中间用&隔开,这个键的名字是程序员自己约定的,不同的网站键的名字也是不同的,因为客户端要给服务器传递的参数是不同的
GET https://www.doubao.com/chat/
?channel=microsoft
&source=microsoft_db_17
&type=daoh
&theme=wangzjh
这个查询字符串就可以类比餐馆点餐来理解,博主是洛阳人,假设博主在小街天府2楼的1号档口开了个汤馆,菜单如下:
汤类
1)牛肉汤
2)羊肉汤
3)驴肉汤
饮料类
1)海碧
2)可乐
饼类
1)饼丝
2)葱油饼
3)油璇
那这时候这个汤馆就是服务器,来消费的顾客就是客户端,客户端发送的URL请求,如下图:

用#的标识部分一般在文档类网站才能见到,如下图:

在百度主页这里,这里的url就特别简单,只有协议和域名,目的端口就用的是默认的,刚打开主页也没有查询字符串部分(本来就是程序员自己定义的),首页无需传递参数
当路径为/时,也能省略不写,表示访问服务器中最顶层的资源/目录,通常网站主页的URL对应就是这样的

在浏览器上搜索字节跳动,会发现URL的查询字符串部分有个q=字节跳动,这个就表示搜索的内容

但是把这个URL复制下来,粘贴到记事本中,就会发现value部分变了(如下图)
因为value部分的内容会包含一些特殊符号,比如/、&、?,就可能使得URL的解析出现错误,因此一些value部分的内容就需要进行转义
除了大小写英文、数字、
-、_、.、~,其他符号都是需要转义的

可以在UTF8编码转换器中看下,这里转义就是按十六进制,每个字节前加一个%,在浏览器上显示时,浏览器自动对value进行了解码

因此,前端拿到用户输入,把内容拼进 URL 查询参数key=value之前,就需要做url encode,进行百分号转义
2,Method
1,GET
方法表如下图,这个是HTTP中的方法,跟Java中的方法是没有关系的

最常用的方法就是两个
- Get:从服务器上获取某个资源
- Post:向服务器上传资源
很多操作都会触发HTTP的Get请求:
- 直接在浏览器地址栏输入url去访问某个网站(在收藏夹中点击也是一样)
- 在页面上点击链接跳转时
- HTML间接加载其他资源的时候(CSS、JSS、图片)
通过link标签加载css、通过script标签加载js、通过img标签加载图片... - 也可以通过js/java/C++/Python手动构造Get请求
下面就测试一下,先清空Fiddler中的列表,接着在浏览器地址栏输入www.bilibili.com访问B站,在表列表中找到交互,就是通过Get方法从服务器获取html资源



接着在B站中点开一个视频(页面上点击链接跳转),在列表中根据url找到这条交互,打开响应部分,使用的也是Get



铁汁们可以随便在列表中点几个点几个交互看一下请求部分,如果是Get请求,几乎都是没有body(请求正文)的,理论上强制给Get请求添加body部分也是可以的,但是通常不会这样做,Get请求想要给服务器传递数据,一般是通过路径或者query string来进行传递的
例如前面在B站中点开个视频,url前面部分是www.bilibili.com/video/BV1aH8b61Epb/,有个BV号BV1aH8b61Epb,服务器就是通过这个BV号知道要看哪个视频,然后返回一个对应的详细的页面
在浏览器上搜索内容,就是通过query string来向服务器传递搜索的内容,如下图

2,POST
Post请求主要是在登录或者上传资源时使用,Post请求通常都带有body,通过body给服务器传递数据(通常是不用query string来传递的),通过body向服务器传递数据
还有个Put方法,也是向服务器上传资源,HTTP规范是让上传文件资源时用Put,因为不是强制约束的,实际上Post和Put的界限是很不清晰的,用Post传输文件资源也是不会报错的,一些公司就是都用Post
这里博主在gitee切换一个极光的头像,也就是向服务器上传数据,接着在列表中打开URL为/profile/update_portrait,就是更新头像的交互,请求部分就是POST请求


在记事本中查看,会发现这个body部分特别长,body部分存储的就是图片的内容,这里就是通过base64编码,把二进制内容转化为文本的内容 (当然,body部分也是可以传输二进制数据的,这里转成文本,是因为服务器处理起来比价方便)
这个base64编码就是通过4个ascii字符,表示3个字节的二进制数据

有一个面试的高频题目:谈谈Get和Post的区别,主要有两点
- Get通常没有body,通过query string传递数据给服务器,Post通常有body,不需要通过query string传递数据
- 语义上的区别,Get表示获取,Post表示提交
网上会有一种这样的说法:Get请求是不安全的,Post请求比Get请求要安全,理由就是:在登录场景中,Get就会把用户名和密码现在到URL上,黑客看一眼屏幕,就知道密码是什么;Post请求的用户名和密码是放在body中的,黑客看屏幕也不知道密码是什么
这个说法是不科学的,因为判断安全不安全主要看密码有没有进行加密传输,如果没有加密传输,黑客也可以通过抓包来获取我们的密码;加密传输了,直接在URL中显示,也是比较安全的
Get请求官方是建议实现幂等性的,Post请求则无要求幂等性就是请求一定,得到的结果就一定,就比如说奶牛,给奶牛喂草,就能挤出来奶,这个就满足幂等性,Get如果实现了幂等,就可以把结果缓存起来(也就是下次访问不一定要访问服务器了,直接从缓存中拿结果),Post不实现幂等,就不用缓存
但是现在很多网站都是"个性化推荐",就比如B站网页,铁汁们每次刷新,首页推荐的视频都是不一样的,是不满足幂等性的
还有个说法:Get请求传输的数据量少,Post请求传输的数据量大,因为Get请求的URL长度存在限制,但HTTP标准文档中没有对URL的长度做出限制
这个长度的限制主要来自于浏览器/HTTP服务器的实现,当今的浏览器的相比之前上限已经高很多了(不同的浏览器的上限可能不同)
3,header(请求)
1,User-Agent
大部分Post请求的header部分都有个User-Agent属性,里面的信息包括浏览器内核、浏览器版本、操作系统(Windows10、macOS、安卓、iOS)、设备信息(手机 / PC)...

早期的浏览器访问网站是纯文本的,后来有了多媒体(图片、音频、视频),再后来有了一些复杂的交互
这就出现了个问题,有的用户用的是老版本的浏览器,只支持文本,有的用的是新版本,支持多媒体和交互,那就要提供多个版本的网站,有只包括文字版本的、包含文字和多媒体版本的,就是通过看User-Agent属性中的信息来确定提供哪个版本(这个只是早期,现在网站的形态以及基本定格了)
网站在手机端和PC端的展示效果是不一样的(手机屏幕宽,计算机屏幕窄),具体怎么展示也是看User-Agent属性中的设备版本,如果是Windows、MacOS就返回PC版本,如果是Android、IOS就返回手机版
在前端开发中还有一个技术叫"响应式编程",代码可以根据浏览器窗口的宽度,自动适应,铁汁们打开豆包网页版本,拖动一下浏览器窗口的大小,网页内容就会自动适应

2,Referer属性
在搜狗浏览器上搜索蛋糕加盟,先清空Fiddler中的列表,再点击一个广告链接,在Fiddler列表中根据host找到这个交互



在记事本中打开请求报文,会发现header部分有个referer属性,内容是一个url,把这个url粘贴下来在浏览器中访问下,这个url表示的就是在搜狗浏览器上搜索蛋糕加盟这个页面(也就是刚刚点开牛约堡网页的上个页面)
referer属性中存储了上个页面的url,通过referer就能得知当前页面是从哪个页面跳转过来的


浏览器右上角有个回退箭头,点击后就能回到上个页面,但这个功能其实跟referer属性没有关系,这个功能是浏览器维护了一个栈来实现的

referer属性主要是让服务器看的,一个广告主会给多个平台投放广告(例如搜狗、百度、夸克),后续就是按照点击量来给平台付费,广告主就需要分清楚这个广告链接到底是从哪个平台点进去的,就是通过看referer属性来计算每个平台的点击量的
当然,广告平台也有"反作弊"算法,能够区分出"有效点击"和"无效点击",对于"有效点击"才计费,用脚本来刷点击量就属于是"无效点击"
运营商劫持就与这个有关
例如搜狗浏览器,广告主通过看服务器收到的请求的referer得出搜狗的点击量,搜狗内部也会进行计算,最后两方对下账,广告主再付钱,但是在十年前,就经常会出现两方点击次数有很大差异的情况,比如搜狗内部计算出点击量是1000次,但是广告主服务器发现收到的referer来自搜狗的点击只有500次
这个就是运营商分公司的一些内部人员搞的鬼,也称为"运营商劫持",因为当时使用的是HTTP传输,不是HTTPS,并没有加密,运营商在返回的 HTML 页面里偷偷插入一段 JS 脚本,把Referer改成运营商中转页,进而获取广告佣金
运营商 HTTP 劫持是国内网站大规模推进 HTTPS 的核心原因之一,HTTPS的链路全部加密 + 完整性校验,运营商中间设备只能看到域名,看不到网页内容、请求头,也不能篡改任何响应报文(HTTPS在其他方面的安全程度也更高,例如防止密码泄露,具体的加密逻辑会在下篇博客中解析)
3,Cookie
在浏览器上搜索东西,请求和header部分有个Cookie属性,如下图,Cookie中存储的是键值对,用;(分号)来分割多个键值对

网站和app是有区别的,app(下载在桌面上的程序)能在计算机硬盘上存储数据,也就是把数据存储在客户端
但是app的安全程度是更高的,app在应用商店中是经过了审核的,但是网站是无人审核的,风险性就比较高了,因此浏览器就禁止网站访问计算机的硬盘,也禁止网站更改其他的应用程序,那如果网站确实有在客户端保存数据的需求,应该怎么办呢?通过Cookie,服务器返回给浏览器的响应中带有Cookie的内容,浏览器把Cookie保存到硬盘中,后续浏览器访问该网站时,Cookie中的数据就通过HTTP的请求报头,带到服务器上
使用Cookie,网站就能通过键值对的方式来简单来存储简单的数据,但是网站是访问不到硬盘的
如下图,在相应报头中就是通过setCookie让浏览器保存Cookie信息
点击地址栏前面的这个锁,再点击Cookie和网站数据
浏览器本地存储cookie就是按照域名来进行管理的(一个公司会有多个域名)
也能查看cookie的详细信息,比如创建时间和到期时间,到期后就会自动删除
很多网站需要登录,比如说gitee,登录成功后,后续点击其他页面来访问服务器时,就不需要再重复登录了,实现这个就需要靠Cookie中的身份凭证
如下图,浏览器在网页上输入账号和密码,发送登录请求,接着服务器在数据库中查询账号和密码,进行校验,就生成一个用户的"身份标识",也就是sessionId(会话ID),后续用户访问服务器的时候,在Cookie中就有sessionId的信息,服务器就知道这个用户已经登录过了,无需重复登录
客户端访问服务器,服务器给客户端一个身份标识,就相当于是客户端和服务器之间维持了一个会话,至于会话的存活时间,就要看具体的业务场景了,对于安全性要求越高的场景,会话存活的时间就越短
比如CSDN网站,存活的时间是很长的;而对于支付宝、网络银行这些网站,存活时间就比较短(防止用户在公共计算机上登录后,忘记退出,导致别人使用这个账号来进行一些操作)
SessionId 就是给用户身份打的唯一编号,用户业务数据全部保存在服务器,这个就类似于医院的就诊卡大概在十年前,在很多医院看病需要先办一张就诊卡,往上面充点钱,这个就诊卡存储的信息很少,只是代表患者的一个编号,很多具体的信息,例如卡内金额、检查报告、既往病史,这些数据都是存储在服务器中的(这些数据如果是存储在就诊卡上,那卡一丢,患者的数据就丢完了😅),后续医生刷这个就诊卡,服务器就能根据编号查询患者的信息,显示给医生
Cookie是按域名维度来存储的,不同浏览器打开同一个网站,拿到的 sessionId 一定不一样,两个 sessionId 是相互独立的两套会话
Cookie并不是浏览器实现存储的唯一方案,还有一些其他的方案,例如LocalStorage也是通过键值对方式来存储,也是按照域名维度来存储;IndexDB类似于数据库表结构组织的方式
4,状态码
接下来进入响应部分的解析,响应格式如下图:
在列表中打开一个微软必应搜索的交互,在记事本中看下响应的报文(点击上面的黄色方框解压下body部分)


首行部分就是版本号 + 状态码 + 状态码描述
HTTP/1.1 200 OK
状态码用来表达「请求层面」成功 / 失败;区分一些情况,这里状态码是200,就表示查询/更新成功了
状态码是很多的,主要分为5大类,如下图,重点了解一些即可

1,1xx
1xx 属于临时信息响应,只是服务端告诉客户端:"你的请求我收到了,继续发剩下的数据",这个是很少见的,Fiddler列表中的result列就表示状态码,在Fiddler列表中几乎看不到这个状态
2,2xx
2xx就表示成功,只是成功的方式不同
例如200 OK,表示访问成功,在Fiddler列表中的result列就表示状态,如下图,大部分交互的状态都是200 OK
注:200 OK表示的是HTTP层面的成功,不是业务层面的成功 ,比如注册账号时,提交了跟别人相同的用户名,导致注册失败了,这个交互的状态仍然是200 OK,表示HTTP层面上成功的把数据提交到了服务器至于业务层面的成功失败,通常会使用其他的属性标识,比如在body的json中引入专门的code/ok/success这样的属性标识
3,3xx
3xx就表示重定向,重定向就是访问一个地址,重新定位到另一个地址
比如换了个手机号,但是别人不知道,别人打这个旧的手机号时,就会自动转移到新的号码上
像广告就会出现这种情况,点开链接会先访问计费服务器进行点击计费,再重新定位到这个广告服务器,这里在搜狗上搜索修手机,打开一个广告,接着在列表中找到这个状态码为302的交互,这个交互访问的就是搜狗的计费服务器


可能有的铁汁会说:这不是搜狗的计费服务器吗,为什么域名有个qq.com呢,这是因为搜狗在21年已经被腾讯收购了

再看下这个交互的响应,header部分有个Location属性,里面存储的是修手机界面的url,Location属性就表示接下来要跳转到哪个界面

访问过程如下图,用户先访问腾讯的计费服务器,计费服务器返回广告主服务器的url,用户再去访问广告主的服务器

对于很多没有技术团队的小商家,他们不用写代码,在腾讯广告后台拖拽组件做页面(放图片、电话、表单),页面静态资源托管在腾讯的服务器 / 云资源上,服务器运维、宕机、带宽全部是腾讯负责维护
4,4xx
4xx就表示请求错误,最常见的就是一个404 NOT FOUND状态,如下图,在地址栏中访问百度的666.html资源,但是明显百度服务器是没有这个资源的,就返回404界面

一些网站在返回404响应时,会在body中加入精美的html,来提升用户的体验,就比如B站(如下图)

这个404属于是异常情况,在列表中就会标红

403 Forbidden就表示访问被拒绝了,因为没有访问权限,就比如在gitee中登录后,打开自己的私有仓库,复制这个页面的url,接着退出gitee的登录,再用这个链接来访问私有仓库,服务器就会拒绝访问,浏览器页面如下图:

开发时,对于用户没有访问权限这种情况,还可以返回200 OK状态,通过body的code属性来返回出错信息(具体看公司习惯)
这个4xx开头最常见的就是404和403,一个是用户访问不存在的资源,一个是用户没有权限还要访问,属于是客户端出错,也就是用户的问题
5,5xx
5xx就是服务器出现了问题,比较常见的是502 Bad Gateway和504 Gateway Timeout
Gateway的翻译是网关,网关就是网站集群最前面的总大门,所有用户的请求,都必须先经过网关,再转发到后面的业务服务器,502和504就表示服务器机房的入口服务出现了问题502就表示服务器挂掉了,网关连接不上服务器;504表示网关成功连上服务器,但是后端处理太慢,网关等不到结果,时间到了直接放弃,返回 504
因为知名网站对于稳定性有严格的要求,会采用多种方案来避免服务器挂掉(比如说多服务器集群、自动监控和自动处理)
500 internal Server Error这个状态,服务器没有挂,但是服务器程序抛出了异常
5,header(响应)
1,Content-Length
接着是header部分,有两个重要的属性:Content-Length和Content-Type,一个描述了body的长度,一个描述了body的数据格式(在请求/响应中,存在body才有这两个属性)

HTTP是基于TCP的,这个Content-Length就是为了解决TCP中的"粘包问题",TCP连接中会传输多个HTTP请求/响应,需要让应用程序区分哪里到哪里是一个完整的应用层数据包
Content-Length的单位是字节,不是行

HTTP区分从哪里到哪里是一个完整的HTTP请求/响应的方法:
- 如果没有body,直接读取到空行处,就是一个完整的HTTP请求/响应
- 如果有body,header中必然有Content-length属性,表示body部分占多少个字节,读取到空行后,就是body的开始,再读取Content-Length个字节,就是一个完整的HTTP请求/响应
2,Content-Type
Content-Type属性,例如Content-Type: text/html; charset=utf-8,就表示响应传递的是html数据,其中的中文部分是按照utf-8来编码的
常见的Content-Type:
- text/html
- text/css
- application/javascript
- application/json
- image/png
- image/jpg
浏览器就是根据Content-Type,来确定body部分的数据如何使用
如果一个请求/响应,有body,但是header部分没有Content-length和Content-Type,那这个请求/响应就是非法的
但是浏览器很可能并不会报错,会根据body部分的数据,来"猜"这个数据是什么类型的,有很大概率会猜对,没有Content-length浏览器也能按照下个请求一定是Get/Post来猜body有多长
这个就称为鲁棒性(英文音译),表示了一个系统的容错能力,商业级软件就要考虑鲁棒性,也就是程序遇到非法输入、异常环境、网络抖动、依赖故障时,不会直接崩溃,能合理报错、降级、恢复,而不是挂掉

6,Restful规范
对于后端开发来说,就需要写一些服务器,提供HTTP API(应用程序编程接口),给前端/浏览器进行调用,这个就属于是库/框架提供的API,就需要有一定的规范设计约束设计风格
Restful风格就是目前定义 HTTP API 最主流的设计风格,主要要求:
- 通过请求中的方法来表示不同的语义
GET:查询
POST:新增
PUT:修改
DELETE:删除 - 通过URL的路径,来表示操作的资源(例如前面B站打开视频,服务器就是通过路径中来返回视频信息)
- 请求和响应携带的数据,都尽量使用json格式的数据
{
key:value1,
key:value2
} - 通过HTTP响应的状态码,表示失败的原因
7,服务器访问
有一个比较朴素的访问服务器的方法,就是按照HTTP请求的格式,往TCP socket中写入字符串,就可以构造出HTTP的请求出来(铁汁们可以看前面的网络编程(二)博客来复习下TCP通信编码)
构造HTTP请求的逻辑不复杂,写个get方法,方法参数是url,接着拼接字符串,分为首行部分、header部分、空行部分,拼接后用outputStream发送给服务器接着用inputStream以字节数组的形式来接收服务器的响应,再把响应转化为字符串,进行打印,代码如下:
java
package network;
import java.io.*;
import java.net.Socket;
public class HttpClient {
private Socket socket = null;
public HttpClient(String serveIp,int port) throws IOException {
socket = new Socket(serveIp,port);
}
public void get(String url){
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
String firstLine = "GET" + url + "HTTP/1.1\n";
String headers = "Host: " + socket.getInetAddress().getHostAddress() + ":" + socket.getPort() + "\n";
String blankLine = "\n";
String httpRequest = firstLine + headers + blankLine;
//发送请求
outputStream.write(httpRequest.getBytes());
outputStream.flush();
//读取响应
byte[] bytes = new byte[1024 * 1024];
int n = inputStream.read(bytes);
String httpResponse = new String(bytes,0,n);
System.out.println(httpResponse);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
但是现在面向互联网普通用户的网页,几乎不再用 HTTP了,为了安全性,浏览器使用的是HTTPS,因此这段代码是没办法测试运行的
用tcp socket来构造出HTTPS请求是非常复杂的,但是可以使用一些现成的库来辅助写代码,在开发中测试程序时,可以使用工具直接构造出HTTP请求
比较经典的工具就是Postman,铁汁们可以在官网中下载使用一下,注册一个账号来使用
登录成功打开后,点击页面中的加号(下图中已圈出),就会进入到这个Resquest界面

用Get请求,后面输入https://www.sogou.com,可以在下面Query Params(参数)中填写键值对,用于向服务器传递信息,也就是query string

如下图,可以在Headers定义报头中的属性和属性值;在Body中选择body中是否为空,如果不为空要携带的数据类型,以及携带什么数据

构造完成请求后点击send,下面就会显示服务器返回的html响应

还可以点击preview进行预览

在实际开发中,后端和前端是各自独立进行开发的,那后端把服务器写完了,就要用Postman或者类似工具来给服务器发送请求,看服务器的响应是否正常;前端代码写好后,也会搞个模拟服务器来测试下,看前端代码是否有问题
当然,最后前端和后端还会一起进行联合测试,提前自己测试一下,就可以排除一些明显的bug
PostMan是老牌客户端,全球开发者在大量使用,还有个类似的国产工具叫Apifox,在团队多人协作方面会方便一些,项近几年国内的一些项目团队开始使用Apifox
Apifox,铁汁们也可以下载个Apifox玩一玩
apifox官网链接
Apifox界面如下图
结语💕💕
- 开发软件基本上都是客户端和服务器的通信,HTTP也属于是程序员必备的基础知识,相关知识点比较多,建议多用Fiddler抓包来看下相关属性的效果
- HTTPS的加密由多个步骤组成,下篇博客会详细解析这些步骤,以及每个步骤都是如何来反制黑客攻击的
以上就是今天的所有内容啦~完结撒花~🥳🎉🎉

🎆个人主页:







