我爱编程

IP, TCP 和 HTTP(二)

2017-05-19  本文已影响36人  zac4j

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

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
<head>
    <meta charset="utf-8" />

它包含一个含有 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 如何工作能让更佳容易实现我们的期望。

上一篇 下一篇

猜你喜欢

热点阅读