HTTP碎片信息

曾认真阅读过《图解HTTP》和《HTTP权威指南》,本文是过程中的阅读笔记。

HTTP相关RFC文档

HTTP(HyperText Transfer Protocol)于1990年问世,下表是相关RFC及年份信息:

HTTP RFC 年份 说明
HTTP 0.9 1990年 作为非正式标准被提出
HTTP 1.0 RFC1945 1996年5月
HTTP 1.1 RFC2068RFC2616(修订版) 1997年1月 目前主流
HTTP 2.0 RFC7540 2015年5月 基于SPDY

URI

URI有两种:URL和URN,即URL和URN都是URI的子集。现在的Web世界中,几乎所有的URI都是URN。但是URL有个毛病,若资源从一个位置移到另外一个位置,则意味着对应的URL失效了,URN致力于解决这个问题。

  • URL,譬如http://www.example.com/index.html
  • URN,譬如urn:ietf:rfc:2141

URL的基本格式是:

<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<fragment>

可以看到,URL包括很多组件,但是在实际应用中,几乎没有哪个URL包含了所有这些组件。最重要的组件有3个:scheme、host、path。简单说明:

  • @将用户和密码组件与URL的其余部分隔开开来
  • path组件可以由很多个路径分段,譬如/path/to/hell/,每个分段可以有一个参数(param)组件
  • query的格式没有限制,但是一般是item1=value1&item2=value2这种结构
  • 片段(fragment)组件表示一个资源内部的片段,当资源比较大时常会用到

一些常见URL的基本格式:

http://<host>:<port>/<path>?<query>#<fragment>
mailto:<rfc-822-addr-spec>
ftp://<user>:<password>@<host>:<port>/<path>;<params>
file://<host>/<path>
telnet://<user>:<password>@<host>:<port>/

HTTP消息格式

TCP的数据单位被称为TCP报文(segment),HTTP的数据单位被称为HTTP消息(message),下文简称HTTP消息为message。
从方向或应答角度来看,message有两种:请求消息(Request)、响应消息(Response)。请求消息和响应消息都由1个开始行(start-line)、0个或多个消息头(headers)、可有可无的消息主体(message-body)组成,如下:

generic-message =
start-line ; 开始行
*(message-header CRLF) ; 消息头
CRLF
[message-body] ; 消息主体
; 其中CRLF表示“结束符”
; *表示“0个或多个“”
; []表示“可有可无”

下面将围绕开始行、消息头、消息主体这几个概念进行最粗浅的概述。

开始行(Start-Line)

开始行是什么样的格式?这可不一定,因为对于不同的消息类型(请求消息和响应消息),开始行的格式是不同的:

  • 对于请求消息,start-line是Request-Line(请求行),请求行的格式
  • 对于响应消息,start-line是Status-Line(状态行),状态行的格式

消息头(Headers)

根据作用域来分,消息头分为:常用头(general-header)、请求头(request-header)、响应头(response-header)、实体头(entity-header)。无论如何,它们的格式总是这样:

message-header = field-name ":" [field-value]

其中field-name对大小写不敏感。首部内容非常丰富,估计得专门开辟一篇博客来记录。

field-value对大小写敏感吗?似乎没有确切的说法,参考:

消息主体(Message-Body)

RFC2616中讲:

The message-body (if any) of an HTTP message is used to carry the entity-body associated with the request or response.

如下:

message-body = entity-body | <entity-body encoded as per Transfer-Encoding>

消息主体 v.s 实体主体

消息主体(message-body)和实体主体(entity-body)是非常容易混淆的两个概念:

  • entity-body可以被理解为客户端想让服务端看到的内容
  • message-body指的是服务端接收到的(来自于客户端)实际内容

通常,消息主体等于实体主体,但是,当在传输过程中进行编码时,entity-body就不再等于message-body了,此时,根据我的理解,message-body等于传输编码后的entity-body。

安全方法和幂等方法

