IP, TCP 和 HTTP(二)

HTTP —— Hypertext Transfer Protocol

万维网的互联超文本文件以及使用浏览器浏览网站源自1989年 CERN 提出的一个 idea。用于数据通信的协议是 超文本传输协议( Hypertext Transfer Protocol ) 或 HTTP。今天的版本是 HTTP/1.1,定义在 RFC 2616。

请求和响应

HTTP 使用简单的请求和响应机制。当我们在 Safari 中输入 http://www.apple.com 时,它会向 www.apple.com 的服务器发送一个 HTTP 请求,服务器会返回一个包含请求文档的(单个)响应。

总是有一个请求对应一个响应,或者多个请求和响应遵循这种方式。

一个简单的请求 A Simple Request

当 Safari 加载 http://www.objc.io/about.html 的 HTML 文件时,它向 www.objc.io 的服务器发送了一个这样的 HTTP 请求:

GET /about.html HTTP/1.1
Host: www.objc.io
Accept-Encoding: gzip, deflate
Connection: keep-alive
If-None-Match: "a54907f38b306fe3ae4f32c003ddd507"
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
If-Modified-Since: Mon, 10 Feb 2014 18:08:48 GMT
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.74.9 (KHTML, like Gecko) Version/7.0.2 Safari/537.74.9
Referer: http://www.objc.io/
DNT: 1
Accept-Language: en-us

第一行是 请求行( request line ),它包含三个部分,操作,资源和 HTTP 版本。

我们的例子中,操作是 GET,该操作也通常被成为 HTTP 方法。资源指定操作需要获取的资源,在我们的例子中是 /about.html,即告诉服务器我们想获取的文档在 /about.html 下。HTTP 的版本是 HTTP/1.1

接下来,我们有十行的 HTTP 请求头信息。它们以空行结尾,请求头内没有请求体。

请求头有各不相同的目的,它们向服务器传达额外的信息。Wikipedia 有详细的请求头域描述。第一个 Host:www.objc.io 标头告诉服务器请求的服务器名称。这种强制性的请求头允许相同的物理提供多个域名。

接着看几个常见的例子:

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us

这里告诉服务器 Safari 想要接收的媒体类型。服务器可能以各种格式发送响应。text/html 字符串是互联网媒体类型, 有时也被成为 MIME typesContent-typesq=0.9 表示允许 Safari 传达的与媒体类型相关联的条件。Accept-Language 告诉服务器 Safari 接受的语言类型。同样这使得服务器选择匹配的语言,如果可以的话。

Accept-Encoding: gzip, deflate

上面的标头,Safari 告诉服务器响应体可以被压缩。如果没有被设置的话,服务器必须发送未压缩的数据。特别是对于文本(如 HTML),压缩比率可以显著的降低服务器需要发送的数据量。

If-Modified-Since: Mon, 10 Feb 2014 18:08:48 GMT
If-None-Match: "a54907f38b306fe3ae4f32c003ddd507"

上面两行是由 Safari 根据已经存在于 cache 中的请求结果生成。Safari 告诉服务器只接收自2月10号发生改变或者 ETag 不等于 a54907f38b306fe3ae4f32c003ddd507的数据。

User-Agent 标头告诉服务器发送请求的客户端信息。

一个简单的响应 A Simple Response

与请求相对的,服务器的响应像这样:

HTTP/1.1 304 Not Modified
Connection: keep-alive
Date: Mon, 03 Mar 2014 21:09:45 GMT
Cache-Control: max-age=3600
ETag: "a54907f38b306fe3ae4f32c003ddd507"
Last-Modified: Mon, 10 Feb 2014 18:08:48 GMT
Age: 6
X-Cache: Hit from cloudfront
Via: 1.1 eb67cb25620df959ba21a943fbc49ef6.cloudfront.net (CloudFront)
X-Amz-Cf-Id: dDSBgR86EKBemW6el-pBI9kAnuYJEaPQYEqGmBnilD12CbixCuZYVQ==

