HTTP协议漫谈,协议漫谈


前言

去年下半年以来各种俗事缠身,所以有段时间没有更新博客了。过完年回来事情不多,项目需求也比较少,又可以愉快的写博客了(≧∇≦)ノ

这几天在网络上搜罗了一些HTTP协议的相关知识,并对这些知识做了一番整理,由于内容较多,预计会写成数篇博客。文章中大部分内容都来源于网络,其中也加入了一些自身的理解。对有明确来源的都会给出参考链接,但由于精力有限,无法查证每个链接的最原始的出处,所以有些链接可能并非是原作者发表的。由于大部分都是一些理论知识,来源也都是网络上的各种资料,加上自身理解能力和知识面的局限,可能会有一些错误,所以如果在阅读时发现有一些谬误,欢迎各种看官指正。

HTTP协议历史和版本

HTTP的诞生

HTTP协议最早由万维网之父Tim Berners-Lee提出。

1989年3月在CERN担任研究员的Tim提交了一个提案《关于信息管理的一份提案》(Information Management: A Proposal),提案中描绘了其对万维网(World Wide Web)最初的设想,他希望在CERN内部建立这样的一个网络,满足内部信息交换的需求。不过这份提案并没有被CERN所重视,好在当时Tim的经理仍然给了他一段时间来实现这个方案。

到了1990年10月,Tim完成了万维网三大基础技术的设计:命名方案(URI),通信协议(HTTP)和用来表示信息的标记语言(HTML)。根据这些设计,他实现了一个webserver (httpd)和一个浏览器(WorldWideWeb.app) 。到1990年底,第一个网页已经能够在CERN内部的internet上浏览了。

关于Tim本人和他创立万维网的一些故事可以参考文章如何评价万维网之父Tim Berners-Lee 获得2016年度图灵奖?

HTTP/0.9

在1991年,Tim根据之前的实现,写了一篇关于HTTP协议的文章,这篇文章后来被看成是HTTP/0.9版本,但实际上这篇文章很难称得上是一个标准的协议,它并非出自某个标准化组织,只是Tim的个人作品,用来解释其之前实现的程序中的通信过程。不过鉴于Tim对万维网的开创性贡献,将他的这篇关于HTTP协议的文章作为HTTP的第一版标准,也是无可厚非的。这篇文章可以在https://www.w3.org/Protocols/HTTP/AsImplemented.html找到,但这似乎是后来重新整理过的,并非是原始版本。这篇文章相当简单,只有短短的四小节。它规定了HTTP使用的TCP/IP连接,HTTP请求只有一个请求行,只有一个GET方法加上请求的URI。HTTP响应则直接返回HTML文本,没有状态码,所以也没有办法区分错误消息和正常的文本。

HTTP/1.0

在这之后万维网经历了一段快速发展而又十分混乱的时期。由于Tim最初版本的HTTP和HTML并不完善,无法满足各种应用场景,因此很多公司和组织在其基础上做了各种扩展,但彼此并不兼容。到1994年,Tim离开了CERN成立了W3C组织,致力于HTML的标准化工作。同年,IEFT成立了HTTP工作组(HTTP Working Group: HTTP-WG),研究改进HTTP协议。HTTP-WG考察了当时市面上已有的各种HTTP协议实现,综合了其中的一些常见用法,终于在1996年5月发布了RFC 1945,这就是现在HTTP/1.0。值得注意的是RFC 1945现在的状态是INFORMATIONAL(区别于INTERNET STANDARD,PROPOSED STANDARD和DRAFT STANDARD,关于Internet Standard的标准化过程可以参见维基百科Internet Standard),所以它并非是一个正式的互联网标准。

HTTP/1.0在HTTP/0.9的基础上做了大量的扩充和改进。增加了请求头域和响应头域,增加了HEAD和POST方法,响应对象不再局限于HTML文本,支持长连接和缓存机制等等。

HTTP/1.1

在RFC 1945发布半年多之后,即1997年1月,HTTP/1.1的第一个版本RFC 2068正式发布,这是一个PROPOSED STANDARD。1999年6月,HTTP/1.1的第二个版本RFC 2616发布并取代了RFC 2068。可以看到这段时间HTTP协议的发展是很快的,在RFC 2616发布后,HTTP协议进入一个相对稳定的时期。到2014年,IETF发布了6个新的RFC文档用来取代RFC 2616, 这6个文档的编号分别为7230, 7231, 7232, 7233, 7234和7235。它们将原先的RFC 2616拆分成了6个部分。这6个RFC文档目前都是PROPOSED STANDARD。

这个6个文档的内容划分如下。