请求消息有若干种方法,常见划分种类有两种:安全方法(Safe Methods)和幂等方法(Idempotent Mehtods)。

要理解安全方法,先介绍一个概念:副作用,副作用指当你发送完一个请求以后,网站上的资源状态没有发生修改,即认为这个请求是无副作用的。比如注册用户这个请求是有副作用的,获取用户详情可以认为是无副作用的。

对于幂等方法,幂等是说一个请求原封不动的发送N次和M次(N不等于M,N和M都大于1)服务器上资源的状态最终是一致的。比如发贴是非幂等的,重放10次发贴请求会创建10个帖子。但修改帖子内容是幂等的,一个修改请求重放无论多少次,帖子最终状态都是一致的。

请求消息的方法众多,有些的方法只是读取服务器的资源,有的方法可能会修改服务器的资源。GET和HEAD属于前者,它们只是获取资源,这些方法被称为安全方法;POST、PUT、DELETE属于后者,它们可能使服务器的资源发生变化,这些方法被称为幂等方法。

P.S: 根据我的理解,所谓的安全方法和幂等方法只是一种臆想,不是绝对的。举个例子,服务器有某篇文章,现在浏览器通过GET方法获取这篇文章,当然,客户端并没有修改这篇文章,但是,服务器可能做了这样的处理:将这篇文章的浏览次数+1;客观来讲,这个GET方法还是修改了服务器的资源;所以,知道安全方法和幂等方法这两个概念就好,不必当真。

P.P.S: 这只是我的理解,可能是错的…

持久连接

客户端发起一个请求,服务端给出响应,这个一来一回的过程被称为「一个HTTP事务」。在非持久连接中,每个HTTP事务处理结束之后,TCP连接会被关闭。

而从1.1版本开始,HTTP默认开启持久连接。简单来说,在事务处理结束之后仍然保持在打开状态的TCP链接被称为持久连接。

HTTP/1.1和HTTP/1.0实现持久连接的方式不一样,因此得分开讨论。

HTTP/1.0的持久连接

在HTTP/1.1之前版本中,HTTP默认都是非持久连接,某些实现支持了持久连接,若想维持持久连接,client需要在request中携带Connection: Keep-Alive首部,同样,sever需要在response中也携带Connection: Keep-Alive首部。
使用HTTP/1.0的Keep-Alive,有一些限制和需要澄清的地方:

  • 如果response中没有Connection: Keep-Alive首部,意味着server会在之后关闭连接
  • Connection: Keep-Alive首部必须随所有希望保持持久连接的报文一起发送,如果某个request没有发送该首部,则服务器会在那条请求之后关闭连接
  • 只有在无需检测到连接的关闭即可确定entity-body长度的情况下,才能将连接保持在持久状态,也就是说
    • 必须有正确的Content-Length来标明entity-body的长度
    • 有multipart媒体类型,或者用分块传输编码的方式进行了编码
  • 《HTTP权威指南》还有一些说明…

HTTP/1.1的持久连接

在HTTP/1.1版本中,持久连接在默认情况下是激活的,无需开启。除非特别指明,否则HTTP/1.1假定所有连接都是持久的。要在事务处理结束之后将连接关闭,HTTP/1.1应用程序必须向报文显式添加Connection: close首部。

问题:Connection: close首部是在request中,还是在response中呢?答案是:Client和server都可以在报文中携带该首部。

HTTP/1.1持久连接的使用,也有一些限制和需要澄清的地方:

  • 发送了Connection: close请求首部后,client就无法在那条连接上发送更多的请求了
  • 如果client不想在连接上发送其他请求,就应该在最后一条请求中发送一个Connection: close首部
  • 只有当连接上所有的报文都有正确的、自定义报文长度时,连接才能持久保持,也就是说,entity-body长度和Content-Length一致,或者用分块传输编码方式编码
  • 一个client对任何server或者proxy,只能维持两条持久连接,以防server过载

Keep-Alive首部