第一行被称为 状态行( status line )。包含 HTTP 版本,紧接着 状态码( status code )和状态信息。

HTTP 定义了一系列状态码和它们的含义。这里我们接收的状态码是 304, 表示我们请求的资源并没有修改。

响应没有包含任何响应体信息,它只是告诉接收者,你的版本是最新的。

缓存已关闭 Caching Turned Off

让我们通过 curl 做另外一个请求:
% curl http://www.apple.com/hotnews/ > /dev/null

curl 不使用本地缓存,整个请求像这样:

GET /hotnews/ HTTP/1.1
User-Agent: curl/7.30.0
Host: www.apple.com
Accept: */*

这和 Safari 的请求很相似,这里没有 If-None-Match 标头,服务器将返回响应的文档。

注意 curl 是如何表示接收任何媒体格式:(Accept:*/*)。

来自 www.apple.com 的响应会像这样:

HTTP/1.1 200 OK
Server: Apache
Content-Type: text/html; charset=UTF-8
Cache-Control: max-age=424
Expires: Mon, 03 Mar 2014 21:57:55 GMT
Date: Mon, 03 Mar 2014 21:50:51 GMT
Content-Length: 12342
Connection: keep-alive




    

它包含一个含有 HTML 文档的响应体。

来自 Apple 服务器的响应包含的状态码为 200, 即 HTTP 请求成功的标准响应。

Apple 服务器同样表示响应的媒体类型为 text/html;charset=UTF-8Content-Length: 12342表示响应体的大小。

HTTPS —— HTTP Secure

传输层安全协议 Transport Layer Security是运行在 TCP 之上的加密协议,它允许两件事:

  • 两端都可以验证另一端的身份。
  • 两端发送的数据都是被加密的。

在 TLS 之上使用 HTTP 可以提供 HTTP Secure,或简单点, HTTPS。

TLS 1.2

如果你的服务器支持 TLS, 则应将 TLSMinimumSupportedProtocol 设置为 kTLSProtocol12 以要求 TLS 最低版本为 1.2。这将使 中间人攻击 (man in the middle attacks) 更加困难。

证书锁定 Certificate Pinning

如果我们不能确定正在交流的另一端是我们认为的那样。服务器证书可以告诉我们服务器是谁,只允许连接到一个特定的证书被称为 证书锁定 certificate pinning

当客户端通过 TLS 连接到服务器时,操作系统将决定服务器的证书是否有效。有几种方式可以绕开它,最显著的方式就是将证书安装在 iOS 设备上,并将其标记为受信任的。一旦这样做了,针对你的 app 选择中间人攻击已显得不那么重要了。

为了防止这种情况发生(或者至少使他变得难以实施),我们可以使用一种证书锁定的方法,当 TLS 连接建立后,我们不仅检查证书是否有效,并且检查证书是否是我们的服务器所具有的。这只有在连接到我们自己的服务器时才有作用,因此我们可以通过对 app 的更新来协调对服务器证书的更新。

为了做到这些,我们需要在连接期间做 受信的服务器 server trust 检查,当一个 URLSession 创建连接时,代理接收一个 URLSession:didReceiveChallenge:completionHandler: 调用,传递的 NSURLAuthenticationChallenge 对象有一个 protectionSpace 属性,是一个 NSURLProtectionSpace 的实例。同样,protectionSpace 拥有 serverTrust 属性。

serverTrust 是一个 SecTrustRef 对象。安全框架有各种方法来查询 SecTrustRef 对象。AFNetworking 的 AFSecurityPolicy 是一个不错的起点。和往常一样,当你自己构建安全相关的代码时,需要找人仔细检查。你不想出现在你的代码中出现 goto fail bug。

大杂烩 Put the Pieces Together

目前我们已经了解所有的这些片段( IP,TCP,HTTP )是如何工作的,这里还有几件是我们可以做到的。