RFC 7230-7235在RFC 2616的基础上做了大量内容上的调整,不过大部分都是结构,名词和表述性的修改,原先的语法和语义部分没有太大变化。目前网上的大部分资料仍然是基于RFC 2616的,这里有些名词和表述已经被废弃或修改了,但这一般并不影响阅读和对协议的理解。后文的表述中也可能会同时存在新旧RFC文档的内容,部分表述上做了区分。

HTTP/2

2015年5月RFC 7540发布,这也是HTTP协议目前的最新版本HTTP/2,该RFC文档目前同样是PROPOSED STANDARD。时间回退到半年前,在2014年10月28日,W3C的HTML工作组发布了HTML5的正式推荐标准,完成了长达八年的HTML5的标准化工作。HTTP和HTML,万维网的两大基础技术,在经过多年的沉寂后终于完成了各自的大更新。

有意思的是这里的版本号并没有和之前版本一样,采用2.0这样的大版本.小版本的形式,维基百科HTTP/2中介绍最初版本命名确实是HTTP/2.0,但是后来去掉了,至于去掉的原因,在HTTP/2 FAQ中有提到,认为旧的大小版本机制造成了很多混乱,但没有指明具体是什么混乱。

HTTP/2的主要目标在于提高传输性能,实现低延迟和高吞吐量。HTTP/2没有改动旧的HTTP版本的语义,方法、状态码、URI 和头部信息。所以通常认为HTTP/2是HTTP/1.1的扩展,而不是替代。HTTP/1.1对应的RFC文档中的大部分内容对HTTP/2也是适用的,RFC 7540中也没有将这部分内容重新编排,只是在涉及到这部分内容时,给出了原先RFC文档中的链接。关于HTTP/2的更多信息参见HTTP/2 简介

HTTP/2协议本身并不要求必须加密传输,它可以基于TLS实现加密传输(HTTP/2 over TLS: h2),也可以使用明文传输(HTTP/2 without TLS: h2c),然而出于推广https的需要,以及一些技术上的考虑,目前所有的浏览器都不支持h2c。所以如果要支持HTTP/2,必须基于TLS来部署,这也在一定程度上限制了HTTP/2的普及。

此外,HTTP/2在发布后一直争议不断,关于HTTP/2的一些争议可以参见文章维基百科HTTP/2中Criticisms一节,The HTTP/2 Protocol: Its Pros & Cons and How to Start Using It和HTTP/2: In-depth analysis of the top four flaws of the next generation web protocol。

由于种种原因,尽管HTTP/2发布后不久,主流的浏览器和服务端程序就已经支持,但在业界的普及率始终不高,根据w3techs的统计,到今天(2018/03/07)使用HTTP/2的网站只有24.2%,但使用HTML5的网站则达到了86.7%。


关于HTTP历史和版本的其他信息可以参考文章 History of the Web,History of the World Wide Web和Brief History of HTTP。

HTTP报文结构

HTTP数据单元

这里顺便复习了一下网络模型中各层的数据单元。在网络模型中,协议数据单元PDU(Protocol Data Unit)表示对等层次之间传递的数据单位。参照维基百科的描述,在OSI网络模型中,物理层PDU为比特(Bit),数据链路层PDU为帧(Frame),网络层PDU为数据包(Packet),传输层PDU对TCP协议来说称为数据段(Segment),对UDP协议来说称为(Datagram)。传输层以上的会话层,表示层和应用层的数据单元都统称为数据(Data)。

下图是维基百科OSI网络模型中的截图

这里关于传输层PDU的描述相当令人困惑,作为抽象网络模型的数据单元,PDU应当也是一个抽象的,和实现协议无关的术语,但是这里却将传输层的PDU和具体的协议关联在一起。查看了一些其他文章,有些文章中将传输层PDU称为Segment,不区分具体的协议。这里姑且采用这种说法,即物理层PDU为比特(Bit),数据链路层PDU为帧(Frame),网络层PDU为数据包(Packet),传输层PDU为数据段(Segment),会话层,表示层和应用层的PUD为数据(Data)。

HTTP协议属于应用层,所以按照之前的PDU定义,HTTP协议的数据单元应当叫做HTTP Data,即HTTP数据。但似乎HTTP协议一般都用报文(Message)来称呼其数据单元,并不使用Data。

以下是维基百科中HTTP协议的截图,可以看到其中HTTP发送和接收的消息都成为message。

HTTP报文结构(Message Format)

在RFC 2616中,将HTTP报文分为请求报文(request message)和响应报文(response message),并分别描述了它们的结构。以下是RFC 2616中关于请求报文和响应报文结构的描述。

HTTP请求报文包含三个部分:请求行(request line),请求头域(request header fields)和请求体(message body)。请求头域可以有多个,最后一个请求头域和请求体之间有一个空白行。有些文章的描述中,将空白行作为一个单独的部分,认为HTTP请求报文包含四个部分(维基百科HTTP协议)。本文采用三个部分的描述形式。

