最近在工作中对 http 的请求参数解析有了进一步的认识,写个小短文记录一下。
回顾下自己的情况,大概就是:有点点网络及编程基础,只需要加深一点点对 HTTP 协议的理解就能弄明白了。
先分享一个小故事:我至今仍清晰地记得大三实习时的第一个工作任务,我需要调用其他部门提供的 api 去完成某项业务。
那个 api 文档只告诉了我请求参数需要传什么,没有提及用什么方式传,比如这样:
其实如果有经验的话,直接在请求体或 url 里填参数试一下就知道了;另一个是新人有时候不太敢问问题,其实只要向同事确认一下就好的。
然而由于当时我掌握的编程知识有限,只会用表单提交数据。所以当我下载完同事安利的 api 调用调试工具 postman 后,我就在网上查怎么用 postman 发送表单数据,结果折腾了好久 api 还是没能调通。
当天晚上我向老同学求助,他问我上课是不是又睡过去了?
我说你怎么知道?
他说当然咯,你上课睡觉不学习又不是一天两天的事情......
后来他告诉我得好好学一下 http 协议,看看可以在协议的哪些位置放请求参数。
一个简单的 http 服务器还原那么,在正式讲解之前,我们先简单搭建一个 http 服务器,阿菌沿用经典的 python 版云你好服务器进行讲解。
云你好服务器的代码很简单,服务器首先会获取 name 用户名这个参数,如果用户传了这个参数,就返回 Hello xxx
,xxx 指的是 name 用户名;如果用户没有传这个参数则返回 Hello World
:
# 云你好服务源码
from flask import Flask
from flask import request
app = Flask(__name__)
# 云你好服务 API 接口
@app.get("/api/hello")
def hello():
# 看用户是否传递了参数 name
name = request.args.get("name", "")
# 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
return f"Hello {name}" if name else "Hello World"
# 启动云你好服务
if __name__ == '__main__':
app.run()
为了快速开发(大伙可以下载一个 python 把这个代码跑一下,用自己的语言实现一个类似的服务器也是可以的),阿菌这里使用了 flask 框架构建后端服务。
在具体获取参数的时候,我选择了在 request.args 中获取参数。这里提前剧透一下:在 flask 框架中,request.args 指的是从 url 中获取参数(不过这是我们后面讲解的内容,大家有个印象就好)
抓包查看 http 报文有了 http 服务器后,我们开始深入讲解 http 协议,em...个人觉得只在学校上课看教材学计算机网络好像还欠缺了点啥,比较推荐大家下载一个像 Wireshark 这样的网络抓包软件,动手拆解网络包,深入学习各种网络协议。抓取网络包的示例视频
为了搞清楚什么是请求参数、表单参数、url 参数、Header 参数、Cookie 参数,我们先发一个 http 请求,然后抓取这个请求的网络包,看看一份 http 报文会携带哪些信息。
呼应开头,用户阿菌是个只会发表单数据的萌新,他使用 postman 向云你好 api 发送了一个 post 请求:
剧情发展正常,我们没能得到 Hello 阿菌(服务器会到 url 中获取参数,咱们用表单形式提交,所以获取不到)
由于咱们对请求体这个概念比较模糊,接下来我们重新发一个一模一样的请求,并且通过 Wireshark 抓包看一下:
可以看到强大的 Wireshark 帮助我们把请求抓取了下来,并把整个网络包的链路层协议,IP层协议,传输层协议,应用层协议全都解析好了。
由于咱们小码农一般都忙于解决应用层问题,所以我们把目光聚焦于高亮的 Hypertext Transfer Protocol
超文本传输协议,也就是大名鼎鼎的 HTTP 协议。
首先我们查看一下 HTTP 报文的完整内容:
可以看到,http 协议大概是这么组成的:
- 第一行是请求的方式,比如 GET / POST / DELETE / PUT
- 请求方式后面跟的是请求的路径,一般把这个叫 URI(统一资源标识符)
补充:URL 是统一资源定位符,见名知义,因为要定位,所以要指定协议甚至是位置,比如这样:
http://localhost:5000/api/hello
- 请求路径后面跟的是 HTTP 的版本,比如这里是
HTTP/1.1
完整的第一行如下:
POST /api/hello HTTP/1.1
第二行的 User-Agent 则用于告诉对方发起请求的客户端是啥,比如咱们用 Postman 发起的请求,Postman 就会自动把这个参数设置为它自己:
User-Agent: PostmanRuntime/7.28.4
第三行的 Accept 用于告诉对方我们希望收到什么类型的数据,这里默认是能接受所有类型的数据:
Accept: */*
第四行就非常值得留意,Postman-Token
是 Postman 自己传的参数,这个我们放到下面讲!
Postman-Token: ddd72e1a-0d63-4bad-a18e-22e38a5de3fc
第五行是请求的主机,网络上的一个服务一般用 ip 加端口作为唯一标识:
Host: 127.0.0.1:5000
第六行指定的是咱们请求发起方可以理解的压缩方式:
Accept-Encoding: gzip, deflate, br
第七行告诉对方处理完当前请求后不要关闭连接:
Connection: keep-alive
第八行告诉对方咱们请求体的内容格式,这个是本文的侧重点啦!比如我们这里指定的是一般浏览器的原生表单格式:
Content-Type: application/x-www-form-urlencoded
好了,下面大家要留意了,第九行的 Content-Length 给出的是请求体的大小。
而请求体,会放在紧跟着的一个空行之后。比如本请求的请求体内容是以 key=value
形式填充的,也就是我们表单参数的内容了:
Content-Length: 23
name=%E9%98%BF%E8%8F%8C
看到这里我们先简单小结一下,想要告诉服务器我们发送的是表单数据,一共需要两步:
- 将
Content-Type
设置为application/x-www-form-urlencoded
- 在请求体中按照
key=value
的形式填写请求参数
好了,接下来我们进一步讲解,大家试想一下,网络应用,其实就是端到端的交互,最常见的就是服务端和客户端交互模型:客户端发一些参数数据给服务端,通过这些参数数据告诉服务端它想得到什么或想干什么,服务端根据客户端传递的参数数据作出处理。
传输层协议通过 ip 和端口号帮我们定位到了具体的服务应用,具体怎么交互是由我们程序员自己定义的。
大概在 30 年前,英国计算机科学家蒂姆·伯纳斯-李定义了原始超级文本传输协议(HTTP),后续我们的 web 应用大都延续采用了他定义的这套标准,当然这套标准也在不断地进行迭代。
许多文献资料会把 http 协议描述得比较晦涩,加上协议这个词听起来有点高大上,初学者入门学习的时候往往感觉不太友好。
其实协议说白了就是一种格式,就好比我们写书信,约定要先顶格写个敬爱的 xxx,然后写个你好,然后换一个段落再写正文,可能最后还得加上日期署名等等。
我们只要按照格式写信,老师就能一眼看出来我们在写信;只要我们按协议格式发请求数据,服务器就能一眼看出来我们想要得到什么或想干什么。
当然,老师是因为老早就学过书信格式,所以他才能看懂书信格式;服务端程序也一样,我们要预先编写好 http 协议的解析逻辑,然后我们的服务器才能根据解析逻辑去获取一个 http 请求中的各种东西。
当然这个解析 http 协议的逻辑不是谁都能写出来的,就算能写出来,也未必写得好,所以我们会使用厉害的人封装好的脚手架,比如 java 里的 spring 全套、Go 语言里的 Gin 等等。
回到我们开头给出的示例:
from flask import Flask
from flask import request
app = Flask(__name__)
# 云你好服务 API 接口
@app.get("/api/hello")
def hello():
# 看用户是否传递了参数 name
name = request.args.get("name", "")
# 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
return f"Hello {name}" if name else "Hello World"
# 启动云你好服务
if __name__ == '__main__':
app.run()
阿菌的示例使用了 python 里的 flask 框架,在处理逻辑中使用了 request.args 获取请求参数,而 args 封装的就是框架从 url 中获取参数的逻辑。比如我们发送请求的 url 为:
http://127.0.0.1:5000/api/hello?name=ajun
框架会帮助我们从 url 中的 ? 后面开始截取,然后把 name=ajun
这些参数存放到 args 里。
切换一下,假设我们是云你好服务提供者,我们希望用户通过表单参数的形式使用云你好服务,我们只要把获取 name 参数的方式改成从表单参数里获取就可以了,flask 在 request.form 里封装了表单参数(关于框架是怎么在数行 http 请求中封装参数的,大家可以看自己使用的框架的具体逻辑,估计区别不大,只是存在一些语言特性上的差异):
@app.post("/api/hello")
def hello():
# 看用户是否传递了参数 name
name = request.form.get("name", "")
# 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
return f"Hello {name}" if name else "Hello World"
思考:我们可以在 http 协议中传递什么参数?
最后,我们解释本文的标题,其实想要明白各种参数之间的区别,我们可以换一个角度思考:
咱们可以在一份 http 报文的哪些位置传递参数?
接下来回顾一下一个 http 请求的内容:
POST /api/hello HTTP/1.1
User-Agent: PostmanRuntime/7.28.4
Accept: */*
Postman-Token: fbf75035-a647-46dc-adc0-333751a9399e
Host: 127.0.0.1:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
name=%E9%98%BF%E8%8F%8C
大家看,咱们的 http 报文,也就是基于传输层之上的应用层报文,大概就长上面这样。
我们考虑两种情况,第一种情况,我们基于别人已经开发好的脚手架开发 http 服务器。
由于框架会基于 http 协议进行解析,所以框架会帮助我们解析好请求 url,各种 Header 头(比如:Cookie 等),以及具体的响应内容都帮我们封装解析好了(比如按照 key=value 的方式去读取请求体)。
那当我们开发服务端的时候,就可以指定从 url、header、响应体中获取参数了,比如:
- url 参数:指的就是 url 中 ? 后面携带的 key value 形式参数
- header 参数:指的就是各个 header 头,我们甚至可以自定义 header,比如 Postman-Token 就是 postman 这个软件自己携带的,我们服务端如果需要的话是可以指定获取这个参数的
- Cookie 参数:其实就是名字为 Cookie 的请求头
- 表单参数:指的就是 Content-Type 为 application/x-www-form-urlencoded 下请求体的内容,如果我们的表单需要传文件,还会有其他的 Content-Type
- json 参数:指的就是 Content-Type 为 application/json 下请求体的内容(当然服务端可以不根据 Content-Type 直接解析请求体,但按照协议的规范工程项目或许会更好维护)
综上所述,请求参数就是对上面各种类型的参数的一个总称了。
大家会发现,不管什么 url 参数、header 参数、Cookie 参数、表单参数,其实就是换着法儿,按照一定的格式把数据放到应用层报文中。关键在于我们的服务端程序和客户端程序按照一种什么样的约定去传递和获取这些参数。这就是协议吧~
还有另一种情况,当然这只是开玩笑了,比如以后哪位大佬或者哪家企业定义了一种新的数据传输标准,推广至全球,比如叫 hppt 协议,这样是完全可以自己给各种形式参数下定义取名字的。这可能就是为啥我们说一流的企业、大佬制定标准,接下来的围绕标准研发技术,进而是基于技术卖产品,最后是围绕产品提供服务了。
一旦标准制定了,整个行业都围绕这个标准转了,而且感觉影响会越来越深远......
讲解参考链接