高效地使用连接

TCP 连接有两个方面的问题:初始设置,以及最后连接间传送的报文段。

配置

连接设置可以是非常耗时的。正如之前提到的,TCP 连接需要三次握手,并没有大量的数据需要来回发送。但是,特别是在移动网络上,数据包从一台主机(一台 iPhone),发送到另一台主机(服务器)可以轻易在 250 ms —— 1/4 秒的时间。对于三次握手,我们通常在发送任何有效载荷之前,耗费 750 ms 来建立连接。

对于 HTTPS,事情会更佳戏剧化。由于 HTTPS 是 HTTP 运行在 TLS 之上,同样是运行在 TCP 之上。 TCP 连接仍将做它的三次握手,接下来 TLS 层做它的三次握手。大致来说,在发送任何数据之前,HTTPS 连接因此占用正常 HTTP 连接两倍的时间。

如果 HTTP 往返时间(round-trip time)是 500 ms(端到端250 ms),HTTPS 需要再加上 1.5 s。

设置是很耗时的,无论连接是否传输的是很少或者很大量的数据。

当将报文段传到一个未知条件的网络时,TCP 需要探测网络以确定可用容量。换句话说,TCP 需要一段时间才能确定通过网络发送数据的速度。只有知道这一点,它才能以最佳速度发送数据。这种算法被称为慢启动。需要指出的是,慢启动算法在数据链路层传输质量比较差的网络上表现不佳,无线网络通常是这样的。

Tear Down

另一个问题出现在数据传输结束的时候。当我们对资源进行 HTTP 请求时,服务器将持续发送报文段到我们的主机,主机在收到数据后返回 ACK 信息。如果一个按照这种方式传输的数据包丢失,服务器将不会接收到这个数据包对应的 ACK,服务器发现包丢了并做所谓的快速重发 fast retransmit。

丢包发生后,接收方会返回与上一次返回相同的 ACK,接收方因此收到两次相同的 ACK。有几种网络条件会引起即使没有丢包也会接收重复 ACK 的问题,因此发送方仅在收到三个重复的 ACK 时执行快速重传。

这样做的问题是当传输结束时,发送者停止发送报文段,接收者停止返回 ACK。在发送最后四个报文段的时候,快速重传算法没有办法检测是否丢包。在典型的网络中,这相当于 5.7 kB 的数据。如果在这 5.7 kB 发生丢包,TCP 需要回退到更有“耐心”的算法去检测丢包,在这种情况下重传需要几秒并不罕见。

保活和流通 Keep-Alive and Pipelining

HTTP 有两种策略去应对这些问题。最简单的是 HTTP persistent connection,有时被称为 keep-alive。在一个请求-响应完成后,HTTP 会简单地复用相同的 TCP 连接。在 HTTPS 中,相同的 TLS 连接会被复用:

open connection
client sends HTTP request 1 ->
                            <- server sends HTTP response 1
client sends HTTP request 2 ->
                            <- server sends HTTP response 2
client sends HTTP request 3 ->
                            <- server sends HTTP response 3
close connection

第二步是使用 HTTP Pipeling,即允许客户端通过想用的连接发送多条请求,而不用等待之前请求的响应。发送请求可以和接收响应并行进行,不过响应仍然按照之前发送请求的次序返回给客户端 —— FIFO原则。

稍微简化一点像这样:

open connection
client sends HTTP request 1 ->
client sends HTTP request 2 ->
client sends HTTP request 3 ->
client sends HTTP request 4 ->
                            <- server sends HTTP response 1
                            <- server sends HTTP response 2
                            <- server sends HTTP response 3
                            <- server sends HTTP response 4
close connection

主要注意的是,服务器可以在任何时候发送响应,而不必等所有的请求都收到之后发送。