在一个HTTP请求中,请求行和请求头域都是必须有的,而请求体是可选的,但即使没有请求体,请求头域之后仍然需要有一个空白行CRLF。表示请求头域的结束。多个请求头域之间也用CRLF分割。

下图来源于博客文章HTTP请求、响应报文格式

HTTP响应报文也包含三个部分:状态行(status line),响应头域(response header fields)和响应体(message body)。响应头域可以有多个,最后一个响应头域和响应体之间有一个空白行。

在一个HTTP响应中,状态行和响应头域都是必须有的,而响应体是可选的,但即使没有响应体,响应头域之后仍然需要有一个空白行CRLF,表示响应头域的结束,多个响应头域之间也用CRLF分割。

下图同样来源于博客文章HTTP请求、响应报文格式

可以看到响应报文的结构和请求报文的结构是基本相同的,主要区别在于请求行和状态行的不同。请求体和响应体实际上是一样的,它们在RFC 2616都称为message body,这里之所以有两个名字,只是为了方便识别这个body究竟是请求报文中的,还是响应报文中的,但需要知道的是,请求体和响应体无论在语义上还是结构上都是完全相同的。

在RFC 2616中对header fields的表述有点混乱,它将消息头分为通用头(general-header),请求头(request-header),响应头(response-header)和实体头(entity-header)四种类型。HTTP协议中又预定义了一系列标准的消息头名字,每个名字都有其明确的语义。通用头指的是那些既可以作为请求头,又可以作为响应头使用的那些消息头,例如缓存控制Cache-Control,消息发送时间Date等。请求头只能用在HTTP请求的头域中,例如包含UA信息的User-Agent等。而响应头只能用在HTTP响应的头域中,例如表示服务器软件版本的Server等。在RFC 2616中还有一个实体(Entity)的概念,实体指的是要传输的对象,实体又包含两部分,实体头(entity body)和实体正文(entity message)。实体头指的是那些和实体相关联的属性组成的消息头,例如,实体的长度Content-Length,实体的类型Content-type等。实体头会放在请求头域和响应头域中,而实体正文则作为请求体或响应体放到HTTP报文中,也就是说,实体是一个包含了message body,以及头域中和message body属性相关的那部分消息头的一个逻辑上的概念。

在RFC 7230中,对报文结构的描述做了一定调整,不再区分请求报文结构和响应报文结构(只是不区分请求报文结构和响应报文结构,请求报文和响应报文还是区分的),而是将整个HTTP报文结构作为整体来描述。

在RFC 7230中,一个HTTP报文包含三个部分:起始行(start line),头域(header fields)和消息体(message body)。起始行根据是请求报文还是响应报文,又分为请求行(request line)和状态行(status line)。头域和消息体之间同样是有一个空白行CRLF。

可以看到在RFC 7230中的一个改变是将请求行和状态行合称为起始行。但更重要的改变是对头域和消息体结构描述的变化。在RFC 7230中,完全去掉了实体的概念,也就是说现在的消息体就是要传输的对象,无需再从头域中挑出一部分消息头,然后和消息体组合在一起,再脑补出一个对象来。同时还去掉了没有多大意义的消息头的分类。无论是请求头域还是响应头域都可以同等看待,大家都是一组名字和值组成的序列。

注意到RFC 7230中的这些变化都是表述上的变化,并不涉及实际结构和语义的改变。

起始行(Start Line)

如前所述,起始行是RFC 7230中增加的一个概念,RFC 7230中对起始行的结构描述为:start-line = request-line / status-line。也就是它表示一个请求行或一个状态行。

在RFC2616中对HTTP请求行的结构描述为:Request-Line = Method SP Request-URI SP HTTP-Version CRLF
翻译过来就是:请求方法+空格+请求URI+空格+HTPP协议版本+回车换行。

这里和上面的配图中的表述稍有不一样,按照RFC文档的描述,请求方法后应当是URI,而不是URL。这里采用RFC文档中的表述,使用URI。但实际上使用URL也是没错的。

在RFC2616中对HTTP状态行的结构描述为:Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
翻译过来就是:HTTP版本+空格+状态码+空格+状态码描述+回车换行。

请求行和状态行中HTTP版本的语义和表示方法都是一样的。HTTP协议版本的表示方式为”HTTP/” + 版本号。其中”HTTP/”是固定的字符串,区分大小写,也就是只能是”HTTP/”,而不能是”http/”或其他形式。版本号由两个十进制数字组成,中间用.分割,第一个数字为大版本号,第二个数字为小版本号。到目前为止,一共有四个可用的版本:0.9,1.0,1.1和2,结合前面的”HTTP/”就是”HTTP/0.9”,”HTTP/1.0”,”HTTP/1.1”和”HTTP/2”。目前应用最广泛的仍然是”HTTP/1.1”版本。关于HTTP版本的更多讨论参见RFC 7230 2.6节。

