本文全网原创,从报文结构分析HTTP协议,足以帮助任何人理解HTTP协议的各种请求方式。文中观点纯属个人观点,如有错误,可批评指正。

很多时候,面试官可能会问你:

请你说一下HTTP三次握手和四次挥手。

当你足够了解HTTP和TCP的关系时,你就会知道面试官的那个问题是不准确的,甚至是错误的。

首先介绍一下什么是传输层协议:

传输层协议的概念我就懒得COPY了,只看概念也不知道是啥玩意儿。这么来说吧,传输层协议就是不同的交通工具。主要有TCP和UDP两种,而交通工具运输的是什么呢?运输的就是应用层协议规定的内容。

那么,具体什么是TCP协议?

TCP协议是一个有连接的全双工协议(全双工协议就是我们两个可以同时说话,还能听清楚对方说的是啥),它属于传输层协议。TCP是有连接的,因此有建立连接和断开连接的两个过程。建立连接就是所谓的三次握手,断开连接就是所谓的四次挥手。有连接保证了TCP的稳定传输,但是降低了效率。

与之相对的是UDP协议,这里也顺便介绍一下吧:

UDP协议是无连接的,和TCP的优缺点互补。特点就是只管往外发,不管你收没收到,我发出去就行了。

看到这里,你懂面试官的问题哪里不合理了吗?

HTTP协议你可以理解为乘客,TCP你可以理解为交通工具,交通工具怎么开的跟乘客有什么关系?

下面重点解释HTTP协议:

HTTP协议就是一个对HTTP报文结构作明确规定的一个规范。说白了就是HTTP协议只负责你长什么样,你坐什么交通工具不归我管。目前,HTTP协议都是使用的TCP协议作为交通工具。但是,HTTP协议本身并没有强制要求你只能使用TCP协议。 至于为什么使用TCP协议?那你吃饭为什么要用筷子而不是直接用手?

补充:
HTTP是无状态,无连接的协议。

  1. 无状态:当你打开浏览器发送一个请求,收到了一个响应。然后你再重新打开浏览器发送一个请求,服务器并不知道两次请求是同一个客户端。
  2. 无连接:有连接是对于传输层TCP协议来说的,这里的连接并不是HTTP规定的。TCP相当于交通工具,而HTTP相当于乘客,交通工具规定了如何运行(有连接),而乘客(HTTP协议)并没有规定自身如何建立连接,HTTP规定的是一次请求一次响应,所以协议本身是没有连接的。
  3. 长连接:在HTTP1.1的时候,请求头有个Connection参数,并且默认值为keep-alive,用于保持长连接。此时的Content-Length的长度就很重要,如果长度不够,那么后面的参数会被截断。
  4. 短链接:在HTTP1.0的时候默认是短链接。Connection的值为close

看到这里,你可能懂了吧?HTTP协议并不是只能用TCP协议实现,所以面试官的问题是有问题的。

那么,HTTP也能使用UDP作为交通工具?
答案是,可以。这里再补充一点,我们根据HTTP协议的要求构造了一个HTTP请求报文(其实就是一个字符串)。那么别人就知道我们想传输的内容是什么了吗?别人并不知道。想要知道对方请求的是什么内容,你必须按照HTTP协议规定的报文结构去进行解析。而目前已经有大量的工具完成了解析HTTP报文的工作,这些工具就是Web服务器。web服务器主要干三件事:

  1. 接收别人的请求报文
  2. 解析别人的请求报文
  3. 做出响应

但是目前来说,Web服务器都只实现了接收TCP协议运输过来的报文,不接收UDP协议运输过来的报文。同理,目前的HTTP客户端(也就是制造请求报文,给web服务器发送报文的工具,浏览器就是一个HTTP客户端)也只实现了通过TCP协议传输HTTP报文。但是,如果我们自己实现一个支持通过UDP协议传输HTTP报文的客户端和Web服务器就可以实现上面的问题。

最新版的HTTP3协议就明确规定了使用UDP作为交通工具。

相信看到这里,你已经非常了解传输层和应用层协议之间的关系,传输层负责如何传输,应用层负责业务逻辑。

最后,从报文结构详细分析HTTP协议:

下面是一个完整的表单方式的GET请求:

GET /form?name=111&aaa=bbb&param=%20%20%E9%9A%8F%E5%BF%83%25%25%3D%3D%2B%2B%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20 HTTP/1.1
Accept: */*
User-Agent: J-HttpClient/1.0
Connection: keep-alive
Host: localhost:8080
Content-Length: 0
Content-Type: application/x-www-form-urlencoded

报文每一行结束都是一个回车换行(占两个字符)。
第一行也叫请求行,由三个部分组成。第一个部分表示请求方式,这里是GET请求,第二个部分表示请求路径,包括请求参数第三个部分表示HTTP协议版本。请求参数是以我们熟悉的key/value形式拼接在请求路径上。第二行到最后一行叫请求首部,我管他叫请求头。最后一行Content-Type表示这个请求是以什么类型提交,application/x-www-form-urlencoded是一个编码的表单提交,这种方式提交的参数,都需要进行编码,所有的中文都会变成上面看见的形式。

下面是一个完整的表单方式的POST请求:

POST /form?name=111 HTTP/1.1
Accept: */*
User-Agent: J-HttpClient/1.0
Connection: keep-alive
Host: localhost:8080
Content-Length: 110
Content-Type: application/x-www-form-urlencoded

