HTTP的核心概念
HTTP tcp port 80
HTTPS tcp port 443 = HTTP + SSL (详见Cryptography)
HTTP over SSL, SLL特点:
用非对称密钥来加密并传递随机数
用随机数作为对称密钥来加密传递内容
除了HTTP存在于应用层之外,该协议还有5个特点。
1. HTTP的标准建立在将两台计算机视为不同的角色:客户端和服务器。客户端会向服务器传送不同的请求(request),而服务器会对应每个请求给出回应(response)。
2. HTTP属于无状态协议(Stateless)。这表示每一个请求之间是没有相关性的。在该协议的规则中服务器是不会记录任何客户端操作,每一次请求都是独立的。(记录用户浏览行为会通过其他技术实现)
3. 客户端的请求被定义在几个动词意义范围内。最长用到的是GET和POST,其他动词还包括DELETE, HEAD等等。
4. 服务器的回应被定义在几个状态码之间:
5开头表示服务器错误,(502 origin server not reachable)
4开头表示客户端错误, (404 page not found)
3开头表示需要做进一步处理,(redirection)
2开头表示成功,
1开头表示在请求被接受处理的同时提供的额外信息。
5. 不管是客户端的请求信息还是服务器的回应,双方都拥有一块头部信息(Header)。头部信息是自定义,其用途在于传递额外信息(浏览器信息、请求的内容类型、相应的语言)。
HTTP GET vs POST
1、GET请求是通过URL直接请求数据,数据信息可以在URL中直接看到,比如浏览器访问;而POST请求是放在请求头中的,我们是无法直接看到的;
2、GET提交有数据大小的限制,一般是不超过1024个字节,而这种说法也不完全准确,HTTP协议并没有设定URL字节长度的上限,而是浏览器做了些处理,所以长度依据浏览器的不同有所不同;POST请求在HTTP协议中也没有做说明,一般来说是没有设置限制的,但是实际上浏览器也有默认值。总体来说,少量的数据使用GET,大量的数据使用POST。
3、GET请求因为数据参数是暴露在URL中的,所以安全性比较低,比如密码是不能暴露的,就不能使用GET请求;POST请求中,请求参数信息是放在请求头的,所以安全性较高,可以使用。在实际中,涉及到登录操作的时候,尽量使用HTTPS请求,安全性更好。
http转发(fwd)是服务器行为,重定向(redirect)是客户端行为。为什么这样说呢,这就要看两个动作的工作流程:
转发过程:客户浏览器发送http请求----》web服务器接受此请求--》调用内部的一个方法在容器内部完成请求处理和转发动作----》将目标资源发送给客户;在这里,转发的路径必须是同一个web容器下的url,其不能转向到其他的web路径上去,中间传递的是自己的容器内的request。在客户浏览器路径栏显示的仍然是其第一次访问的路径,也就是说客户是感觉不到服务器做了转发的。转发行为是浏览器只做了一次访问请求。
重定向过程:客户浏览器发送http请求----》web服务器接受后发送302状态码响应及对应新的location给客户浏览器--》客户浏览器发现是302响应,则自动再发送一个新的http请求,请求url是新的location地址----》服务器根据此请求寻找资源并发送给客户。在这里location可以重定向到任意URL,既然是浏览器重新发出了请求,则就没有什么request传递的概念了。在客户浏览器路径栏显示的是其重定向的路径,客户可以观察到地址的变化的。重定向行为是浏览器做了至少两次的访问请求的。
解释二
重定向,其实是两次request,
第一次,客户端request A,服务器响应,并response回来,告诉浏览器,你应该去B。这个时候IE可以看到地址变了,而且历史的回退按钮也亮了。重定向可以访问自己web应用以外的资源。在重定向的过程中,传输的信息会被丢失。
TCP 连接容易在两个时点出现问题:初始设置,以及通过连接传输的最后一部分报文。
建立连接
连接设置可能会非常耗时。正如前文所说,TCP 建立连接的过程中需要进行三次握手。这个过程中本身没有太多的数据需要传递。但是,对于移动网络来说,从手机端向服务器端发送一个数据包普遍需要 250ms,也就是四分之一秒。推及到三次握手,也就是说在还没有传送任何数据之前,光建立连接就要花费 750ms。
HTTPS 的情况更夸张,由于 HTTPS 是基于 TLS 的 HTTP,而 HTTP 又基于 TCP。TCP 连接就要执行三次握手,然后到了 TLS 层还会再握手三次。估算一下,建立一个 HTTPS 连接的耗时至少是创建一个 HTTP 连接的两倍。如果 RTT 时间是 500ms(假设单程 250ms),HTTPS 建立连接累计总耗时将达1.5秒。
不管建立连接后是要传递多少数据,建立连接本身都太过耗时了。
另一个影响 TCP 连接的因素是传送大规模数据。如果要在网络情况未知的条件下传送报文,TCP 需要侦测当前网络的能力。换句话说,TCP 得花费一定的时间去计算此网络的最佳传输速率。上文提到过,TCP 需要逐步调整以便找到最佳速度。这种算法被称为 慢启动 (slow-start)。还有一点值得注意,慢启动策略在那些数据链路层传输质量较差的网络环境中的表现更差,无线网络就是典型的例子。
结束连接
另一个问题主要存在于数据传输的最后阶段。每当客户端发起 HTTP 请求某些资源的时候,服务器会持续的向客户端主机发送 TCP 报文数据,客户端收到数据后会给服务器反馈 ACK 确认信息。假如某个报文在传输过程中发生丢包,那么服务器也就不会收到该包的确认 ACK。一旦服务器发现有数据包没有 ACK 反馈,就会触发快速重传 (fast retransmit)。
每当某个数据包丢失,数据接收方在收到下个数据包后发出的确认 ACK 与所接收的前一个数据包的确认 ACK 相同。那么数据发送方自然就会收到重复的 ACK。除了报文丢失,还有很多种网络状况会导致重复 ACK 的问题。一般情况下,如果数据发送方连续收到 3 个重复的 ACK 就会立即进行快速重发。
这所导致的问题将发生在数据传输的收尾阶段。如果发送方完成数据发送,接受方自然会停止发送 ACK 确认。在最后四个报文传输的过程中,快速重发算法是没有办法处理这四个报文的数据包的丢失问题的(因为不会收到三个相同的确认 ACK,所以不能界定传输丢包)。在常规网络环境下,四个数据包相当于 5.7kB 的数据规模。总之,在这最后 5.7kB 的传输的过程中,快速重发机制是无效的。针对这种情况,TCP 会启用其他机制来侦测丢包问题。对于这种情况,重传操作可能要消耗几秒钟去执行,这并不奇怪。
长连接和管线化
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 管线 (pipelining) 处理,即允许客户端利用同样的连接并行发送多个请求,也就是说无需等待上一个请求的响应完成可以发下一个请求。这表示能同时处理请求和响应,请求处理的顺序采用先进先出原则,响应结果会按照请求发出的顺序依次返还给客户端。
稍微简化一下,看起来会是这样:
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 就可以最大限度的利用带宽。此种情况下,拥塞控制也会随之提升。因为快速重发机制无法处理的最末四个报文丢失情况只会发生在使用本连接的最后一个请求-响应中,而不是像之前那样每一个请求-响应都需要建立自己的连接,每个连接中都可能出现最后四个报文丢失的问题。
HTTP 管线化对高网络延迟连接的通讯性能提升尤为显著,在你的 iPhone 没有通过 Wi-Fi 访问网络的时候,此类网络连接就属于高延迟范畴。实际上,有调查显示,在移动网络环境下,SPDY 的通讯性能并不优于 HTTP 管线。
RFC 2616 指明,在与同一个服务器通讯的时候,如果启用了 HTTP 管线,建议启用两个连接。按照说明所述,这样能获得最优响应效率,能最大限度避免拥塞。增加更多的连接也不会再对性能有什么明显改善。
遗憾的是,还是有相当多的服务器不支持管线化。由于这个原因,HTTP 管线在 NSURLSession 中默认是关闭的。如果想要启用 HTTP 管线,需要将 NSURLSessionConfiguration 中的 HTTPShouldUsePipelining 设置为 YES。另外,建议服务器最好还是支持管线化。
我们都有在网络不太好的情况下使用 app 的经历。很多 app 大概 15 秒左右就会结束请求并且反馈一个超时信息。这种设计其实是很不友好的。应该给用户一个他们可以理解的友好提示,诸如“你好,现在网络状况不太好,您需要多等一会儿。”。但是即便网络状况不好,只要连接还在,TCP 都会保证将请求发出去并且会一直等待响应的返回,只是时间长短的问题。
从另一个角度来说:在较慢的网络中,请求-响应的RTT时间可能会有 17 秒。如果 15 秒就决定中止请求,就算用户有足够的耐心,他们也没机会等到想要的操作结果。反过来,如果我们给出用户相应的提示信息,而他们又刚好愿意多等一会,用户可能会更喜欢使用这样的应用。
一直以来都有一种误解,用重发请求来解决上面的问题。注意,这不是问题的关键,因为 TCP 有自己的重发机制。
正确的处理方式应该是:每当发起一个请求的时候,同时启动一个 10 秒计时器。如果请求在 10 秒之内返回,就把计时器停掉。如果超过 10 秒,可以给用户一个提示“网络不好,请稍后。”,我建议再给用户一个取消按钮,让他们可以自行选择等待还是取消请求,当然提示信息的具体内容和是否配备取消按钮,这个可以视乎各 app 的情况去决定。总而言之,开发者最好不要直接替用户做决定,比如直接中止他们的请求。
只要连接双方的 IP 地址是不变的、可用的,连接就一定会是“活跃”的。如果把 iPhone 从 Wi-Fi 连接切换到 3G 网络,这样连接就会变得不可用,因为手机的 IP 地址发生了变化,基于原 IP 地址创建的路由自然是失效的。
看看第一个例子中发送的这段 header 信息:
If-None-Match: "a54907f38b306fe3ae4f32c003ddd507"
这表示客户端本地已经针对所请求的资源做过缓存了,如果服务器上的资源有过更新,需要将最新的资源返回给客户端,否则不需要返回。如果自己构建客户端和服务器的数据通信,建议充分利用这个机制。这种机制叫做 HTTP ETag,如果使用得当,会对通讯的速度有明显的优化。
记住“最快的请求是不发请求”。举个极端的例子,拿一个请求来说,哪怕你有最好的网络,请求的数据量极小,有超快的服务器,你也不大可能在 50ms 内拿到请求的响应。这还只是一个请求。想想吧,如果有可能在本地创建相同的数据,而且耗时小于 50ms,那就不要发这样的请求。
针对已请求的资源,只要服务器上对应的资源具备在一定时间内不发生变化特性,建议在本地缓存起来。注意检查 header 中缓存过期的相关属性,也可以直接利用 NSURLSession 中的 NSURLRequestUseProtocolCachePolicy 策略。
利用 NSURLSession 发 HTTP 请求是非常简单便捷的。但是请求背后有很多技术点做支撑。只有知晓和理解其中的细节和内涵才能更好的去优化 HTTP 请求。用户期望的是我们的 app 时时刻刻都是好用的。只有深刻理解 IP,TCP 和 HTTP 的工作原理才能更好的去满足用户的期望。