HTTP/1.0定义了Keep-Alive首部(在HTTP/1.1中不复存在),可以用来调节持久连接的行为,譬如:

Keep-Alive: timeout=10, max=500

说明如下:

  • 参数timeout是在response首部里发送的,它估计了server持久连接保持活跃的时间,这并不是一个承诺值
  • 参数max也是在response首部里发送的,它估计了server还希望为多少个事务保持此连接的活跃状态,这依然不是一个承诺值

P.S: 注意区分Keep-Alive首部和上文的Connection: Keep-Alive首部,后者中的Keep-Alive是一个field value,前者是一个field name。

P.S: 显然,Keep-Alive首部只有在提供Connection: Keep-Alive的情况下才有意义。

根据我的理解,Keep-Alive首部在HTTP/1.1中没有使用的必要。

Cookie用于管理服务器和客户端之间的状态,它并没有被纳入到HTTP/1.1的RFC2616中,但其应用非常广泛。这一部分旨在整理Cookie相关知识,并尝试搞清楚它在移动客户端中的应用。

Cookie相关协议

Cookie最开始由网景公司于1994年为其浏览器开发,并制定了相关的规则标准,目前最为普及的Cookie方式也只是在此基础上建立的。后来陆陆续续又产生了一些RFC:

RFC 说明
RFC2109 差不多淡出人们视线了
RFC2965 诞生于IE和Netscape浏览器大战的年代,定义了新的Set-Cookie2和Cookie2,事实上,几乎没怎么被使用
RFC6265 将网景公司制定的标准作为业界事实标准,重新定义Cookie标准后的产物

目前使用的最广泛的Cookie标准不是上述RFC中的任何一个…

相关首部

为cookie服务的常用首部字段有两个:

  • Set-Cookie,由response携带,指示client存储cookie到本地
  • Cookie,由request携带,将本地cookie传给server

P.S: 这两个首部没有在HTTP/1.1中定义。

Set-Cookie首部

Set-Cookie字段包括好些属性,如下是说明:

属性 说明 版本 举例
Expires 到期时间,若不指定,则浏览器关闭时即删除 网景、RFC6265
Domain cookie适用的域名,若不指定,则默认为创建cookie的服务器的域名 网景、RFC2965、RFC6265
Path cookie适用的path 网景、RFC2965、RFC6265
Secure 指示客户端仅当在HTTPS请求时才发送Cookie 网景、RFC2965、RFC6265 Set-Cookie: id=42; secure意味着,告诉客户端只对HTTPS请求发送cookie
HttpOnly 加以限制,使得cookie不能被JavaScript脚本访问 RFC6265 防止跨站脚本攻击(Cross-site scripting,XSS)
Version 必选,Set-Cookies2使用 RFC2965
Comment 说明server将如何使用这个cookie RFC2965
CommentURL 更加详细说明server如何使用这个cookie的url RFC2965
Discard 如果提供了这个属性,客户端在程序终止时,需删除cookie RFC2965
Max-Age 和Expires作用类似 RFC2965、RFC6265
Port cookie适用的端口 RFC2965

前五个属性是目前使用最广泛的。

《HTTP权威指南》将cookie笼统分为了两种类型:会话cookie、持久cookie。前者是一种临时cookie,应用程序退出时,会话cookie就被删除了。持久cookie的生存时间更长一些,它们存储在硬盘上,应用程序退出时,仍然会保留它们。简单来说,当设置了Discard,或者没有设置Expires或Max-Age,就意味着这个cookie是一个会话cookie。

Cookie首部

Cookie首部比较简单,用于将本地cookie传给server,没啥好说的。

iOS与Cookie

作为iOS开发者,我比较关心iOS中与Cookie相关的资源。iOS里有两个与HTTP cookie相关的类型:

  • NSHTTPCookie,用于封装cookie,它的属性覆盖了上文表格中Set-Cookie的每一个属性
  • NSHTTPCookieStorage,单例类,提供了管理所有NSHTTPCookie对象的接口,在OS X里,cookie是在所有程序中共享的,而在iOS中,cookie只当当前应用中有效