aaa=bbb&param=%20%20%E9%9A%8F%E5%BF%83%25%25%3D%3D%2B%2B%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20

我们可以认真对比一下这个报文和GET请求报文的区别:

  1. 请求行标注了POST
  2. 请求行的请求路径依然有拼接的参数,只不过参数会少很多
  3. 请求体之后多了一行空行,并且多了一行参数。

这是一个需要编码的表单提交,最后一行被编码的参数叫做请求体,请求体和请求头之间有一个空行(空行就是回车符号+换行符号,你看见的每一行最后都有一个回车换行)分割。

为什么参数被分割在了请求路径和请求体两部分?
其实不难发现POST提交和GET提交的参数是完全一样的,只不过我想说明的是,POST方式依然可以使用GET方式提交参数(请求路径后面的部分)。并且请求路径后面的参数和GET提交的参数有相同性质(数据量限制,不安全),而请求体部分的参数才是POST提交专有的性质(安全,数据量更大)。

现在可以发现GET和POST提交的区别了吧?其实GET提交参数的方式适用于任何别的提交方式(包括JSON提交等等,也可以在URL上提交参数)。

下面再来一个简单的JSON提交方式:

POST /json HTTP/1.1
Accept: */*
User-Agent: J-HttpClient/1.0
Connection: keep-alive
Host: localhost:8080
Content-Length: 59
Content-Type: application/json

{"name":"  随心%%==++                  ","age":"  11 "}

其实请求路径后面也是可以跟参数的,和GET类似。和POST的主要区别如下:

  1. Content-Type的值为application/json
  2. 请求体部分为json字符串,并且是没有编码的

因为有请求体,所以只能POST提交。

最后再来一个不编码的表单提交:

POST /upload HTTP/1.1
Accept: */*
User-Agent: J-HttpClient/1.0
Connection: keep-alive
Host: localhost:8080
Content-Length: 126127
Content-Type: multipart/form-data;  boundary=--------------------------66487d1a67364530b3e2f509d5261357

----------------------------66487d1a67364530b3e2f509d5261357
Content-Disposition: form-data; name="single_file"; filename="456.png"
Content-Type: application/octet-stream

这里省略一堆ISO-8859-1编码的字符
----------------------------66487d1a67364530b3e2f509d5261357
Content-Disposition: form-data; name="name"

这里省略一堆ISO-8859-1编码的字符
----------------------------66487d1a67364530b3e2f509d5261357
Content-Disposition: form-data; name="batch_files"; filename="asd.txt"
Content-Type: text/plain

asdasf
afa4你好24

----------------------------66487d1a67364530b3e2f509d5261357
Content-Disposition: form-data; name="batch_files"; filename="dasd.txt"
Content-Type: text/plain

这里省略一堆ISO-8859-1编码的字符
----------------------------66487d1a67364530b3e2f509d5261357
Content-Disposition: form-data; name="batch_files"; filename="111"
Content-Type: application/octet-stream

这里省略一堆ISO-8859-1编码的字符
----------------------------66487d1a67364530b3e2f509d5261357
Content-Disposition: form-data; name="batch_files"; filename="aaa"
Content-Type: application/octet-stream

你好
----------------------------66487d1a67364530b3e2f509d5261357--

和POST提交的区别:

  1. Content-Type后面的值不同,类型是multipart/form-data; 后面跟了一串分隔符。
  2. 请求体部分比较复杂,下面单独分析。请求体和请求头之间有一个空行,空行后面所有内容都是请求体。

下面是不编码的表单提交的参数基本结构:

----------------------------66487d1a67364530b3e2f509d5261357
Content-Disposition: form-data; name="single_file"; filename="456.png"
Content-Type: application/octet-stream

这里省略一堆ISO-8859-1编码的字符

首先是一个分隔符,这个分隔符就是Content-Type后面定义的。
第二行是这个参数的元信息,部分是固定写法,后面有这个参数的名字。所有的参数你都可以想成是键值对的,而键就是后面name的值,这里的键是single_file,值是省略的数据。filename是这个上传的文件原始文件名。
下面还有一行是这个文件的MIME类型,如果不知道是什么类型,则直接使用通用类型:application/octet-stream

下面说一下省略的字符是什么:
从文件中读取数据,最后以ISO8859-1编码。如果你上传的是txt文件,并且txt文件的内容是ASCII规定的字符,那么你就可以看见不是乱码的字符。

当然,此方式提交的参数不仅仅可以是文件,也可以是普通的字符串:

----------------------------66487d1a67364530b3e2f509d5261357
Content-Disposition: form-data; name="name"

这里省略一堆ISO-8859-1编码的字符

这里提交的就是健为name,值为一串中文的的参数(这里省略了)。中文汉字将以UTF-8解码之后再以ISO8859-1进行编码,然后就成了乱码。因此,这里省略了。

本文就是HTTP协议的各种请求方式报文,最后想深入学习HTTP报文结构的人可以看下我写的HTTP异步客户端

Q.E.D.


擅长前端的Java程序员