HTTP 协议
前面讲述完传输层,接下来开始讲应用层的协议。从哪里开始讲呢,就从咱们最常用的 HTTP 协议开始。
HTTP 协议,几乎是每个人上网用的第一个协议,同时也是很容易被人忽略的协议。
既然说看新闻,咱们就先登录 http://www.163.com 。
http://www.163.com 是个 URL,叫作统一资源定位符。之所以叫统一,是因为它是有格式的。HTTP 称为协议,www.163.com 是一个域名,表示互联网上的一个位置。有的 URL 会有更详细的位置标识,例如 http://www.163.com/index.html 。正是因为这个东西是统一的,所以当你把这样一个字符串输入到浏览器的框里的时候,浏览器才知道如何进行统一处理。
HTTP请求准备
浏览器会将 www.163.com 这个域名发送给 DNS 服务器,让它解析为 IP 地址。有关 DNS 的过程,其实非常复杂,这个在后面专门介绍 DNS 的时候,我会详细描述,这里我们先不管,反正它会被解析成为 IP 地址。那接下来是发送 HTTP 请求吗?
不是的,HTTP 是基于 TCP 协议的,当然是要先建立 TCP 连接了,怎么建立呢?还记得第 11 节讲过的三次握手吗?
目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面,默认是开启了 Keep-Alive 的,这样建立的 TCP 连接,就可以在多次请求中复用。
学习了 TCP 之后,你应该知道,TCP 的三次握手和四次挥手,还是挺费劲的。如果好不容易建立了连接,然后就做了一点儿事情就结束了,有点儿浪费人力和物力。
HTTP请求的构建
建立了连接以后,浏览器就要发送 HTTP 的请求。
请求的格式就像这样:
HTTP 的报文大概分为三大部分:
- 请求行
- 请求的首部
- 请求的正文实体。
请求实例:
POST http://127.0.0.1:8085/uniQualityWeb/member/login HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.21.0
Accept: */*
Cache-Control: no-cache
Postman-Token: f1fa1852-2ee9-42d8-84a8-a3a9f8bb02d7
Host: 127.0.0.1:8085
Accept-Encoding: gzip, deflate
Content-Length: 58
Connection: keep-alive
{
"userCode":"dev",
"pwd":"123",
"identifyCode":"111"
}
第一部分:请求行
在请求行中,URL 就是 http://www.163.com ,版本为 HTTP 1.1。这里要说一下的,就是方法。方法有几种类型。
常见的方法类型:
- GET
- POST
- PUT
- DELETE
对于访问网页来讲,最常用的类型就是 GET。顾名思义,GET 就是去服务器获取一些资源。对于访问网页来讲,要获取的资源往往是一个页面。其实也有很多其他的格式,比如说返回一个 JSON 字符串,到底要返回什么,是由服务器端的实现决定的。
例如,在云计算中,如果我们的服务器端要提供一个基于 HTTP 协议的 API,获取所有云主机的列表,这就会使用 GET 方法得到,返回的可能是一个 JSON 字符串。字符串里面是一个列表,列表里面是一项的云主机的信息。
另外一种类型叫做 POST。它需要主动告诉服务端一些信息,而非获取。要告诉服务端什么呢?一般会放在正文里面。正文可以有各种各样的格式。常见的格式也是 JSON。
例如,我们下一节要讲的支付场景,客户端就需要把“我是谁?我要支付多少?我要买啥?”告诉服务器,这就需要通过 POST 方法。
再如,在云计算里,如果我们的服务器端,要提供一个基于 HTTP 协议的创建云主机的 API,也会用到 POST 方法。这个时候往往需要将“我要创建多大的云主机?多少 CPU 多少内存?多大硬盘?”这些信息放在 JSON 字符串里面,通过 POST 的方法告诉服务器端。
还有一种类型叫 PUT,就是向指定资源位置上传最新内容。但是,HTTP 的服务器往往是不允许上传文件的,所以 PUT 和 POST 就都变成了要传给服务器东西的方法。
在实际使用过程中,这两者还会有稍许的区别。POST 往往是用来创建一个资源的,而 PUT 往往是用来修改一个资源的。
例如,云主机已经创建好了,我想对这个云主机打一个标签,说明这个云主机是生产环境的,另外一个云主机是测试环境的。那怎么修改这个标签呢?往往就是用 PUT 方法。
再有一种常见的就是 DELETE。这个顾名思义就是用来删除资源的。例如,我们要删除一个云主机,就会调用 DELETE 方法。
第二部分:首部字段(请求头)
请求行下面就是我们的首部字段。首部是 key value,通过冒号分隔。这里面,往往保存了一些非常重要的字段。
这里需要重点说一下的就是缓存。为啥要使用缓存呢?那是因为一个非常大的页面有很多东西。
例如,我浏览一个商品的详情,里面有这个商品的价格、库存、展示图片、使用手册等等。商品的展示图片会保持较长时间不变,而库存会根据用户购买的情况经常改变。如果图片非常大,而库存数非常小,如果我们每次要更新数据的时候都要刷新整个页面,对于服务器的压力就会很大。
对于这种高并发场景下的系统,在真正的业务逻辑之前,都需要有个接入层,将这些静态资源的请求拦在最外面。
这个架构的图就像这样:
其中 DNS、CDN 我在后面的章节会讲。和这一节关系比较大的就是 Nginx 这一层,它如何处理 HTTP 协议呢?对于静态资源,有 Vanish 缓存层。当缓存过期的时候,才会访问真正的 Tomcat 应用集群。
在 HTTP 头里面,Cache-control 是用来控制缓存的。当客户端发送的请求中包含 max-age 指令时,如果判定缓存层中,资源的缓存时间数值比指定时间的数值小,那么客户端可以接受缓存的资源;当指定 max-age 值为 0,那么缓存层通常需要将请求转发给应用集群。
另外,If-Modified-Since 也是一个关于缓存的。也就是说,如果服务器的资源在某个时间之后更新了,那么客户端就应该下载最新的资源;如果没有更新,服务端会返回“304 Not Modified”的响应,那客户端就不用下载了,也会节省带宽.
到此为止,我们仅仅是拼凑起了 HTTP 请求的报文格式,接下来,浏览器会把它交给下一层传输层。怎么交给传输层呢?其实也无非是用 Socket 这些东西,只不过用的浏览器里,这些程序不需要你自己写,有人已经帮你写好了。
常见的请求头字段:
Accept:这个头信息指定浏览器或其他客户端可以处理的 MIME 类型。有
text/html
,image/
等几种常用类型。/*
可以简单的概括为告诉服务器,客户端什么数据类型都支持。Accept-Charset:表示客户端可以接受的字符集。防止传过来的是另外的字符集,从而导致出现乱码。
Content-Type: 是指正文(请求体)的格式。例如,我们进行 POST 的请求,如果正文是 JSON,那么我们就应该将这个值设置为 JSON。
Accept-Encoding:这个头信息指定浏览器知道如何处理的编码类型。
gzip
或compress
是最常见的两种可能值。Accept-Language:这个头信息指定客户端的首选语言,在这种情况下,Servlet 会产生多种语言的结果。例如,
en
、en-us
、ru
等。Authorization:这个头信息用于客户端在访问受密码保护的网页时识别自己的身份。
Content-Length:这个头信息只适用于 POST 请求,并给出 POST 数据的大小(以字节为单位)。
Cookie:这个头信息把之前发送到浏览器的 cookies 返回到服务器。
Cookie: requestUrl=/index.html; JSESSIONID=7BBF114FE94F3EB12C4AD1A36658CA0D
Host:这个头信息指定原始的 URL 中的主机和端口。
Referer:这个头信息指示所指向的 Web 页的 URL。例如,如果您在网页 1,点击一个链接到网页 2,当浏览器请求网页 2 时,网页 1 的 URL 就会包含在 Referer 头信息中。
User-Agent:这个头信息识别发出请求的浏览器或其他客户端,并可以向不同类型的浏览器返回不同的内容。
首部字段详细描述:HTTP协议首部字段
第三部分:请求实体(请求体)
请求头和请求体之间有一个空行,用于区分它们两者。
GET请求实际上也是能在请求体中带参数的,只不过有些服务器(尤其是缓存代理服务器)可能不支持这种方式而已。而ES的设计者认为GET比较符合检索信息的这个动作,比POST要好,所以使用了GET请求,不过为了与习惯保持一致,而且为了兼容所有的服务器,所以把GET换成POST也是可以的。
HTTP 请求的发送
HTTP 协议是基于 TCP 协议的,所以它使用面向连接的方式发送请求,通过 stream 二进制流的方式传给对方。当然,到了 TCP 层,它会把二进制流变成一个的报文段发送给服务器。
在发送给每个报文段的时候,都需要对方有一个回应 ACK,来保证报文可靠地到达了对方。如果没有回应,那么 TCP 这一层会进行重新传输,直到可以到达。同一个包有可能被传了好多次,但是 HTTP 这一层不需要知道这一点,因为是 TCP 这一层在埋头苦干。
TCP 层发送每一个报文的时候,都需要加上自己的地址(即源地址)和它想要去的地方(即目标地址),将这两个信息放到 IP 头里面,交给 IP 层进行传输。
IP 层需要查看目标地址和自己是否是在同一个局域网。如果是,就发送 ARP 协议来请求这个目标地址对应的 MAC 地址,然后将源 MAC 和目标 MAC 放入 MAC 头,发送出去即可;如果不在同一个局域网,就需要发送到网关,还要需要发送 ARP 协议,来获取网关的 MAC 地址,然后将源 MAC 和网关 MAC 放入 MAC 头,发送出去。
网关收到包发现 MAC 符合,取出目标 IP 地址,根据路由协议找到下一跳的路由器,获取下一跳路由器的 MAC 地址,将包发给下一跳路由器。
这样路由器一跳一跳终于到达目标的局域网。这个时候,最后一跳的路由器能够发现,目标地址就在自己的某一个出口的局域网上。于是,在这个局域网上发送 ARP,获得这个目标地址的 MAC 地址,将包发出去。
目标的机器发现 MAC 地址符合,就将包收起来;发现 IP 地址符合,根据 IP 头中协议项,知道自己上一层是 TCP 协议,于是解析 TCP 的头,里面有序列号,需要看一看这个序列包是不是我要的,如果是就放入缓存中然后返回一个 ACK,如果不是就丢弃。
TCP 头里面还有端口号,HTTP 的服务器正在监听这个端口号。于是,目标机器自然知道是 HTTP 服务器这个进程想要这个包,于是将包发给 HTTP 服务器。HTTP 服务器的进程看到,原来这个请求是要访问一个网页,于是就把这个网页发给客户端。
HTTP 响应的构建
HTTP 的返回报文也是有一定格式的。这也是基于 HTTP 1.1 的。
第一部分:状态行
状态码会反应 HTTP 请求的结果。“200”意味着大吉大利;而我们最不想见的,就是“404”,也就是“服务端无法响应这个请求”。然后,短语会大概说一下原因。
第二部分:首部(响应头)
这里面,Retry-After 表示,告诉客户端应该在多长时间以后再次尝试一下。“503 错误”是说“服务暂时不再和这个值配合使用”。
在返回的头部里面也会有 Content-Type,表示返回的是 HTML,还是 JSON。
第三部分:实体(响应正文)
注意:在响应头和响应正文之间会有一个空行,用以区分两者。
响应体实例:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: requestUrl=/article/news/37.html; Path=/
Content-Type: text/html;charset=UTF-8
Content-Language: zh-CN
Transfer-Encoding: chunked
Date: Mon, 27 Jan 2020 04:36:28 GMT
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta author="tianjindong.cn 田金东">
...
HTTP响应的发送
构造好了返回的 HTTP 报文,接下来就是把这个报文发送出去。还是交给 Socket 去发送,还是交给 TCP 层,让 TCP 层将返回的 HTML,也分成一个个小的段,并且保证每个段都可靠到达。
这些段加上 TCP 头后会交给 IP 层,然后把刚才的发送过程反向走一遍。虽然两次不一定走相同的路径,但是逻辑过程是一样的,一直到达客户端。
客户端发现 MAC 地址符合、IP 地址符合,于是就会交给 TCP 层。根据序列号看是不是自己要的报文段,如果是,则会根据 TCP 头中的端口号,发给相应的进程。这个进程就是浏览器,浏览器作为客户端也在监听某个端口。
当浏览器拿到了 HTTP 的报文。发现返回“200”,一切正常,于是就从正文中将 HTML 拿出来。HTML 是一个标准的网页格式。浏览器只要根据这个格式,展示出一个绚丽多彩的网页。
这就是一个正常的 HTTP 请求和返回的完整过程。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论