头域(Header Fields)

HTTP头域包含了请求或响应过程中需要用到的一些额外的信息,它通常包含一个名字和对应的值。在HTTP报文中,多个头域之间用CRLF(回车换行)来分割,一个头域的名字和值之间则用冒号:分割,值前面可以包含任意多个空白字符,一般用一个空格,也就是”名字: 值”的形式来表示,这些空白字符在解析时都会被自动忽略。在RFC 2616中允许一个头域的值跨越多行,只需要在每行的开头,也就是名字前加上至少一个空格(SP)或水平制表符(HT),但是这一条在RFC 7230中被标记为deprecated。

头域中的名字不区分大小写,也就是说cache-control,Cache-Control和Cache-control都是一样的,表示同一个含义,不过一般来说都采用每个单词首字母大写,其他字母小写的形式,对缓存控制就是Cache-Control。

头域的值一般区分大小写,值也可以没有,但即使没有值,名字后面的冒号也不能少。值前后的空格在解析时都会被移除,值中的多个连续的空白字符一般也会在解析时被替换成一个空格。

多个头域的名字如果不同,则它们之间没有先后顺序,但协议同时规定多个头域中允许出现重复的名字,如果有重复名字的头域,那么它们有先后顺序之分。

如果出现重复名字的多个头域,协议要求能够将它们组合成一个头域,组合的方式为,将这些头域的值按照顺序排列,中间用逗号分隔,这样组合之后的头域和原先的多个头域含义必须相同。

举例来说,如下几组头域,前三组是等价的,后面两组也是等价的,但前三组中任意一个和后两组中的任意一个都不等价。

HTTP报文大小限制

如前所述,一个HTTP报文包含起始行,头域和消息体,HTTP协议本身并没有对报文中任一部分的长度做限制,也就是说,理论上一个请求URI可以无限长,头域可以无限多,请求体可以无限大。但在实际场景下,请求URI的长度会受到浏览器的限制,如果在浏览器中输入过长的URL,那么浏览器会自动进行截断。而服务器出于安全性和效率的考虑,也会对头域和消息体的大小作出一定的限制。

后记

这篇文章是从2月28号开始写的,到今天正好是一周时间。原本是打算将所有内容写在一篇文章中,题目是”HTTP协议漫谈”,也就是没有这里的副标题。一开始是资料查到哪里写到哪里,没有固定的提纲。但是随着资料越查越多,原先零散的内容也开始有了一些关联,并且有了一些结构化的东西。后来索性填充了一个大体的框架,然后不断补充内容。在这期间一直都在断断续续的查找资料,补充文章和修改之前写好的内容。但是到今天发现仍然有十多节的内容没有写,要全部写完估计还得需要不少时间,项目又开始有一些需求要做了。为了避免时间拖太长,也避免文章太长,所以就先把前面部分完善了,作为一篇博客发布,后面的部分预计还会分成四篇博客:请求方法,请求URI,头域和HTTPS。希望不会烂尾,哈哈哈~(虽然前面已经烂尾了好几篇了^_^)

下一篇会介绍HTTP请求方法的一些相关知识。

参考链接:

[1]: https://www.w3.org/History/1989/proposal.html
[2]: https://tools.ietf.org/html/rfc2068
[3]: https://tools.ietf.org/html/rfc2616
[4]: https://tools.ietf.org/html/rfc7230
[5]: https://tools.ietf.org/html/rfc7231
[6]: https://tools.ietf.org/html/rfc7232
[7]: https://tools.ietf.org/html/rfc7233
[8]: https://tools.ietf.org/html/rfc7234
[9]: https://tools.ietf.org/html/rfc7235
[10]: https://tools.ietf.org/html/rfc7540

[11]: https://en.wikipedia.org/wiki/OSI_model
[12]: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol
[13]: https://en.wikipedia.org/wiki/History_of_the_World_Wide_Web
[14]: https://en.wikipedia.org/wiki/Internet_Standard
[15]: https://en.wikipedia.org/wiki/HTTP/2

[16]: http://blog.csdn.net/a19881029/article/details/14002273
[17]: https://webfoundation.org/about/vision/history-of-the-web/
[18]: https://www.zhihu.com/question/58037961/answer/155413736
[19]: http://www.infoq.com/cn/news/2014/06/http-11-updated
[20]: https://http2.github.io/faq/
[21]: https://www.upwork.com/hiring/development/the-http2-protocol-its-pros-cons-and-how-to-start-using-it/
[22]: https://www.imperva.com/docs/Imperva_HII_HTTP2.pdf
[23]: https://w3techs.com/

查看评论

相关内容