前言
如果你不想花多长时间去读繁芜的理论书,而是希望从简单易懂的语言中快速了解HTTP 协议基础和进阶内容,那么这篇博客适合你。
本文由浅入深,除了整理了 HTTP 的基础概念、常用字段、Cookie、缓存等基础至进阶的内容,还适当增加部分面试题的答案,例如 HTTP2.0跟1.1的区别、从 URL 输入到页面的展示发生了什么、浏览器缓存、HTTP状态码等等高频面试题。
由于 HTTP 的相关知识博大复杂,故而我只将比较常见且我能懂的知识用通俗的话叙述出来,确保不班门弄斧,同时也是对阅读我博客的朋友们负责。
本文参考主要来源于《透视 HTTP 协议》和《图解 HTTP》,还有部分资料参考于 MDN,在定义上,它更像是一个读书笔记,只是我把书中很多繁芜的知识剔除,做了这样一份整理。
如果您觉得我整理的知识对您有用,那么随手给笔者点个赞是莫大的鼓励👍🏻👍🏻。
最后,本人在维护一个 github博客地址,主要用于从学习到总结,记录前端比较重要的知识点,涉及 Javascript 深入、HTTP 协议、数据结构和算法、浏览器原理、ES6等内容,目前正在更新,欢迎大家 🌟🌟star🌟🌟
github 地址: 我的github
下面正文开始:
HTTP的由来
史前时期
20 世纪 60 年代,美国国防部高等研究计划署(ARPA)建立了 ARPA 网,它有四个分布在各地的节点,被认为是如今互联网的“始祖”。
然后在 70 年代,基于对 ARPA 网的实践和思考,研究人员发明出了著名的 TCP/IP 协议。由于具有良好的分层结构和稳定的性能,TCP/IP 协议迅速战胜其他竞争对手流行起来。
创世
1989 年,一位名叫蒂姆·伯纳斯 - 李(Tim Berners-Lee)的博士发表了一篇论文,提出了在互联网上构建超链接文档系统的构想。
这篇论文中他确立了三项关键技术。
- URI:即统一资源标识符,作为互联网上资源的唯一身份;
- HTML:即超文本标记语言,描述超文本文档;
- HTTP:即超文本传输协议,用来传输超文本。
基于这个思想,李博士把这个系统称为“万维网”(World Wide Web),也就是现在的 Web。在这一年,HTTP 诞生了。
HTTP/0.9
由于当时互联网世界受到内存、cpu 计算能力、网速等多重影响,在 HTTP 涉及初始时,结构简单,它也采用纯文本形式。这时候只允许 GET 请求,通过这个动作从服务器获取 HTML 文档,并且请求一次后马上关闭连接。
虽然它很简单,但是作为一个原型,很成功完成了历史使命,验证了 web 的可行性,为后来的拓展打下基础。
HTTP/1.0
1993年,实际上第一个可以图文混合的浏览器 Mosaic 诞生,随后1995年服务器软件 Apache 诞生,简化了 HTTP 服务器的搭建工作。
同时期,计算器多媒体类似于 JPEG 和MP3的诞生促进了 HTTP 的发展。这时候进入1.0时代,这个版本从形式上跟目前的 HTTP 差别非常小。比如:
- 增加了 HEAD、POST 等新方法;
- 增加了响应状态码,标记可能的错误原因;
- 引入了协议版本号概念;
- 引入了 HTTP Header(头部)的概念,让 HTTP 处理请求和响应更加灵活;
- 传输的数据不再仅限于文本。
HTTP/1.1
1995 年,网景的 Netscape Navigator 和微软的 Internet Explorer 开始了著名的“浏览器大战”,这一大战极大推动了 Web 的发展。
1999年,HTTP/1.1发布 RFC文档,此后这份文档延续了十余年。相较于1.0,它变更了以下内容:
- 增加了 PUT、DELETE 等新的方法;
- 增加了缓存管理和控制;
- 明确了连接管理,允许持久连接;
- 允许响应数据分块(chunked),利于传输大文件;
- 强制要求 Host 头,让互联网主机托管成为可能。
HTTP2.0与1.1的区别
由于1.1版本存在连接慢、无法跟上迅猛发展的互联网等问题,Google 公司为了解决这个问题,在自家流行的 Chrome 浏览器上应用了自家的服务器,并且推出新的 SPDY 协议。这时候互联网标准化组织以此为基础最终发布了2.0版本。
HTTP2.0充分考虑到目前的宽带、安全性、移动互联网等情况,补充了1.1版本的性能短板:
- 二进制协议,不再是纯文本;
- 可发起多个请求;
- 压缩请求头,减少数据传输量;
- 允许服务器主动向客户端推送数据;
- 增强了安全性,“事实上”要求加密通信。
不过2.0版本直到今天还不够普及。
HTTP3.0的延伸
谷歌公司再次发布更加优化版本的 QUIC 协议,在自己的 Chrome 浏览器中试验,以庞大用户量和数据为支撑,持续推动 QUIC 协议的发展。
2018年,国际化标准组织以此为标准发布了HTTP3.0版本。有可能我们会直接跳过2.0采用3.0的标准。
HTTP 是什么
HTTP 的全称是超文本传输协议(HyperTextTransfer Protocol),从名字来看,这个协议可以拆成三个部分:
- 超文本
- 传输
- 协议
协议
什么是协议呢?这个就跟平常生活中的协议:劳动协议、租房协议等是一个概念,HTTP 协议的本质也是类似于这样的协议。
我们从协议的字面意思入手,首先协是协同的意思,也就是说会有多方参与。其次,正是有了多方的协同,才需要一些基本的交流礼仪和行为约定,这就是议。
协议意味着多个参与者为了同样的目的而站着一起,为了保证协同工作,必须制定各方的责任、权利等行为约定。
那么自此,HTTP 的第一层含义出来了:
HTTP 是一个用在计算机世界里的协议。它使用计算机能够理解的语言确立了一种计算机之间交流通信的规范,以及相关的各种控制和错误处理方式。
传输
HTTP 是一个用于传输的协议,传输(transfer)的意思就是把某个东西从 A 搬运到 B 点。它包含以下两个信息:
1.HTTP 是双向协议
HTTP 的传输起码需要保证有两个参与者,A 与 B。数据需要在 A 跟 B 之间双向流动。通常把先发起动作的 A 称之为请求方,后接收传输方称为响应方。
比如浏览器就经常是请求方,而响应方则是服务器,他们依靠 HTTP 协议进行通信,浏览器将数据请求发送给服务器,服务器返回一些格式的数据,最后浏览器拿到数据并进行渲染展示。
2.数据虽然是双向传输,但是允许有中转。
原先的 A<==>B,变成了 A<==>中转站1<==>中转站2等<==>B,只要不影响 A 到 B 的传输,原理上传输过程中运行存在多个中间人。这样中间人同样也遵循HTTP 协议,但是他们可以做数据转发、安全认证、数据压缩等功能,优化传输过程。
由此,我们知道 HTTP 的第二层含义:
HTTP 是一个在计算机世界里专门用来在两点之间传输数据的约定和规范。
超文本
既然称文本(Text),就表示HTTP 传输的是一段完整的、有意义的数据,可以被浏览器、服务器等处理,并不是二进制包。
在早期,传输的文本只是普通的文字,但是现在,我们可以传输音频、图片、视频等多种数据流,所以超文本的意思就是超越普通文本,是多种数据的混合体文本,它还可以包括超链接,能够从一个超文本跳转到另外一个超文本,形成网状的结构关系。
HTML 就是超文本的一种,他本身只是纯文字的文件,但是他的标签可以定义图片、视频的连接,最后被浏览器所解释,形成含有多种视听信息的页面。
那么超文本的解释就是
文字、图片、音频、视频等超文本数据的集合
小结
HTTP 是一个在计算机世界里专门用于两点之间传输视频、音频、图片、文字等超文本数据的通信约定和规范。
虽然HTTP 没有实体,但是依赖需要技术实现,它也是构建互联网的重要基础和技术
HTTP 相关的应用
Web 服务器
浏览器是 HTTP 中的请求方,而协议的另一方就是应答方==>服务器,Web Server
Web服务器有两个层面的含义:硬件和软件。
硬件指的就是物理形式或者云形式的机器,在多数情况下它不一定是服务器,而是利用反向代理、负载均衡等多种技术组成的集群。
软件就是提供 Web 服务的应用程序,通常运行在硬件含义的服务器上,它利用硬件能力响应海量的 HTTP 请求,处理各种网页、图片等文件,或者转发请求等等。
目前世界上最流行两类 Web 服务器,分别是老牌的 Apache 服务器(特点:功能完善,学习门槛低)和后起之秀 Nginx(特点:高性能、高稳定性)。
CDN
CDN 全称是内容分发网络Content Delivery Network,它是浏览器和服务器之间的内容架设,它应用了 HTTP 协议中的缓存和代理技术,能够代理源站响应客户端的请求。
它有什么好处呢?
它可以缓存源站数据,让浏览器不用再千里迢迢到达源站服务器,而是在半路直接获取响应。如果 cdn 的调度算法优秀,那么就可以找到距离用户跟进的节点,大幅度缩短响应时间。
爬虫
浏览器是一种用户代理,代替我们访问互联网。
但是这个代理也可以是机器人,这些机器人就称为爬虫。本质上是一种可以自动访问 Web 资源的应用程序。
爬虫是怎么来的呢?绝大多数都是由搜索引擎放出来的,它能够抓取网页并存入庞大的数据库中,再建立关键字索引,这样我们才可以在搜索引擎中搜索到互联网的各个页面。
爬虫也有一些不好的地方,比如导致敏感信息泄露。所以,现在也出现了一种反爬虫技术,通过各个手段来限制爬虫。
Web Service
Web Service 和 Web Server的名字很像,但是确实完全不同的东西。
它是基于 Web(HTTP)的服务架构技术,既可以运行在内网,也可以在适当保护后运行在外网。
因为采用了 HTTP 协议传输数据,所以在 Web Service 架构里服务器和客户端可以采用不同的操作系统或编程语言开发。例如服务器端用 Linux+Java,客户端用 Windows+C#,具有跨平台跨语言的优点。
小结
-
互联网上绝大部分资源都使用 HTTP 协议传输,浏览器是 HTTP 协议里的请求方,即 User Agent; 服务器是 HTTP 协议里的应答方,常用的有 Apache 和 Nginx;
-
CDN 位于浏览器和服务器之间,主要起到缓存加速的作用;
-
爬虫是另一类 User Agent,是自动访问网络资源的程序;
-
Web Service 是一种服务架构技术,具有跨平台跨语言的特点。
HTTP 相关的协议
TCP/IP
TCP/IP 协议是目前网络世界“事实上”的标准通信协议,是一系列网络通信协议的统称,其中最核心的两个协议是TCP和IP。
它还有其他协议例如:UDP、ICMP、ARP 等等。他们共同组成一个复杂的协议栈。
这个协议有四层
- 应用层
- 传输层(TCP 归属层)
- 网际层(IP 归属层)
- 链接层
IP 协议是Internet Protocol的缩写,它主要用来解决寻址和路由以及如何传送数据包的问题。
IP 系统用了 IP 地址这个概念来定位每一台计算机。对应电话系统,需要打电话必须要接入电话网,由通信公司分配一个号码,这个号码就相当于 IP 地址。
TCP 协议是Transmission Control Protocol的缩写,意思是“传输控制协议”,它位于 IP 协议之上,基于 IP 协议提供可靠的、字节流形式的通信,是 HTTP 协议得以实现的基础。
可靠指的是保证数据不流失,字节流则保证数据完整。
TCP/IP是可靠的、完整的协议
DNS
TCP/IP使用 IP 地址来标识计算机,由于计算机本身处理的就是数据,这样的地址对应计算机相当方便,但是却不利于人类记忆。
DNS 域名系统就可以解决这个问题。它用了一组有意义的名字来替代 ip 地址,并建立映射关系。比如:访问 www.baidu.com就是访问百度的 IP 地址14.215.177.38(不止这一个)
在 DNS 中,域名又称主机名,它被设计成一个非常有层次的结构。其中,最右边的比如.com,.cn等都是顶级域名
顶级域名下面是二级域名,它位于顶级域名的左侧。例如,在zh.wikipedia.org中,wikipedia是二级域名。w3.org中,w3也是二级域名。
二级域名下面是三级域名,它位于二级域名的左侧。例如,在zh.wikipedia.org中,zh是三级域名
想要使用 TCP/IP 协议来通信仍然要使用 IP 地址,所以需要把域名做一个转换,“映射”到它的真实 IP,这就是所谓的“域名解析”。
URI 和 URL
DNS 和 IP 地址标记了互联网上的主机,但是主机上有大量文本、网页等,需要找哪一个呢?
这就出现了 URI,全称统一资源定位符标识符。使用它可以唯一地标记互联网上的资源。
URI 的另一个表现形式是 URL(统一资源定位符),就是我们俗称的网址。它是 URI 的子集,但是两者相差不大。
URI是怎样的呢?
http://nginx.org/en/download.html
上面的URI 由三个基本部分组成:
1.协议名:http
2.主机名:主机标记位置,可以是域名或 ip 地址。这里是nginx.org
3.路径,也就是资源在主机上的位置,用/分隔成多个目录。这里是/en/download.html
HTTPS
这是 HTTP 的安全版本,由于 HTTP 协议不够安全,所以在 TCP/IP协议之上,又架设一层 SSL/TLS 的协议,而 HTTPS 的意思就是运行在在 SSL/TLS 协议上的 HTTP。
全称为HTTP over SSL/TLS
SSL/TLS是负责加密通信的安全协议,也是可靠的传输协议
SSL/TLS一开始叫 SSL(Secure Socket Layer),后来改名叫 TLS(Transport Layer Security),由于历史原因很多人称之为 SSL/TLS。
它使用了许多密码学的研究成果,综合了对称加密、非对称加密、摘要算法、数字签名、数字证书等技术,能够在不安全的环境中为通信的双方创建出一个秘密的,安全的传输通道。
如果有网址的协议名是 https,则代表其启用了 HTTPS 协议。
代理
代理(proxy)是 HTTP 协议中请求方和应答方中间的环境,作为中转站,可以转发请求,也可以转发响应。
以下列举代理的种类
-
匿名代理:完全“隐匿”了被代理的机器,外界看到的只是代理服务器;
-
透明代理:顾名思义,它在传输过程中是“透明开放”的,外界既知道代理,也知道客户端;
-
正向代理:靠近客户端,代表客户端向服务器发送请求;
-
反向代理:靠近服务器端,代表服务器响应客户端的请求;
CDN 实际上也是代理的一种,它替代服务器响应客户端的请求,通常扮演透明代理和反向代理的角色。
代理作为传输的中间层,可以做很多事情,例如:
-
负载均衡:把访问请求均匀分散到多台机器,实现访问集群化;
-
内容缓存:暂存上下行的数据,减轻后端的压力;
-
安全防护:隐匿 IP, 使用 WAF 等工具抵御网络攻击,保护被代理的机器;
-
数据处理:提供压缩、加密等额外的功能。
关于HTTP 代理还有一个特殊的代理协议(proxy protocol)。
小结
TCP/IP是世界上最常用的协议,具有可靠、完整的特点。HTTP 运行在 TCP/IP 之上。
DNS 域名是 IP 地址的等价替代,需要用域名解析实现到 IP 地址的映射
URI 由协议、主机名、路径构成。
HTTPS 由 HTTP+SSL/TLS+TCP/IP 组成。
代理是 HTTP 传输的中转站,可以实现缓存加速、负载均衡等功能。
TCP/IP网络分层模型和 OSI 网络分层模型
TCP/IP网络分层模型
TCP/IP 采用分而治之的思想,将复杂的网络通信分成了四层
最下面的为第一层,往上递增。
第一层:链接层(link layer)
这一层主要负责在以太网、wifi 等这样的底层网络中发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标记网络设备,所以也叫 MAC 层。
MAC 地址也称局域网地址,可以唯一标识一个网卡,也标识网卡所属的设备。
第二层:网络互连层(internet layer) IP 协议就在这一层,因为 IP 协议定义了 IP 地址的概念,所以在链接层的基础上,用 IP 地址替代 MAC 地址,在由局域网、广域网等组成的虚拟网络中找设备时,把 IP 地址翻译成 MAC 地址就可以了。
第三层:传输层(transport layer) 这一层的职责是保证数据在两点之间可靠地传输,这一层是 TCP 所在层,同时还有一个 UDP 协议。
TCP 是可靠的、有状态的、有序的协议,必须在双方建立连接才可以发送数据,而且需要保证数据不丢失和重复。所以就诞生了那个经典的面试题:三次握手和四次挥手。
UDP 则比较简单,它没有状态也不需要事先建立连接就可以随意发送数据,但是并不保证数据一定能发送给对方。
TCP的数据是有先后顺序的,而 UDP 则是顺序发,乱序收。
第四层:应用层(application layer) 这一层就是 HTTP 协议所在的层,这里有各种面向应用的协议,除了 HTTP 外,还有负责远程终端协议的Telnet、SSH 加密等。
MAC 层传输单位是帧,IP 传输单位是包,TCP 传输单位是段,HTTP 传输单位是消息报文(message)。统称为数据包
OSI 网络分层模型
OSI 全称“开放式系统互联通信参考模型”
在 TCP/IP协议诞生的年代,还有很多其他协议,这时候国际标准化组织就来了一个统一规范,于是设计了新的网络分层模型OSI。
国际标准化组织组织的简称是 ISO,倒过来念就是 OSI。
OSI 模型共七层,部分层次类似 TCP/IP,从下至上分别是
- 第一层:物理层,网络的物理形式,例如电缆、光纤、网卡、集线器等等;
- 第二层:数据链路层,它基本相当于 TCP/IP 的链接层;
- 第三层:网络层,相当于 TCP/IP 里的网际层;
- 第四层:传输层,相当于 TCP/IP 里的传输层;
- 第五层:会话层,维护网络中的连接状态,即保持会话和同步;
- 第六层:表示层,把数据转换为合适、可理解的语法和语义;
- 第七层:应用层,面向具体的应用传输数据
从特点来看,OSI 比 TCP/IP 更加完整,在补充了TCP/IP协议的细节内容后,还特地加上物理设备的层级。
映射关系
由于OSI 在设计初期就参考了 TCP/IP 的分层结构,所以比较容易但不精确地表示两者的对应关系。以下图表示
由于 OSI 的七层模型分的太细,而 TCP/IP 实际应用时的会话管理、编码转换、压缩等和具体应用经常联系的很紧密,很难分开。所以就导致一个结果:
OSI 是标准协议,但是实际应用依然可以用TCP/IP协议,这样一比较,第五层会话层和第六层表示层就消失了。
四层负载均衡和七层负载均衡
现在我们已经知道了xx层的概念,那么我们就可以知道,四层指的是传输层,七层指的是应用层。
四层负载均衡指的是在传输层上,通过对 IP 地址、端口号等实现对后端服务器的负载均衡。
七层负载均衡指的是在应用层上,比如通过 HTTP 的 URL、主机名、资源类型等数据通过适当的策略转发给后端服务器。
TCP/IP是怎么工作的
当发送数据的时候:
1.首先HTTP 协议会对我们需要传输的内容进行包装---添加 HTTP专用附加连接。
这就好比我们需要发快递,那么需要拿个塑料袋自己包一下。
2.在经过 TCP 层时,TCP 层给数据再次打包,并加上 TCP 头
这就好比快递员拿到我们的快递,那么会压缩一下,继续包装。
3.按照上面的套路,分别给 TCP 数据加上 IP 头和 MAC 头
这就好比快递员把快递放进三轮车,最后放进大卡车。
当对方接收到数据的时候:
1.数据通往对方的 MAC 层和 IP 层,对方分别对数据进行拆包
快递员把快递卸下大卡车,装上三轮车通往对方家
2.去掉 TCP 头和 HTTP 头,获取到数据
对方拆掉快递包装和你的塑料袋,拿到里面的东西。
HTTP协议的传输过程就是这样通过协议栈逐层向上,每一层都添加上本层专用的数据,然后打包,最后发送出去。用一个成语形容就是层层包围
而接收数据刚好相反,从下往上穿过协议栈,每一层都去掉本层的专有头,最后拿到数据。用一个成语形容就是抽丝剥茧
可以用下图表示
小结
TCP/IP 有四层,核心的是第二层 IP 和第三层 TCP,HTTP 在最上层。
OSI 有七层模型,其中数据链路层就是 TCP 的链接层,网络层就是 TCP 的网络互连层,传输层就是 TCP 的传输层,第七层应用层对应 TCP 的第四层。
OSI 的第一层TCP没有、第五层跟第六层都在 TCP 的应用层中,对比之下,这几层都消失了。
HTTP 利用 TCP/IP协议栈逐层打包,再逐层拆包实现数据传输。
一般来说,写应用程序的层为应用层,操作系统处理的是四层或者以下。
域名系统
我们现在已经知道 DNS 跟 IP 之间的关键,而 DNS 服务器将域名转化为 IP 地址的过程就叫域名解析。
目前全世界有几亿个站点,每天发生的 HTTP 流量更是天文数字,而这些请求大多数都是采用域名访问的,所以 DNS 成为了互联网重要的基础设施,必须要保证域名解析的准确、高效。
DNS 的核心系统是一个三层的树状、分布式服务,基本对应域名的结构:
1.根域名服务器(Root DNS Server):管理顶级域名服务器,返回“com”“net”“cn”等顶级域名服务器的 IP 地址
2.顶级域名服务器(Top-level DNS Server):管理各自域名下的权威域名服务器,比如com 顶级域名服务器可以返回 apple.com 域名服务器的 IP 地址;
3.权威域名服务器(Authoritative DNS Server):管理自己域名下主机的 IP 地址,比如apple.com 权威域名服务器可以返回 www.apple.com的 IP 地址
这里根域名服务器是关键,目前全世界共有 13 组根域名服务器,又有数百台的镜像,保证一定能够被访问到。
有了这个系统以后,任何一个域名都可以在这个树形结构里从顶至下进行查询,就好像是把域名从右到左顺序走了一遍,最终就获得了域名对应的 IP 地址。
例如,你要访问www.apple.com,就要进行下面的三次查询:
- 访问根域名服务器,它会告诉你“com”顶级域名服务器的地址;
- 访问“com”顶级域名服务器,它再告诉你“apple.com”域名服务器的地址;
- 最后访问“apple.com”域名服务器,就得到了
www.apple.com的地址。
虽然核心的 DNS 系统遍布全球,服务能力很强也很稳定,但如果全世界的网民都往这个系统里挤,即使不挤瘫痪了,访问速度也会很慢
所以在核心 DNS 系统之外,还有两种手段用来减轻域名解析的压力,并且能够更快地获取结果,基本思路就是“缓存”。
首先,许多大公司、网络运行商都会建立自己的 DNS 服务器,作为用户 DNS 查询的代理,代替用户访问核心 DNS 系统。
这些“野生”服务器被称为“非权威域名服务器”,可以缓存之前的查询结果,如果已经有了记录,就无需再向根服务器发起查询,直接返回对应的 IP 地址。
其次,操作系统里也会对 DNS 解析结果做缓存,如果你之前访问过www.apple.com,那么下一次在浏览器里再输入这个网址的时候就不会再跑到DNS 那里去问了,直接在操作系统里就可以拿到 IP 地址。
另外,操作系统里还有一个特殊的“主机映射”文件,通常是一个可编辑的文本,在 Linux里是/etc/hosts,在 Windows 里是C:\WINDOWS\system32\drivers\etc\hosts,如果操作系统在缓存里找不到 DNS记录,就会找这个文件。
小结
域名使用字符串来代替 IP 地址,方便用户记忆,本质上一个名字空间系统;
域名解析可以将域名转化成 IP 地址。DNS 就像是我们现实世界里的电话本、查号台,统管着互联网世界里的所有网站,是一个“超级大管家“。
全世界有13台重要的根域名服务器保存顶级域名的ip 地址,域名会从右往前查找 ip 地址,对应域名服务器就是根域名服务器==>顶级域名服务器(com查apple.com)==>权威域名服务器(apple.com查 www.apple.com)
DNS 是一个树状的分布式查询系统,但为了提高查询效率,外围有多级的缓存
常见的两种缓存方法分别是
- 由网络商、电信商提供非权威域名服务器,缓存 IP 记录而无需经过根域名服务器。
- 操作系统本身会缓存 IP地址,其次本地还有 host 文件可以缓存IP 地址。
HTTP 报文长什么样
报文结构
TCP/UDP 的报文在传输的时候,都会加上头部数据,这个在上面我们已经讲过了。HTTP 协议也一样,会包装上一层数据,由于 HTTP 是超文本协议,那么实际上传输的很多东西并不需要经过解析,也可以肉眼看懂。
HTTP 请求报文和响应报文的结构基本相同,由三大部分组成
1.请求行(start line):描述请求或者响应的基本信息。
2.请求头(header):使用键值对的形式来表示表文
3.消息体(entity):实际传输的数据,有可能是纯文本,也可能是图片、视频等二进制数据。
其中请求行和请求头经常被合称请求头或者响应头。消息正文被称为“body”。
HTTP 协议规定报文必须有 header,但是可以没有 body,而且在 header 之后必须加一个空行。
在浏览器中他是这样的
这是一个请求报文:
它的第一行,是请求行,表示 GET 方式,基于 HTTP1.1版本
第二行是请求头,描述主机名、链接状态、接收方式等。
最后一行为空白行。后面没有 body
在发送 GET 请求的时候,允许没有 BODY。
虽然 HTTP 协议没有明确对 header 的大小做出限制,但是每个浏览器、Web 服务器都限制请求头以免太大可能影响运行效率。
请求行
请求行由三部分构成:
1.请求方法:是一个动词,如 GET/POST,表示对资源操作。
2.请求目标:通常是一个 URI,标记了请求方法要操作的资源。
3.版本号:表示采用的 HTTP 协议版本
这三个部分通常由空格分割,以换行CRLF结束。
在浏览器中是这样的
GET / HTTP/1.1
GET 就是请求方发。紧接着的/是目标的根目录,意思是我要访问默认资源。最后是 HTTP 版本。
状态行
状态行是在响应报文里面运行的,你也可以叫响应行,但是它也有个标准名字--状态行。意思是服务器响应的状态。
状态行分成三个部分:
1.版本号:表示报文使用的 HTTP 版本
2.状态码:一个三位数,用数字的形式表示处理的结果,比如200是成功,500是服务器错误。
3.原因:作为数字状态的补充,是更加详细的解释文字,比如帮助别人理解原因。
在浏览器中,它可能是这样的
HTTP/1.1 200 OK
意思是基于 HTTP1.1版本的请求,状态码是200,一切 ok。
还有可能是这样的
HTTP/1.1 404 Not Found
意思是404状态码,没有找到对应的资源。
头部字段
请求行或状态行加上头部字段的集合就构成了 HTTP 完整的请求头或者响应头
请求头和响应头的结构是一样的,唯一的区别是起始行。
头部字段分为 key-value 的形式,之间使用:分割,最后以换行结束。比如“Host: 127.0.0.1”这一行里 key 就是“Host”,value就是“127.0.0.1”。
HTTP 字段非常灵活,不仅可以采用诸如 Host的标准字段,还可以使用任意的字段。所以扩展性非常强。
不过也需要注意一些细节:
-
字段名不区分大小写,例如“Host”也可以写成“host”,但首字母大写的可读性更好;
-
字段名里不允许出现空格,可以使用连字符“-”,但不能使用下划线“_”。例如,“test-name”是合法的字段名,而“test name”“test_name”是不正确的字段名;
-
字段名后面必须紧接着“:”,不能有空格,而“:”后的字段值前可以有多个空格;
-
字段的顺序是没有意义的,可以任意排列不影响语义;
-
字段原则上不能重复,除非这个字段本身的语义允许,例如 Set-Cookie。
常用头字段
HTTP 协议有非常多的头字段,可以实现各种功能,总体分为四大类:
1.通用字段:请求头或者响应头都可以出现。
2.请求字段:仅能够出现在请求头里,进一步说明请求信息或者额外条件。
3.响应字段:仅出现在响应头里,补充说明响应报文信息。
4.实体字段:也属于通用字段,描述 body 的额外信息。
下面来说几个常用字段
-
Host 字段:HTTP1.1中规定的必须出现的字段,表示告诉服务器这个请求应该让哪个主机处理。
-
User-Agent字段:请求字段,只在请求头中出现,表示发起请求的客户端。
-
Date 字段:通用字段,通常出现在响应头中,表示 HTTP 创建的时间。
-
Server 字段:响应字段,只出现在响应头中,它告诉客户端当前正则提供 Web 服务的软件名称和版本号。这个字段会把服务器的一部分信息暴露给外界,所以有可能引起黑客攻击,所以允许没有,或者给一段完全无关的描述信息。
在 github中,这个字段就没有告诉我们使用Apache 还是 Nginx。
- Content-Length字段:表示 Body 的长度,这是实体字段。告诉服务器有多少数据需要接收。
小结
HTTP 报文由起始行+头部+空行+实体组成,简单来说就是 header+body
HTTP 报文可以没有 body,但是必须有header 和空行。
请求头由请求行+头部字段组成,响应头由状态行+头部字段组成
请求行由请求方法、请求目标、版本号组成
状态行由状态码,附加信息,版本号组成
头部字段采用 key:value 形式,不拘束大小写,不存在顺序问题,还可以自定义字段,实现随意扩展的功能。
HTTP1.1中,必须出现 HOST 字段,它必须出现在请求头中,表示对方的主机名
请求方法
在 Request Header 中,有请求方法和请求的目标,目前 HTTP1.1一共有8种请求方法:
1.GET:获取资源,可以理解为读取或者下载数据
2.HEAD:获取资源的元信息
3.POST:提交数据,相当于写入数据
4.PUT:修改数据等
5.DELETE:删除 资源
6.CONNECT:建立特殊的连接隧道
7.OPTIONS:列出对资源实行的方法
8.TRACE:追踪请求-相应的传输路径
这些方法基本都是大写。
GET/HEAD
GET 是 HTTP 中最古老的请求方法,也是用得最多的方法。
它的语义是从服务器中获取资源,这个资源可以是静态的文本、页面、视频等等,一般来说,GET 请求如果有参数,那么它的参数需要增加查询字符串在 URI 中。
HEAD 跟 GET 请求方法类似,也是从服务器获取资源,但是服务器不会返回请求的实体数据,而是传回响应头,也就是资源的元信息。
HEAD 方法可以看成是 GET 方法的简化版或者说是轻量版。它可以用于不需要用到资源的场合,避免传输数据的浪费。
比如说,检查一个文件是否存在,就只要发 HEAD 请求就可以了。
POST/PUT
这两个方法非常相似,都是指定向服务器发送数据,数据一般放到 body 里面。
例如,我们向服务器发送加入购物车请求,那么你喜欢的商品就会作为 body 中的数据发送给服务器。
PUT 也是一样,向服务器提交数据,但是从语义上来看,PUT 更像是 update,而 POST 更像是 create。
非常用方法
DELETE:方法指示服务器删除资源,因为这个动作危险性太大,所以通常服务器不会执行真正的删除操作,而是对资源做一个删除标记。
CONNECT是一个比较特殊的方法,要求服务器为客户端和另一台远程服务器建立一条特殊的连接隧道,这时 Web 服务器在中间充当了代理的角色。
OPTIONS方法要求服务器列出可对资源实行的操作方法,在响应头的 Allow 字段里返回。它的功能很有限,用处也不大,有的服务器(例如 Nginx)干脆就没有实现对它的支 持。
TRACE方法多用于对 HTTP 链路的测试或诊断,可以显示出请求 - 响应的传输路径。它的本意是好的,但存在漏洞,会泄漏网站的信息,所以 Web 服务器通常也是禁止使用。
安全与幂等
在实际面试中,有两个重要的概念,安全和幂等。
所以安全,就是不会对服务器资源造成修改的风险,这里只有 GET 请求和 HEAD 请求是安全的,因为它们都是只读操作。
所谓幂等是一种数学概念,意思就是不管操作多少次,结果都是相同的。也就是幂次数结果相等。
很显然,GET 和 HEAD 是幂等的,而 POST 是不幂等的,原因是它的语义就代表着创建,既然创建了,当然会新增结果。PUT 比较特殊,虽然它也会修改数据,但是它的语义是修改而非新增或减少,不管做多少次,它都只更新一个资源,所以它也是幂等的。
我们可以认为这里的结果指的是数据的数量。多次 post 会增加数据量,而多次修改不会增加数据量,这就是幂等跟非幂等的通俗含义。
小结
请求是客户端发出的,要求服务器执行的、对资源的一种操作。
请求只是指示,具体要怎么做,需要跟服务端协商。
常用的请求方法是 GET 和 POST。分别是获取数据和发送数据。
HEAD 是轻量级 GET,每次只获取元信息,也就是响应头。
PUT 基本上与POST 相同,多用于更新数据,PUT 是幂等的,POST 是非幂等的。
安全和幂等是描述请求方法的两个重要属性。
URI
请求方法是通过指令的方式向服务器发送指示来指导服务器完成某个动作,常用的就是获取资源,那么怎么区分资源呢?
答案是用的是 URI 获取,也就是统一资源标识符。由于它经常出现在浏览器的地址栏,所以俗称为网址。
严格来说,URI 并不完全等同于网址,它包含 URl和 URN 两个部分,在 HTTP 中用的是 URL---统一资源定位符,由于 URL 实在太普及,所以经常把 URI 跟 URL 划上等号。
URI 非常重要,要搞懂 HTTP,就必须搞懂 URI。
URI 的格式
URI 本质上是一个字符串,这个字符串的作用是唯一地标记资源的位置或者名字,它不仅可以标记万维网的资源,也可以标记邮件系统、本地文件系统等任意资源,而资源既可以是静态文本、页面数据,也可以是Java 提供的动态服务。
URI 常用形式由 scheme、host:post、path 和 query 四个部分组成。
scheme
URI 的第一个组成部分叫 scheme,也叫协议名,它表示应该使用哪种协议来访问资源。
最常见的是 HTTP 协议,另外还有经过安全加密的 HTTPS 协议等等。
在浏览器中,如果浏览器看到地址栏上有 scheme,就会调用相应的下层 API来处理 URI。
在 scheme 的后面,固定有三个字符:‘://’,它把 scheme 和后面的部分分离开。
authority
在“://”之后,是被称为“authority”的部分,表示资源所在的主机名,通常的形式是“host:port”,即主机名加端口号。
主机可以是 IP 地址或者域名,必须要有,否则浏览器会找不到服务器。端口号有时候可以忽略,浏览器会自动根据 scheme 使用默认的端口号,常见的有 HTTP 默认端口号80,HTTPS 默认端口号是443。
path
有了 authority,后面的 path 部分标记着资源在哪个path 下,有了 scheme、主机名、端口号和 path,那么服务区就可以访问资源了。
URI 里面的 path 采用了类似文件系统的目录路径,早期的 UNIX 系统的文件目录就是采用/做分割的。
path部分必须以/开始。
实例
下面分析几个实例
http://nginx.org
http://www.chrono.com:8080/11-1
https://tools.ietf.org/html/rfc7230
file:///D:/http_study/www/
第一个 URI 比较简单,协议是 http,主机名是 nginx.org,端口号是默认的80.路径被忽略了,默认为/。也就是根目录的意思。
第二个 URI 有完整的路径和端口号。
第三个 URI 则是 HTTPS 协议,端口号为默认的443,路径为/html/rfc7230
第四个 URI 则是 file 协议,表示本地文件,后面三个斜杠的含义是
://表示分割协议和 authority的部分,剩下的/表示的是路径开头,也就是根目录下的 D 盘的/http_study/www/路径。由于 file 是 URI 的特例,它允许忽略主机名,默认为localhost。
在浏览器上看到的 URI 和服务器上看到的是不一样的,例如我的掘金主页,打开F12开发者工具,点击 view source 后可以看到这样的原始请求头
服务器看到的 URI是/v1/list,对应我请求时的路径,这是因为协议名和主机名都已经出现在请求行和 Host 字段中,所以服务器只需要取删除了协议名和主机名的 URI 即可。
查询参数
使用协议名+主机名+路径的方式已经可以精确定位到网络上的资源了,但我们还想附加一些额外的修饰参数来做特定的场景,比如想要获取商品列表,根据某种规则来做分页和排序,查询参数就用上派场了。
URI 后面还有一个 query 部分,它采用?开始,表示附加的要求。
查询参数 query 有一套自己的格式,往往是多个 key=value 的字符串,这个键值对不是用 Javascript 的:连接,而是用&做连接的,浏览器和客户端都按照这个格式把长串的查询参数解析成可以理解的数据结构。
https://www.youtube.com/watch?v=FUN5rfoqLLA&t=116s
上面网址中的?v=FUN5rfoqLLA&t=116s就是查询参数 query。
URI 编码
在 URI 中只能使用 ASCII编码,如果要用到英语之外的语言,比如汉语、日语等,还有某些特殊的 URI 会在 path、query 上出现“@&?”这些字符,而这些字符在 URI 上有特殊用途,要如何区分呢?
这就要说到 URI 的编码规则了,URI引入了编码机制,对除了 ASCII编码外的字符集或者特殊字符做了特殊操作,俗称转义。
URI 的转义规则有点简单粗暴,直接把非 ASCII 码或者特殊字符转化成十六进制字节值,再在前面加上%。
比如银河会被转义成%E9%93%B6%E6%B2%B3”
encodeURI('银河')
//"%E9%93%B6%E6%B2%B3"
decodeURI('%E9%93%B6%E6%B2%B3')
//"银河"
encodeURIComponent('你好')
//"%E4%BD%A0%E5%A5%BD"
decodeURIComponent("%E4%BD%A0%E5%A5%BD")
//"你好"
上面两个JavaScript函数可以对 URI进行编码和解码。
fragment
fragment 是片段标识符,也就是速成的锚点。浏览器获取资源后根据这个锚点来直接跳转到它指示的位置,不过这个片段标识符只对浏览器有用,因为它不会像查询参数一样发送给服务器处理。
https://github.com/sudheerj/reactjs-interview-questions#table-of-contents
这里的#table-of-contents就是锚点。
小结
URI 是用来唯一标记服务器上资源的字符串,通常也叫 URL。
URI 通常由 scheme、host:post、path 和 query 四个部分组成。
URI 会对@&?这些有特定用法的特殊字符以及汉字等非 ASCII 码的字符进行编码转义。
HTTP状态码
响应报文由响应头和响应体数据组成,响应头由状态行和头部字段组成。
以下是状态行的结构
在状态行中,协议号版本跟 原因短语(Reason)的作用不是很大,最重要的是状态码。它是一个十进制的数字,以代码的形式表示服务器的处理结果。它的意义在于表达 HTTP 数据处理的状态,客户端可以根据代码适时转换处理状态。
目前 RFC 标准规定状态码为3位数,按照100-599的范围一共分为五类,类别以百分号为标识:
1xx:提示信息,表示目前是协议处理的中间状态,需要后续操作
2xx:成功,报文已经收到并被正确处理
3xx:重定向,资源位置发生变化,需要客户端重新发送请求
4xx:客户端错误,请求报文有误,服务器没办法处理
5xx:服务器错误,服务器在处理请求时内部发生错误
在HTTP中,正确地理解并应用这些状态码不是客户端或者服务器单方的责任,是双方共同的责任。
客户端作为请求的发起方,获取响应报文后,需要通过状态码知道请求是否被正确处理,是否要再次发送请求,如果出错了原因是什么。
服务端作为请求的接收方,也要很好地运用状态码,在处理请求时,选择最恰当的状态码回复客户端,告知客户端处理的结果,指示客户端下一步行动,特别是出错时,尽量不要简单回复400、500这样含糊不清的状态码。
目前 RFC 有41个状态码,但是状态码的定义是开放的,允许自动扩展,下面是常用状态码的介绍。
1xx
1xx类状态码属于提示信息,是协议处理的中间状态,实际运用非常少,偶尔我们会遇到‘101 Switching Protocols’,它的意思是要求客户端在 HTTP 协议的基础上使用其他协议继续通信。
2xx
2xx 表示服务器成功收到并处理了客户端的请求。
200 OK:表示一切正常,服务器返回了处理结果。
204 No Content:表示状态成功,但是响应没有 body 数据。
3xx
3xx 类状态码表示客户端请求的资源发生变动,客户端必须用新的 URI 重新发送请求获取资源,也就是说的重定向。
-
301 Moved Permanently:永久重定向,意思是此次请求的资源已经不存在了,需要改用新的 URI 访问。
-
302 Found:临时重定向,意思是请求的资源还在,但是暂时需要另一个 URI 访问。
301和302都会在响应头后使用字段 Location 指明需要跳转的 URI,最终效果类似,但是语义差别很大。
比如 HTTP 升级成 HTTPS,原来的 HTTP 不打算继续用了,这时候就需要用永久重定向301跳转。
有时候服务器升级,暂时服务不可用,这时候可以配置成302临时重定向,浏览器看到302就知道是暂时的情况,不会做缓存优化,第二天还会访问原来的地址。
- 304 Not Modifield:这个状态码用于缓存控制,它不具备跳转含义,可以理解成重定向到已缓存的文件。在HTTP对比缓存中,如果服务器没有修改资源,那么浏览器就会访问已缓存的文件,并返回这个状态码。
4xx
4xx 类状态码表示客户端发送的请求报文有误,服务器无法处理,它就是真正的错误码含义了。
-
400 Bad Request:通用错误码,表示请求有误,但是哪里有误没有明确说,只是笼统的错误,一般来说最好使用其他更有明确含义的状态码
-
403 Forvidden:表示不是客户端请求出错,而是服务器禁止访问资源,有可能是请求没有权限,或者没有登录等等原因。
-
404 Not Found:表示需要的资源服务器上没找到,只是现在这个状态码被服务端滥用了,只要服务器“不高兴”就直接返回404。
-
414 Request-URI Too Long:请求的 URL 过长。
5xx
5xx 类状态码表示客户端请求正确,但服务器处理时内部发生错误,无法返回响应数据,是服务器的错误码。
-
500 Internal Server Error:通用错误码,表示服务器有问题,不过是什么问题就不明说了。实际上这样的处理对于服务器是好事,因为它能够防止黑客的窥探或者分析。
-
501 Not Implemented:表示客户端请求的功能现在还不支持,敬请期待。
-
502 Bad Geteway:通常是服务器作为网关或者代理时返回的错误码,表示服务器自身正常,访问后端服务器时发生了错误。
-
503 Service Unavailable:表示服务器很忙,暂时无法响应服务。503是临时的状态,很可能过一段时间就不忙了,所以503响应报文中通常还会有
Retry-After的字段,表示过多久再来试试可能就好了。
小结
状态行在响应报文中表示服务器对请求的处理结果
状态码后的原因短语是简单的文字描述,可自定义
状态码是十进制的三位数,从100-599分为五类
1xx:不常用,表示还需要后续操作
2xx:请求并处理成功,常用200、204
3xx:请求重定向,常用301、302、304
4xx:客户端错误,常用400、403、404、414
5xx:服务器错误,常用500、501、502、503
HTTP 的特点
基础的 HTTP 知识已经过完了,下面就基础内容对 HTTP 做一个特点总结
灵活可扩展
HTTP 是一个灵活可扩展的传输协议。
最初诞生时,HTTP 协议就本着开放的态度只规定了报文的基本格式,也就是请求行、空格、换行符、header+body 等,报文的各个组成部分并没有做严格规定,可以随开发者定制。
随着互联网的增长,HTTP 协议逐渐增加方法、版本号、状态码、头字段等。而 body 也不再局限于文本形式的 TXT 或者 HTML,增加了图片、音频视频等任意数据,也来源于其灵活可拓展的特点。
而 RFC 文档,更多的可以理解为对已有扩展的承认和标准化,实现了从实际中来,到实践中去的良性循环。
可靠传输
HTTP 协议是一个可靠的传输协议。
这是由于 HTTP 是基于TCP/IP 协议的,而 TCP 协议本身就是可靠的、完整的协议,所以 HTTP 继承了这个特性,能够实现在请求方和应答方进行可靠地传输。
它的具体做法跟 TCP/IP 差不多,都是对实际传输数据做一层包装,加上一个头,然后通过 TCP/IP 协议栈发送或者接收。
我们必须正确理解可靠的含义,可靠的传输意思是在正常的网络环境下,信息的收发必定成功。
HTTP 不能保证100%地传输能够从一方发送到另一方,可靠只是向使用者提供一个“承诺”,会在下层“尽量”保证数据的完整送达。
如果遇到非常恶劣的网络环境,比如连接环境差、网络繁忙等,也是有可能传输失败的。
应用层协议
HTTP 是应用层的协议。
在 TCP/IP 诞生后的十多年,出现了非常多的应用层协议,比如 负责远程登录的 SSH 协议,负责文件传输的 FTP 等等,但是他们都知关注于非常小的应用领域,在通用数据传输领域完全不能打。
HTTP 凭借可携带任意头字段和实体数据的报文结构,以及连接控制、缓存代理等特性,只要不太苛求性能,HTTP 几乎可以传递一切东西,满足各种需求,称得上万能协议。
请求-应答
HTTP 使用的是请求-应答的通信模式。
通俗来说,这个模式就是一发一收,有来有回,就像是函数调用,只要填写好头部字段,调用后就会收到答复。
请求-应答模式同时也明确了 HTTP 协议中双方的定位,永远是请求方先发送请求,是主动的,而应答方只能在收到请求后才能够回复,是被动的,如果没有请求就不会有动作。
在浏览器-服务器中往往浏览器是请求方,服务器是应答方。但如果服务器作为中间代理连接后端服务器,那么它就有可能同时扮演请求-应答的角色。
传统的C/S 系统架构是 Client/Server,也就是客户端/服务器,HTTP 协议的模式下请求方为客户端,应答方是服务器,这种应答模式非常契合传统架构。
随着互联网的发展,也出现了 B/S(Browser/Server)架构,用轻量级的浏览器作为客户端应用,实现客户端瘦身,而服务器则摒弃私有协议而采用通用的 HTTP 协议。
此外,请求-应答模式也完全符合 RPC(Remote Procedure Call) 的工作模式,可以把 HTTP请求封装成远程函数调用,导致了 WebService 等出现。
无状态
HTTP 协议是无状态的。
什么是无状态呢?状态其实就是客户端或者服务器保存的一些数据或者标志,记录通信过程中的变化信息。
作为对比,我们来看一下 TCP 协议,它就是一个有状态的协议,一开始它处于 CLOSED 状态,连接成功后是 ESTABLISHED 状态,断开连接是 FIN-WAIT 状态,最后又是 CLOSED 状态。
这些状态需要 TCP 内部有一些数据结构去维护。简单来说,就是一个标志量,标志着当前所处的状态。例如0代表 CLOSED,2是 ESTABLISHED 等等。
无状态可以形象地称为没有记忆能力,比如,浏览器发送了一个请求,并且附带上自己的身份令牌,比如 Token,服务器就会检查一下权限,然后发送数据回去。过了一会,浏览器再发送一个请求,由于服务器并不记录请求状态,所以也不知道是同一个浏览器请求的,依然要重新验证一下权限,这就是无状态。
在 HTTP 协议中,没有规定的任何状态,客户端和服务器永远都处于无知的状态。连接前两者并不知情,收发报文也是独立的,没有任何的联系,收发报文也不会对服务器或者浏览器造成影响,连接后也不会保存任何信息。
UDP 协议也是一种无状态的协议,但是UDP同时也是无连接的,顺序发包乱序收包,发送出去后就不管了。而 HTTP 则是有连接的,顺序发送,顺序收包,按照收发顺序管理报文。
不过由于 HTTP也是灵活可扩展的协议,虽然标准中没有规定状态,但是完全可以在协议的框架下打个补丁,增加这个特性。
其他特点
HTTP 传输的实体数据可缓存可压缩,可分段获取、支持身份认证、国际化语言等等都是 HTTP 的特点。
小结
HTTP 是灵活的可拓展的,可以添加任意头部字段实现各种功能
HTTP 是可靠的传输协议,基于 TCP/IP 尽量保证数据的完整送达
HTTP 是万能的应用层协议,比FTP、SSH 实现更多功能,可以传送任意数据
HTTP 使用了请求-应答模式,客户端主动发送请求,服务器被动回复请求
HTTP 本质上是无状态的,所谓无状态,通俗来讲就是没有记忆能力,协议不要求记录连接信息,每个请求之间、浏览器和服务器之间都是相互独立,毫无关联的。
Cookie 机制
HTTP 是无状态的,也就是没有记忆能力,好在 HTTP 可以扩展,有了扩展,也就有了记忆能力,这里要说到Cookie 技术
Cookie 是什么
Cookie相当于服务器给浏览器的小纸条,上面写了只有服务器才可以理解的数据,需要客户端把这个信息发送给服务器,当服务器看到这张小纸条,就能认出发送请求的客户端是谁。
Cookie 的传输过程
这里需要用到两个HTTP 头部字段:响应头Set-Cookie 和 请求头Cookie。
1.当用户第一次访问服务器的时候,服务器肯定不知道它的身份,于是服务器首先要创建一个 key=value 格式的身份标识数据,这就需要用到 Set-Cookie,然后通过这个响应头部字段,将键值对的数据发送给浏览器。
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: theme=light
Set-Cookie: sessionToken=abc123; Expires=Wed, 09 Jun 2020 10:18:14 GMT
...
2.当浏览器收到响应报文后,发现响应报文中有 Set-Cookie 的字段,就将其保存下来,下次请求时自动将这个值放进 Cookie 头字段中发送给服务器。
GET /spec.html HTTP/1.1
Host: www.example.org
Cookie: theme=light; sessionToken=abc123
…
3.当服务器看到请求字段中含有 Cookie 字段时,就会认出这是之前来过的浏览器,识别出身份后,可以提供个性化服务。
之前我们说过,大多数 HTTP 头字段是不可以重复的,但是 Set-Cookie 除外,它可以一次性设置多个。这样就可以存储多个 key=value 格式的数据,浏览器发送时,只用一个 Cookie 字段,但是会用;隔开。
由于 Cookie 是浏览器保存的,所以换了浏览器还得重新走一遍 Cookie 的流程。
Cookie 的属性
Cookie是服务器委托浏览器存储在客户端的一些数据,而这些数据会识别用户的关键信息,所以就需要在 key=value以外再加上一些其他属性来保护。
生命周期
Cookie 的生命周期俗称为有效期,这里可以用到两个属性:Max-Age 和 Expires。
Max-Age 是相对时间,翻译过来是最大存在时间,它的单位是秒,也就是服务器收到 Cookie 并返回后,浏览器收到响应报文的时间再加上Max-Age 时间就是 Cookie 的有效时间。
Expires 是绝对时间,翻译就是过期时限,可以理解为截止日期。
它俩的写法是这样的
Set-Cookie:Max-Age=100;Expires=Fri,07-Jun-19 08:19:00 GMT;
意思是过期时间100秒,截止时间2019年6月7日8点19分,星期五。
两者可以同时存在,浏览器优先选择 Max-Age
作用域
作用域就是让浏览器不要随便发 Cookie,要发给特定的服务器和 URI。
Set-Cookie:Domain=www.baidu.com;path=/;
这个设置就比较简单,浏览器在带 Cookie 的时候,会去比较域名Domain和路径path 部分。如果不满足条件,就不会在请求头中发送Cookie。
使用这两个属性可以分别在不同的路径发送不同的Cookie,比如/index和/users路径分别发不同的Cookie,不过一般为了省事,会在 path 部分用一个/表示根目录下都发Cookie。
安全性
在 JavaScript 中,有一个读取 Cookie 的方法document.cookie,这可能造成安全隐患,造成 XSS 攻击(跨站脚本攻击)。
这时候可以使用 HttpOnly属性告诉浏览器,不允许使用 Javascript 访问。
还有一个属性,可以防止XSRF 攻击(跨站请求伪造),它就是SameSite=Strict,它可以严格规定Cookie 不能随着跳转链接跨站发送,SameSite=Lax则宽松一点,允许 GET/HEAD 发送 Cookie,但禁止POST发送。
浏览器Cookie 的查看方法
Cookie本身不是加密的,在浏览器中可以查看得到。
通过浏览器 Network-Cookies 和 Application都可以看到 Cookie。Application 则可以看到全站所有 Cookie。
Cookie 的应用
Cookie的基本作用是身份识别,保存用户的登录信息,实现会话事务。
比如,你用账号和密码登录某电商,登录成功后网站服务器就会发给浏览器一个 Cookie,内容大概是“name=yourid”,这样就成功地把身份标签贴在了你身上。
之后你在网站里随便访问哪件商品的页面,浏览器都会自动把Cookie 发给服务器,所以服务器总会知道你的身份,一方面免去了重复登录的麻烦,另一方面也能够自动记录你的浏览记录和购物下单(在后台数据库或者也用 Cookie),实现了“状态保持”。
Cookie 的另一个常见用途是广告跟踪。
你上网的时候肯定看过很多的广告图片,这些图片背后都是广告商网站(例如 Google),
它会“偷偷地”给你贴上 Cookie 小纸条,这样你上其他的网站,别的广告就能用 Cookie读出你的身份,然后做行为分析,再推给你广告。
小结
Cookie 是服务器委托浏览器存储的一些数据,让服务器有了 “记忆能力”;
响应报文使用 Set-Cookie 字段发送“key=value”形式的 Cookie 值;
请求报文里用 Cookie 字段发送多个 Cookie 值;
为了保护 Cookie,还要给它设置有效期、作用域等属性,常用的有 Max-Age、Expires、Domain、HttpOnly 等;
Cookie 最基本的用途是身份识别,实现有状态的会话事务。
HTTP Body数据的内容协商
数据类型与编码压缩
在 TCP/IP的协议栈中,传输数据基本上都是 Header+body 的格式,在传输过程中加上各自的头,它们并不关心 body 的数据是什么,只要把数据发送出去就可以了。
HTTP 并不是这样的,它是应用层的协议,数据到达之后的工作只完成了一半,它还必须告诉上层应用这是什么数据。
假设 HTTP 没有告知数据类型的功能,服务器把数据发送给浏览器,那么浏览器该怎么办?
它可以靠猜测,很多数据都是有固定格式的,所以通过代码检查数据的前几个字节也许就可以知道这是个 gif 或者是 mp3文件,但这样做无疑是低效的。
在 HTTP 诞生之前就已经有了针对这种问题的解决方法,这就是应用于电子邮件系统的 MIME(多用途互联网邮件扩展),他可以让电子邮件发送除了 ASCII 码外的数据。
HTTP 借鉴了一部分,用来标记 body 的数据类型,这就是我们经常听到的 MIME type
MIME 把数据分成八类,每个大类下继续分成子类,形式是 type/subtype的字符串,这刚好可以纳入 HTTP 头字段中。
常用类别:
-
text:即文本格式的可读数据,我们常见的就是 text/html,表示超文本文档。此外还有text/plain 和 text/css。
-
image:图像文件,常见 image/gif、image/jpeg、image/png 等。
-
audio/video:音频和视频数据,例如 audio/mpeg、video/mp4等。
-
application:数据格式不固定,由上层应用程序解释。常见的有 application/json、application/JavaScript、application/pdf 等。如果实现不知道数据是什么,就会是 application/octet-stream,即不透明的二进制数据。
在 HTTP 传输时,为了节约带宽,有时候还会压缩数据,为了不让浏览器继续猜,还需要一个 Encoding type,告诉数据是用什么编码格式,这样对方才能够正确解压缩,还原出原始的数据。
比起 MIME type ,Encoding type 就少很多,常用的就只有三种:
1.gzip:GNU zip 压缩格式,也是互联网流行的压缩格式。
2.deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip
3.br:专门为 HTTP 优化的新压缩算法。
数据类型使用的头字段
有了 Encoding type 和 MIME type,服务器和浏览器都可以知道body 数据的类型,那么能不能有一种字段,让双方互相协商,传递对方想要的数据呢?
有的。HTTP 定义了两个 Accept 请求头字段和两个 Content 实体头字段,用于客户端和服务器进行“内容协商”。
其中 Accept(接收)是浏览器告诉服务器希望接收什么样的数据,Content(内容)是服务器告诉浏览器传递过来的数据类型。
请求的头字段
POST /api/base/facade/file/file?action=upload HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
响应的头字段
HTTP/1.1 200 OK
content-type: application/json;charset=UTF-8
Content-Encoding: gzip
Accept字段标明客户端能够理解的MIME type,上面*/*的意思是都可以理解。多个格式之间使用,分割。
Accept:text/html,application/json
Content-Type字段标明服务器给明的实体数据的类型。这样可以方便浏览器根据数据类型做出处理,比如是 text/html 就可以将它渲染出来。
Accept-Encoding和Content-Encoding分别是浏览器支持的解压缩格式和服务器的实际的压缩格式。
这个过程可以用收发快递来类比,比如我寄出去一个快递告诉我朋友我希望拿到 xxx 类型的东西(Accept),不过这东西我知道我朋友肯定会打包寄给我,所以我告诉他我支持用什么方式来拆包装(Accept-Encoding),我朋友收到后就返回给我某种类型(Content-Type),然后告诉我用指定的方式去拆包装(Content-Encoding)。
语言类型与编码
MIME type 和 Encoding type 解决了计算机理解 body 数据的问题,那么如何解决各国语言的问题呢?
HTTP 引入了语言类型与字符集。
所谓语言类型就是我们使用的汉语、英语、日语等,而语言可能也还有下属的地方语言,所以也是采用 type-subtype的形式,唯一的区别是语言类型用-做分割。
例如:en 表示任意的英语,en-US 则表示美式英语,en-GB 表示英式英语,我们用的 zh-CN 则是汉语。
字符集是为什么而诞生呢?在计算机早期出现了很多语言环境下的字符集,比如英语下的 ASCII 码,汉语下的 GBK 等,为了做到字符的统一,Unicode 诞生了,后来随着互联网的普及,作为 Unicode的实现之一--utf-8大幅度流行,目前 utf-8也成为互联网上的标准字符集。
语言类型使用的头字段
HTTP 同样支持语言类型的 Accept 和 Content 实体头字段。
浏览器用的头字段为 Accept-Language,允许用,分割。
Accept-Language:zh-CN,zh,en
上面的头字段意思是给我zh-CN 的汉语文字,没有的话就给我随便什么汉语,实在不行就给我英文吧。
字符集在请求短则使用 Accept-Charset。
Accept-Charset:utf-8,gbk
服务器使用的头字段为Content-Language,也就是服务器告诉浏览器我传的什么文字类型。
Content-Language:zh-CN
在服务器返回的字符集则是放在 Content-Type字段的后面,用‘charset=xxx’来表示。
Content-Type:text/html;charset=utf-8
现在的浏览器由于功能太强大,一般来说不会发送 Accept-Charset,因为它支持多种字符集,而服务器也不会发送 Content-Language,而是直接告诉浏览器用什么 charset 就行了。
小结
数据类型表示实体数据的内容是什么,使用 MIME type,相关的头部字段为 Accept 和 Content-Type
数据编码表示实体数据的压缩方式,相关的字段为 Accept-Encoding 和 Content-Encoding
语言类型表示实体数据的语言,相关的字段是 Accept-Language 和 Content-Language
字符集是实体数据的编码方式,相关的字段是 Accept-Charset 和 Content-Type
传输大文件的方法
数据压缩
在上面我们已经讲过 HTTP 传输数据时,有一个字段是 Accept-Encoding,它代表客户端支持的数据压缩的格式,这样服务器就可以选择其中一种,放到 Content-Encoding 中,再把原来的数据压缩后发送给浏览器。
如果压缩率有50%,那么原来100k 的数据就可以压缩成50k 大小,极大地提升传输效率。
这种压缩方式非常适合文本如(text/html)的压缩,不过并不适合传输图片、音频视频等数据,因为它们本身已经高度压缩了。
分块运输
除了数据压缩之外,还可以把大文件整体变小,分解成很多个小块,这样就可以把小块分发给浏览器,浏览器收到后复原。
这种方式有个好处,每次只收发一小部分,网络不会被大文件长时间占用,可以节省带宽资源。
这种方法在 HTTP 中叫 chunked 分块传输,在响应报文里用头字段 Transfer-Encoding:chunked 来表示。意思是报文的 body 可以分成多次发送。
分块传输可以用于流式数据,例如数据库动态生成的表单页面,这种情况下 body 数据的长度是未知的,无法在头字段 Content-Length 里给出确切的长度,所以也只能用 chunked 方式分块发送。
“Transfer-Encoding: chunked”和“Content Length”这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked)。
范围请求
分块传输就是把大文件分成很多个小块,那么假设我希望获取大文件中特定的片段数据,显然没办法用分块传输做到。
HTTP 协议中,还有一种范围请求可(range requests)的概念,允许客户端在请求头里面使用专用字段来表示只获取文件的一部分。
在做范围请求前,需要 Web 服务器在响应头上使用字段 Accept-Ranges:bytes,明确告知客户端支持范围请求。不支持则可以不传这个字段或者设置为 none
请求头 Range 是 http 范围请求的专用字段,格式是 bytes=x-y,其中 x和 y 是以字节为单位的数据范围。
需要注意 x、y 表示的是偏移量,范围从0计算,比如前10个字节表示为0-9.
其中 x 和 y 是可以省略的,0-表示文档起点到终点,-1表示文档最后一个字节,-10表示从文档末尾倒数10个字节。
服务器收到 Range 字段后,需要做四件事
-
检查范围是否合法。不合法可以返回416编码。
-
范围正确,服务器需要根据 Range 头计算偏移量,读取文件的片段,返回
206 Partial Content,表示 body 只是数据的一部分 -
服务器需要加上响应头
Content-Range,告诉片段的实际偏移量和资源的总大小,格式则是bytes x-y/length,和请求头的 Range 字段区别是没有=号且范围后多了总长度。 -
最后发送数据了
范围请求的常见应用是视频的拖拽进度和多段下载、断点续传等。
多段数据
范围请求还支持一次获取多个片段数据,可以在 Range 中使用多个x-y。
这种情况需要一种特殊的 MIME 类型:multipart/byteranges,表示报文的 body 是由多段字节序列组成的,并且还要给一个参数 boundary=xxx给出段之间的分割标记。
小结
压缩 HTML 等文件是传输大文件的基本方法
分块传输可以流式收发数据,节省内存和带宽。使用响应头 Transfer-Encoding:chunked
范围请求可以只获取部分数据。主要应用于断点续传和视频拖拽等。使用请求头字段 Range 和响应头字段 Content-Range。相关响应状态码416和206
也可以一次性请求多个范围,此时响应报文的Content-type 是multipart/byteranges,body 部分多个部分会用 boundary 字符串分隔。
HTTP 的缓存控制
缓存(cache)是优化系统性能的利器,由于链路长,网络延迟不可控,通过 HTTP 获取网络资源的成本比较高,所以,浏览器采用了缓存机制,将拿到的数据缓存起来,下次再请求的时候尽可能复用,这样的好处也很明显:响应速度快、节省网络带宽。
缓存流程
整个浏览器的缓存是这样的:
- 首先浏览器发现发送请求,获取服务器资源
- 服务器响应请求,返回资源,同时标记资源有效期
- 浏览器缓存资源,等待下次重用
强制缓存
服务器标记资源的有效期的头字段是Cache-Control,常用的属性是max-age=xx,意思是最大持续时间多少秒。这个属性在 Cookie 那一节也有类似,只是头字段不一样。
Cookie 的 max-age 是在浏览器收到 Cookie 的时间开始算的,而 Cache-Control的 max-age 则是在服务器发送响应头字段开始计算的。
还有其他的属性例如:
- no_store:不允许缓存
- no_cache:可以 缓存但是使用前必须去服务器验证是否过期,是否有最新的版本
- must-revalidate:意思是缓存不过去就可以继续使用,但是过期了还想用就要去服务器验证。
客户端缓存控制
服务器发送了缓存指令,浏览器就会把缓存数据给保存到本地,等到需要用的时候,就会读取本地缓存中的服务器资源,提高网络效率。
不止浏览器可以发 Cache-Control 头,浏览器也可以发,这说明请求-应答双方都可以用Cache-Control 字段进行缓存控制。
当我们点击刷新按钮的时候,浏览器会在请求头里面加Cache-Control:max-age=0,表示的意思是我要最新的资源。这时候不会读取浏览器的缓存,而是向服务器发送请求。
下图是我在chrome浏览器中点击刷新时发送出去的请求头,自动带上了 Cache-Control:no-cache,跟 max-age效果是一样的。
浏览器的缓存什么时候生效呢?当我们点击前进、后退按钮的时候,会看到 from disk cache 的字样,这就触发了浏览器的缓存。
因为前进、后退等操作,浏览器只会添加最基本的请求头,不会增加Cache-Control之类的字段,所以会触发本地缓存。
协商缓存
使用 Cache-Control 做缓存控制只能刷新数据,不能很好地利用缓存数据,所以这里又有一个协商缓存的概念,也叫条件请求。
它的做法是利用两个连续的请求来组成验证动作,它是这样运作的:
第一步:先发一个 HEAD 请求,获取资源的修改时间或者其他元信息,然后与缓存数据比较,如果没有改就使用缓存数据,如果改了就走第二步。
第二步:发送 GET 请求,获取最新的版本。
为此 HTTP 协议还定义了 If 开头的条件请求字段。服务器第一次响应时需要提供 Last-Modified 和 ETag,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。
如果资源没有变,服务器就回应一个304 Not Modified 的状态码,表示缓存依然有效,浏览器就可以更新一下有效期,然后使用缓存。
Last-modified 的意思是文件的最后修改时间。
ETag 是资源唯一标识,主要用来解决修改时间无法准确区分文件变化的问题。
比如说一个文件定期更新,但是有时什么内容都没有发生变化,这时候用 Last-modified 就会误以为发生变化,传给浏览器就会浪费带宽。
使用 ETag可以精确识别资源的变动情况,让浏览器可以有效利用缓存。
其中 Last-modified 对应的常用条件请求字段是 if-Modified-Since,ETag 对应的常用条件请求字段是 If-None-Match。
当服务器第一次发送 Last-modified 给浏览器时,浏览器就把资源缓存下来,然后下次请求时在HEAD请求的头部带上 if-Modified-Since:第一次的 Last-modified 的值给服务器,服务器经过对比,告诉浏览器我这里的文件都是那段时间以前的,于是浏览器就使用缓存加载资源。
ETag 同理,只是这时候请求头的字段换成了If-None-Match。
小结
缓存是优化系统性能的重要手段,HTTP 传输时每个环节都可以使用到缓存
服务器使用 Cache-Control 来设置缓存策略,常用的是 max-age,表示资源有效期
浏览器收到数据就会存入缓存,如果没过期就可以直接使用,过期就要去服务器验证是否依然可用
验证资源是否失效需要使用条件请求,常用if-Modified-Since和 If-None-Match,如果返回304就可以使用缓存里面的资源
验证资源是否被修改涉及到两个条件,ETag和Last-Modified,需要服务器预先在响应报文中设置,搭配条件请求使用
浏览器也可以发送 Cache-Control,比如刷新操作,就会发送 max-age=0来刷新数据
浏览器缓存的一个中心思想就是:没有请求的请求,才是最快的请求
HTTP 代理
HTTP 采用了请求-应答的模式,也就是都说起码会有两方互相协商,在前面的章节说过,我们的 HTTP 中间还可以夹杂各种代理,相当于中间商,这就是 HTTP 代理。
在这一层级链中,起点是浏览器,中间的角色被称为代理服务器,终点则是源服务器。
代理服务本身不产生任何数据内容,而只是作为中间位置转发上下游的请求和响应,也就是说,它既可以是信息的请求方,也可以是信息的响应方。
代理的作用
在简单的请求-应答模式下,为什么要加上一层代理,它有什么作用呢?
代理最基本的功能是负载均衡,所谓负载均衡,就是代理服务器掌握请求分发,将不同的请求流量尽可能地分散发送给不同的源服务器,尽量避免源服务器压力过大,提高整体资源利用率。
在做负载均衡的同时,中间的代理还可以做更多的功能:
- 安全防护:抵御网络攻击或过载
- 加密:通过 SSL/TLS加密通信认证
- 数据过滤:拦截上下行数据
- 内容缓存:暂存复用服务器的响应
代理相关头部字段
代理服务器使用字段 Via标明代理的身份。
Via 是一个通用字段,在响应或者请求都可以使用。每当报文经过一个代理节点,代理服务器就会把自身的信息追加到字段的末尾。
如果通信链路中有很多个代理,就会在 Via 里形成一个链表,这样就可以知道报文究竟走过了多少个环节才到达目的地。
图中有两个代理:
proxy1 和 proxy2,客户端发送请求会经过这两个代理,依次添加就是 Via: proxy1, proxy2 ,等到服务器返回响应报文的时候就要反过来走,头字段就是 Via: proxy2, proxy1 。
Via 字段只解决了 客户端和源服务器判断是否存在代理的问题,还不能知道对方的真实信息。
但服务器的 IP 地址应该是保密的,关系到企业的内网安全,所以一般不会让客户端知道。不过反过来,通常服务器需要知道客户端的真实 IP 地址,方便做访问控制、用户画像、统计分析 。
可惜的是 HTTP 标准里并没有为此定义头字段 ,但已经出现了很多 事实上的标准 ,最常用的两个头字段是 X-Forwarded-For 和 X-Real-IP 。
- X-Forwarded-For:链式存储
字面意思是为 谁而转发 ,形式上和 Via 差不多,也是每经过一个代理节点就会在字段里追加一个信息,但 Via 追加的是代理主机名(或者域名),而 X-Forwarded-For 追加的是请求方的 IP 地址。所以,在字段里最左边的 IP 地址就客户端的地址。
- X-Real-IP:只有客户端 IP 地址
是另一种获取客户端真实 IP 的手段,它的作用很简单,就是记录客户端 IP 地址,没有中间的代理信息。
如果客户端和源服务器之间只有一个代理,那么这两个字段的值就是相同的。
代理协议
有了 X-Forwarded-For 等头字段,源服务器就可以拿到准确的客户端信息了。但对于代理服务器来说它并不是一个最佳的解决方案。
因为通过 X-Forwarded-For 操作代理信息 必须要解析 HTTP 报文头 ,这对于代理来说成本比较高,原本只需要简单地转发消息就好,而现在却必须要费力解析数据再修改数据,会降低代理的转发性能 。
另一个问题是 X-Forwarded-For 等头 必须要修改原始报文 ,而有些情况下是不允许甚至不可能的(比如使用 HTTPS 通信被加密 )。
所以就出现了一个专门的 代理协议 (The PROXY protocol) ,它由知名的代理软件 HAProxy 所定义,是一个 事实标准 ,被广泛采用(注意并不是 RFC)。
代理协议有 v1 和 v2 两个版本,v1 和 HTTP 差不多,也是明文,而 v2 是二进制格式。今天只介绍比较好理解的 v1,它在 HTTP 报文前增加了一行 ASCII 码文本,相当于又多了一个头。
这一行文本其实非常简单,开头必须是 PROXY 五个大写字母,然后是 TCP4 或者 TCP6 ,表示客户端的 IP 地址类型,再后面是请求方地址、应答方地址、请求方端口号、应答方端口号,最后用一个回车换行(\r\n)结束。
例如下面的这个例子,在 GET 请求行前多出了 PROXY 信息行,客户端的真实 IP 地址是 1.1.1.1 ,应答方地址是2.2.2.2,端口号是 55555。
PROXY TCP4 1.1.1.1 2.2.2.2 55555 80\r\n
GET / HTTP/1.1\r\n
Host: www.xxx.com\r\n
\r\n
服务器看到这样的报文,只要解析第一行就可以拿到客户端地址,不需要再去理会后面的 HTTP 数据,省了很多事情。
不过代理协议并不支持 X-Forwarded-For 的链式地址形式,所以拿到客户端地址后再如何处理就需要代理服务器与后端自行约定。
小结
- HTTP 代理就是客户端和服务器通信链路中的一个中间环节,为两端提供 代理服务
- 代理处于中间层,为 HTTP 处理增加了更多的灵活性,可以实现负载均衡、安全防护、数据过滤等功能
- 代理服务器需要使用字段 Via 标记自己的身份,多个代理会形成一个列表
- 如果想要知道客户端的真实 IP 地址,可以使用字段 X-Forwarded-For 和 X-Real-IP
- 专门的 代理协议 可以在不改动原始报文的情况下传递客户端的真实 IP
后话
写到这里,涉及 HTTP 的基础-进阶知识基本已经写完了,如果您没有读够,推荐看一下《透视 HTTP 协议》以及《图解 HTTP》深入阅读。
如果您觉得对您有帮助,随手点👍🏻是对我莫大的鼓励,同时,后续可能我会更新章节,第一时间会发布在 github 上。
如果您觉得博客较长,希望逐篇阅读,也可以查看我的 github,欢迎 star 🌟🌟
github 地址: 我的github