实体

实体(entity)包括实体首部(entity-header)和实体主体(entity-body):

entity-body@2x.png

实体首部和实体主体之间被一个空白行(CRLF)分隔。

P.S: CR(0d,\r)LF(0a,\n)。

划定报文结束

HTTP 1.1与之前版本,划定报文结束的逻辑处理时不一样的。

早期版本(1.1之前版本),采用关闭连接的办法来划定报文的结束。即便这样,如果没有Content-Length首部,client无法区分到底是报文结束时正常的连接关闭,还是报文传输中由于server出差错而导致的关闭,因此Content-Length还是非常必要的。

对于1.1版本,由于HTTP默认为持久连接,无法再根据TCP报文来判断body传输完毕…

实体首部字段

实体首部还蛮多,如下表:

首部 说明 例子 报文类型
Allow 通知client所支持的request方法类型(GET、POST) Allow: GET, HEAD Response
Content-Type 实体中所承载对象的类型 Response
Content-Length 如果有压缩,该值记录的是压缩后的大小(单位是字节),而不是原始大小;除非使用分块编码,否则该字段是带有实体的报文必须要使用的。 Response
Content-Language 与所传送对象最相匹配的人类语言 Content-Language: zh-CN Response
Content-Encoding 通知client对entity-body所使用的内容编码方式,主要有4种:gzip、compress、deflate、identity(没有编码) Content-Encoding: gzip Response
Content-Location 给出与entity-body相对应的URI,表明该entity-body取自何处 Content-Location: http://www.example.com Response
Content-MD5 对entity-body的校验和,客户端会对接收的entity-body执行相同的MD5算法,然后与该字段值进行比较 Response
Content-Range 如果这是个部分entity,该首部说明它是整体的哪个部分 Response
Expires 指定资源失效的时间 Response
Last-Modified 指定资源最终被修改的时间 Response

内容编码

内容编码(Content Encoding)通常用于对实体内容进行压缩编码,目的是优化传输,例如用gzip压缩文本文件,能大幅减小体积。内容编码通常是选择性的,例如jpg/ png 这类文件一般不开启,因为图片格式已经是高度压缩过的,再压一遍没什么效果不说还浪费CPU。

关于它,没啥好说的…

传输编码

曾一度弄不清楚内容编码和传输编码的区别,《图解HTTP》这本书似乎也没讲清楚这个问题。再次翻看《HTTP权威指南》才恍惚搞懂是怎么回事儿。

除了《HTTP权威指南》,博客HTTP协议中的Transfer-Encoding也很清晰解释了何为传输编码,参考它们就好了,本文就不再对这个概念进行赘述。

传输编码相关首部

与传输编码相关的首部有两个:

Transfer-Encoding

该首部用于指定传输编码的编码方式,在1.1版本之前,HTTP支持多种传输编码方式,但在1.1版本中,只支持一种传输编码方式 – 分块传输编码,即Transfer-Encoding只能指定首部值chunked,即Transfer-Encoding: chunked

该首部一般在server发给的client的response报文中出现,告诉client已经对body进行了分块传输编码。

TE

Server怎么知道它发送的分块编码报文是否能被client接受呢?TE首部正是用于解决这个问题,它用在request报文中,告知server可以使用哪些传输编码(当然,一般只有值chunked),譬如TE: chunked

P.S: TE首部还可以挂上trailers参数,下文会提到。

传输编码的作用

传输编码的意义有哪些呢?《HTTP权威指南》总结了两点:

  • 处理未知尺寸的报文。这是本文阐述的重点,详见下文的分块传输编码部分
  • 提高安全性,可以用传输编码把报文扰乱,然后在共享网络上传输;不过,由于SSL/TLS这种传输层安全体系的流行,很少要靠传输编码来处理安全性事务了