这样我们能够以更有效的方式使用 TCP。我们现在只是从一开始进行握手,并且由于我们使用的是相同连接,TCP 可以更好地利用有效的带宽,TCP 拥塞控制算法也能做得更好。最终如上所述,快速重传的问题只影响整个连接的最后四个报文段,而不影响每个请求-响应的最后四个报文段。

使用 HTTP pipelining 对于提升高延迟连接的性能可能会非常显著 —— 如在我们的 iPhone 上使用,而不是 WiFi。实际上有一些研究表明在移动网络下使用 SPDY 相较 HTTP pipelining 并没有额外性能的提升。

当与服务器通信时,RFC 2616 建议在 HTTP pipelining可用时使用两条连接。根据这些建议操作,将会得到最佳的响应时间,并避免拥塞。

杯具的是,目前仍有很多服务器不支持 pipelining。你应该尝试启用它,并检查特定的服务器是否支持它。NSURLSession 默认是关闭 HTTP pipelining 的。确认设置 NSURLSessionConfigurationHTTPShouldUsePipelining 属性为 YES 如果你能够使用 pipelining 。

Timeout

我们都在缓慢的网络下使用过 app 。很多 app 在 15 s 之后会停止请求,这是一种非常差的 app 设计。给予用户反馈可能会更好:“你的网络出现了一些问题,可能需要等待更长时间。”但是只要有连接,即使网络环境很差,TCP 仍能确保请求和响应都能到达另一端,即使这需要耗费一段时间。

或者看看另一种方式:如果我们处在很差的网络环境中,请求-响应往返时间需要17 s 完成。假如 app 在 15 s 的时候停止请求,这时候即使用户是有耐心的也无法去执行他想要的操作。如果用户处在缓慢的网络环境,他知道操作需要耗费一段时间(我们可以发送通知栏提醒),如果用户有足够耐心等待,我们不应该阻碍他继续使用 app。

有一种误解是重启 HTTP 连接将修复这种问题,实际并不是这样。TCP 将会重新发送那些需要重发的数据包。

正确的做法是这样:在我们发送一个 URL 请求时,我们设置一个 10 s 的定时器。当我们接收到响应时,停止定时器。如果定时器在我们接收到响应之前触发,我们可以在 UI 上提示:“您目前所处的网络可能较慢,请耐心等待。”,根据对应的 app ,我们可能希望给用户一个取消操作的选项,而不是我们为用户直接作出决定。

只要两端保留着相同的 IP 地址,连接相会持续“存活”。在一台 iPhone 上,当你从 WiFi 网络转换到 3G 时将使连接断开( IP 地址发生改变),因为另一端不能继续将数据包路由到用于创建连接的 IP 地址。

缓存 Caching

记得在我们第一个例子中:

If-None-Match: "a54907f38b306fe3ae4f32c003ddd507"

我们发送这样一行到服务器,告诉它我们本地已经有这样的资源,希望服务器如果有新的版本发给我们。如果你正在跟服务器建立自己通信,尝试利用这种机制。如果使用得当,这将显著加速通信效率。这种机制被称为 HTTP ETag。

甚至更佳极端,记住:“最快的请求是没有请求。( The fastest request is the one never made. )”。当我们发送请求到服务器,即使在一个很理想的网络环境,请求少量的数据,非常快速的服务器,你也不可能在 50 ms 内获取响应。这只是一个请求。如果有种方式可以让你在本地创建数据少于 50ms,那就不要做那些请求。

当你认为数据可以有效的保持一段时间可以尝试本地缓存资源。检查 Expires 报头或者依赖 NSURLSession 的缓存机制 —— NSURLRequestUseProtocolCachePolicy

总结

使用 NSURLSession 发送 HTTP 请求非常方便。但是创建这一请求牵涉到很多到技术。知道这些步骤可以方便你优化 HTTP 请求,我们希望 app 能在各种网络环境下表现良好。理解 IP ,TCP ,和 HTTP 如何工作能让更佳容易实现我们的期望。

你可能感兴趣的:(IP, TCP 和 HTTP(二))