对于第二点「提高安全性」,我还不咋理解,如何通过传输编码提高安全性?暂且搁下吧,若有需要,以后再补充…

分块编码

对于分块编码,《HTTP权威指南》的描述是:分块编码把报文分割为若干个大小已知的块。

我刚开始以为分块编码是把一个HTTP报文拆分为多个报文,后来发现完全理解错了。

分块编码与持久连接

如上文所述,刚开始,我以为「分块编码是把一个HTTP报文拆分为多个报文」,如果果真如此,那么持久连接显然是分块编码的先决条件…

在非持久连接中,client无需知道它正在读取的body的长度,只需读到server关闭连接为止,Content-Length于client而言没那么必要。但一旦HTTP支持持久连接后,就不能再以连接关闭作为body读取完毕的哨兵事件了,因此非常依赖server提供的Content-Length首部。

有时候,server提供Content-Length是一件非常吃力的事情,借用HTTP协议中的Transfer-Encoding的举例说明:

例如实体来自于网络文件,或者由动态语言生成。这时候要想准确获取长度,只能开一个足够大的buffer,等内容全部生成好再计算。但这样做一方面需要更大的内存开销,另一方面也会让客户端等更久。

分块编码就是为了解决这种困难而生的。它允许server把body逐块发送,说明每个block的大小即可。因为body是动态创建的,server可以缓冲它的一部分,发送block及相应的size说明,然后在body发送完之前重复这个过程,最后,server用大小为0的block作为body结束的信号,这样就可以继续保持连接,为下一个response做准备了。

再次说明,这些过程仍然是在一个message上进行的,并没有将message拆分为多个。

分块编码报文的基本结构

分块编码报文的基本结构非常简单,和普通的HTTP报文差距仅在于body部分。body部分由各个block组成,某个block包含一个长度值和该block的数据,长度值和block数据使用CRLF分隔开,长度值是十六进制形式,block数据大小以字节计。最后一个block有些特别,它的长度值为0,无数据部分,表示「body结束」。如下图所示:

transfor-encoding-trunk@2x.png

分块报文的拖挂

如上图所示,在最后一个block后面还有一个拖挂(trailer),这个名字挺怪的;我的理解,server可以在分块报文的末尾加上可选元数据,数据格式HTTP不作要求。

上图的拖挂是Content-MD5首部,只是因为Content-MD5只有在body全部生成完了才能计算,所以把它放在trailer里是合适的选择。

对于client来说,它可以对trailer不做任何处理;对于server而言,它如何知道client是否会处理trailer呢?如果不处理,硬塞一个trailer岂不浪费?HTTP的TE首部解决了这个问题,TE首部除了告诉server它接受的传输编码方式之外,它还可以指定trailers属性,以告诉server它可以处理分块报文中携带的拖挂,譬如:TE: trailers

Q & A

这部分以Q & A的形式补充对HTTP基本概念的理解。

如何在HTTP消息中指定URL?

HTTP基于URL定位网络资源,有多种方式指定URL。

请求行(Request-Line)的格式如下:

Request-Line = Method SP Request-URL SP HTTP-Version CRLF
;
; SP表示“分隔符”
; CRLF表示“结束符”

显然,可以在Request-Line中指定URL,譬如这样:

; 请求行
GET http://example.com/index.html HTTP/1.1

也可以这样:

; 请求行
GET /index.html HTTP/1.1
; 首部字段Host中写明网络域名或IP地址
Host: example.com

GET请求可以携带消息主体吗?

据我所知,RFC文档没有明确表明GET请求不能携带message-body,综合各种说法,我的判断是:有些server不处理GET的message-body信息,有些server会处理。总之,与具体的实现有关。

参考:

Client可以发送分块编码的报文给server吗?

Client也可以发送分块的数据给server。只是,client事先不知道server是否接受分块编码(这是因为server不会在给client响应中发送TE首部),所以server必须做好server用411 Length Required(需要Content-Length首部)响应来拒绝分块请求的准备。