计网学习笔记应用层篇1-HTTP

计算机网络

Posted by Cc1over on June 9, 2019

计网学习笔记应用层篇1-HTTP

前言

最近快考试了,本来早就想好好写一份计网和机组的学习笔记,之前前前后后经历的项目的开发还有各种比较水科目的考试,现在终于有时间写了,心情很激动,主要想以QA的形式,除了记录下一下知识点外,还记录下现阶段的思考历程……

HTTP/1.x

消息格式

请求报文

  • 请求方式:
    • GET:输入信息通过request行的URL字段上传
    • POST:在请求消息的消息体中上传客户端的输入
    • HEAD:请Service不要将请求对象放入
    • PUT:将消息体中的文件上传到URL字段所指定的路径(HTTP/1.1的PUT方法自身不带验证机制,任何人都可以上传文件,存在安全问题,一般不用)
    • DELETE:删除URL字段所指示的文件
    • OPTIONS:询问支持的方法,用来查询针对请求URI指定资源支持的方法(客户端询问服务器可以提交哪些请求方法)
    • TRACE:追踪路径
    • CONNECT:要求用隧道协议连接代理
  • URL基本组成:

    scheme://host:port/path?query#fragment

    • scheme://host:port
    • 可选部分:
      • /path 路径
      • ?query=xx 查询参数
      • #fragment 锚点 (表示文档的某个部分,可以让浏览器定位到锚点,不会发送到HTTP请求中,只对浏览器起作用)

[Q1:老生常谈问题之GET和POST请求区别?]

  • GET方法用于信息获取,它是安全的(安全:指非修改信息,如数据库方面的信息),而POST方法是用于修改服务器上资源的请求
  • GET请求的数据会附在URL之后,而POST方法提交的数据则放置在HTTP报文实体的主体里,所以POST方法的安全性比GET方法要高
  • GET方法传输的数据量一般限制在2KB,其原因在于:GET是通过URL提交数据,而URL本身对于数据没有限制,但是不同的浏览器对于URL是有限制的,比如IE浏览器对于URL的限制为2KB,而Chrome,FireFox浏览器理论上对于URL是没有限制的,它真正的限制取决于操作系统本身;POST方法对于数据大小是无限制的,真正影响到数据大小的是服务器处理程序的能力

[Q2:HEAD请求方式有什么应用场景咧?]

用于确认URI的有效性及资源更新的日期时间等,具体来说:

  • 判断类型
  • 查看响应中的状态码,看对象是否存在(响应:请求执行成功了,但无数据返回)
  • 测试资源是否被修改过

响应报文

  • 状态码与描述:http://tool.oschina.net/commons?type=5

连接管理

短连接

  • 每一个 HTTP 请求都由它自己独立的连接完成
  • 每一个 HTTP 请求之前都会有一次 TCP 握手
  • HTTP/1.0 没有指定 Connection协议头,或者是值被设置为close采取这个模型
  • HTTP/1.1 只有当 Connection被设置为close时才采取这个模型

长连接

  • 连接会保持一段时间,重复用于发送一系列请求,节省了新建 TCP 连接握手的时间
  • 连接在空闲一段时间后会被关闭(服务器可以使用Keep-Alive协议头来指定一个最小的连接保持时间)

[Q:长连接和HTTP Keep-Alive有什么区别?]

二者的用处并不同。Http协议在完成一个请求后,服务器会自动关闭连接。这时,可以在请求里带上一个Keep Alive给服务器,告诉服务器不要立即关闭连接,我还想继续复用这条连接;而对TCP协议层而言,是不会自动断开的,但这也带来了一个问题,万一由于某些外部原因导致连接断开,那我如何知道连接已失效呢?TCP会在2个小时间隔后,自动发送一个Keep Alive数据包给服务端,探测一下服务器是否还在响应。它的功能类似心跳包,只是间隔太长,不适合做真正的心跳包

[Q:长连接什么时候会断开?]

  • 长连接所在进程被杀

    这个很容易理解,如果我们的App切换到后台,那么系统随时可能将我们的App杀掉,这时长连接自然也就随之断开

  • 用户切换网络

    比如手机网络断开,或者发生Wi-Fi和蜂窝数据切换,这时会导致手机IP地址变更。而我们知道,TCP连接是基于IP + Port的,一旦IP变更,TCP连接自然也就失效了,或者说长连接也就相当于断开了

  • 系统休眠等导致NAT超时

    这里对NAT简单解释下,方便有的同学不太了解。当手机连接上网络时,网关会给我们分配一个IP地址,这个其实是内网IP,此时还未真正连接上公网,也连接不上服务器;如果想要连接公网,需要运营商将我们的内网IP映射成一个公网IP,有了公网IP,服务器就能与我们建立连接了。NAT指的就是这个映射过程

    也就是说,运营商会给每台设备分配一个公网IP,类似一张通信证。不过,随着连接网络的设备不断增多,网关负载也会不断加大,这时,运营商就会对一些不太活跃的设备进行公网IP回收了,如果下次这个设备需要连网,那就重新分配一个IP即可

    看似没问题,但实际上,如果我们的App在一段时间不活跃,发生了NAT超时,便会导致我们的公网IP失效,长连接也随之失效了

  • DHCP 租期

    DHCP 租期过期,如果没有及时续约,同样会导致IP地址失效。综合而言,长连接在正常情况下是不会断开的,但是,一旦手机的IP地址失效,这时就不得不重新建立连接了

[Q:如何建立稳定的长连接?]

  • 长连接独立进程

    将长连接逻辑单独提取到了一个独立的进程里。这个进程只做网络交互,消耗的内存等资源自然较少,从而减少了被系统回收的概率

  • 长连接进程复活

    进程被杀难以避免,不过可以通过AlarmReceiver、 ConnectReceiver、BootReceiver,达到进程的及时唤醒

  • 心跳机制

    对于心跳包很多人误以为只是用来定期告诉服务端我们的状态,实际并非如此

    上面我们提到了 NAT 超时,即如果App一段时间内不活跃,会导致运营商那里删除我们的公网IP映射关系,这会导致我们的TCP长连接断开。因此,我们需要通过心跳机制来保证App的活跃度,防止发生 NAT 超时

  • 断开重连

    • 创建Receiver,监控网络状态,如果网络发生切换则立即重连
    • 监控服务端心跳包回包,如果连续5次没有收到回包,则认为长连接已经失效
    • 设置心跳包超时限制,如果超过时间还没有收到心跳回包,则重连,这种方式比较耗电
    • 等socket IO异常抛出,不过耗时太长,需要15s左右才能发现

流水线

  • 默认情况下,HTTP请求是按顺序发出的。下一个请求只有在当前请求收到应答过后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间
  • 流水线是在同一条长连接上发出连续的请求,而不用等待应答返回。这样可以避免连接延迟。理论上讲,性能还会因为两个 HTTP 请求有可能被打包到一个 TCP 消息包中而得到提升。就算 HTTP 请求不断的继续,尺寸会增加,但设置 TCP 的MSS选项,仍然足够包含一系列简单的请求
  • 注意事项:并不是所有类型的HTTP请求都能用到流水线:只有幂等方式才能用到HTTP流水线,比如 GET、HEAD、PUT、DELETE能够被安全的重试:如果有故障发生时,流水线的内容要能被轻易的重试

[Q3:什么是幂等方式?]

  • 如果在使服务器处于相同状态的情况下,可以在具有相同效果的行中一次或多次执行相同的请求,则HTTP方法是幂等的,等方法不应该有任何副作用
  • 实际例子:

    1)GET / pageX HTTP / 1.1是幂等的。 连续多次调用,客户端获得相同的结果

       GET /pageX HTTP/1.1   
       GET /pageX HTTP/1.1   
       GET /pageX HTTP/1.1   
       GET /pageX HTTP/1.1

​ 2)POST / add_row HTTP / 1.1不是幂等的; 如果它被多次调用,它会添加几行

       POST /add_row HTTP/1.1
       POST /add_row HTTP/1.1   -> Adds a 2nd row
       POST /add_row HTTP/1.1   -> Adds a 3rd row

​ 3)DELETE / idX / delete HTTP / 1.1是幂等的,即使返回的状态代码可能在请求之间发生变化

       DELETE /idX/delete HTTP/1.1   -> Returns 200 if idX exists
       DELETE /idX/delete HTTP/1.1   -> Returns 404 as it just got deleted
       DELETE /idX/delete HTTP/1.1   -> Returns 404

域名分片

  • HTTP/1.x 的连接,请求时序列化的,哪怕本来是无序的,在没有足够庞大可用的带宽时,也无从优化。一个解决方案是,浏览器为每个域名建立多个连接,以实现并发请求。曾经默认的连接数量为 2 到 3 个,现在比较常用的并发连接数已经增加到 6 条,再多就有可能会引起Dos攻击
  • 服务器端想要更快速的响应网站或应用程序的应答,它可以迫使客户端建立更多的连接。例如,不要在同一个域名下获取所有资源,假设有个域名是 www1.example.com,我们可以把它拆分成好几个域名:www2.example.com、www3.example.com、www4.example.com。所有这些域名都指向同一台服务器,浏览器会同时为每个域名建立 6 条连接,这一技术就是域名分片

[Q3:浏览器对同一 Host 建立 TCP 连接到数量有没有限制?]

  • 那个时候没有多路传输,当浏览器拿到一个有几十张图片的网页该怎么办呢?肯定不能只开一个 TCP 连接顺序下载,那样用户肯定等的很难受,但是如果每个图片都开一个 TCP 连接发 HTTP 请求,那电脑或者服务器都可能受不了
  • 但是如果1000张图片就建立1000条TCP连接,显然是不科学的,因为每条TCP连接都需要经历慢启动的过程,而且对于NAT来说,这也是难以接受的
  • 所有浏览器对同一Host建立的TCP连接数量有所限制,chorme是6,这也解释了为什么上面的域名分片技术有存在的意义

HTTP/2

在开始探索HTTP/2技术之前,首先思考一个问题

[Q4:HTTP/1.x 存在什么问题?]

  • HTTP/1.x 在使用时,header里携带的内容过大,而且大部分时候几乎header的内容都是相同的,增加了传输的成本,在移动端增加用户流量
  • HTTP/1.x 虽然支持了keep-alive,来减少多次创建连接产生的延迟,但是keep-alive 使用多了也会给服务端带来大量的性能压力,并且对于单个文件被不断请求的服务,因为文件被请求之后还保持了不必要的连接时间,keep-alive可能会极大的影响服务器的性能
  • 虽然HTTP/1.x可以让客户端向服务器并行发送多个请求,而且服务器也可以并行处理多个请求,但是HTTP/1.x 有严格的串行返回响应机制,通过 TCP 连接返回响应时,必须一个接一个,前一个响应没有完成,下一个响应就不能返回,如果第一个响应时间很长,那么后面的响应处理完了也无法发送,只能被缓存起来,占用服务器内存
  • HTTP/1.x 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下

HTTP/2的新特性

  • 二进制分帧层
  • 请求/响应的复用
  • 优先级和依赖性
  • 首部压缩
  • 服务器推送

下文会分点概述这些新的特性,不过在了解原理之前,先来看看HTTP/2是如何解决HTTP/1.x中存再的问题的:

  • header压缩:  HTTP/2使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小

  • 请求/响应的复用:HTTP/2使用在同一连接上 进行多个并发交换,请求和响应同时发送接受,然后再拼装组合,不阻塞;一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面

  • 优先级和依赖性:可以请求的时候告知服务器端,资源分配权重,优先加载重要资源

二进制分帧层

二进制分帧层是位于套接字接口与应用可见的高级 HTTP API 之间一个经过优化的新编码机制,HTTP 的语义(包括各种动词、方法、标头)都不受影响,不同的是传输期间对它们的编码方式变了;HTTP/1.x 协议以换行符作为纯文本的分隔符,而 HTTP/2 将所有传输的信息分割为更小的消息和帧,并采用二进制格式对它们编码,但是这样就会导致HTTP/1.x 客户端无法理解只支持 HTTP/2 的服务器

数据流、消息和帧

二进制分帧层中有一些新引入的概念:

  • 数据流:已建立的连接内的双向字节流,可以承载一条或多条消息
  • 消息:与逻辑请求或响应消息对应的完整的一系列帧
  • 帧:HTTP/2 通信的最小单位,每个帧都包含帧头,会标识出当前帧所属的数据流

由上面这些概念便形成了一个连接的拓扑解构

  • 连接与数据流的关系便是仅与一个对等节点建立一个连接,并在该连接上传输多个流。因为流可以交织,所以可以同时快速的传输多个流
  • 消息中包含了多个帧,而由于在对等节点上重建这些帧时,它们形成一个完整的请求或响应,所以一个请求或响应只能映射到一个可识别的流
  • 帧是通信的基本单位,每个帧有一个标头,其中包含帧的长度和类型、一些布尔标志、一个保留位和一个流标识符

  • LENGTH:记录帧的大小,它最多可在一个 DATA 帧中携带 224 个字节(约 16 MB),但默认的最大值设置为 214 个字节 (16 KB)。帧大小可以通过协商调得更高一点
  • TYPE:标识帧的用途
    • HEADERS:帧仅包含 HTTP 标头信息
    • DATA:帧包含消息的所有或部分有效负载
    • PUSH_PROMISE:通知一个将资源推送到客户端的意图
    • PRIORITY:指定分配给流的重要性
    • RST_STREAM:错误通知:一个推送承诺遭到拒绝,终止流
    • SETTINGS:指定连接配置
    • PING:检测信号和往返时间。
    • GOAWAY:停止为当前连接生成流的停止通知。
    • WINDOW_UPDATE:用于管理流的流控制。
    • CONTINUATION:用于延续某个标头碎片序列
    • 具体文档:https://tools.ietf.org/html/rfc7540#section-11.2
  • FLAGS:一个布尔值,指定帧的状态信息
    • DATA帧可定义两个布尔标志:END_STREAMPADDED,前者表示数据流结束,后者表示存在填充数据。
    • HEADERS帧可以将相同的标志指定为 DATA帧,并添加两个额外的标志:END_HEADERSPRIORITY,前者表示标头帧结束,后者表示设置了流优先级
    • PUSH_PROMISE 帧可以设置 END_HEADERSPADDED标志

请求/响应的复用

从上面数据流、消息和帧以及TCP连接的关系可以看到HTTP/2基于二进制分帧层的结构为每帧分配一个流标识符,然后在一个 TCP 连接上独立发送它们,这样就可以实现完全双向的请求和响应消息复用

如上图所示,服务器可以在分别在数据流1、3中交错发送一系列帧,而客户端此时正在数据流5中发送一个Data帧,因此,一个连接上同时有三个并行数据流

以这种交错的形式发送帧的好处:

  • 所有请求和响应都在一个套接字上发生
  • 所有响应或请求都无法相互阻塞
  • 减少了延迟

HTTP/1.x映射到HTTP/2

示例1:HTTP 请求映射到右侧的一个 HEADERS帧

在 HEADERS帧中,设置了两个标志。第一个是END_STREAM,它设置为 true(由加号表示),表明该帧是给定请求的最后一帧。END_HEADERS`标志也设置为 true,表明该帧是流中最后一个包含标头信息的帧

示例2:HTTP 响应映射到右侧的一个 HEADERS帧和DATA帧

在 HEADERS 帧中,END_STREAM 表明该帧不是流中的最后一帧,而 END_HEADER 表明它是最后一个包含标头信息的帧。在 DATA帧中,END_STREAM 表明它是最后一帧

首部压缩

如图中所示:两次请求的请求头的信息大多是相同的,除了标记的黄色部分不一样,HTTP/2的解决办法就是在客户端和服务器各自保存一份表,然后在经历了request1之后再发request3只需要差量发送不同的信息就可以了

[Q5:那用什么样的数据结构维护这个表效率最高?]

哈夫曼树,因为在头部字段的使用上,其实是会存在优先级的,而且用哈夫曼树去维护,在传输过程中只需要传输相应的哈夫曼编码就可以达到优化的效果了

优先级和依赖性

将 HTTP 消息分解为很多独立的帧之后,我们就可以复用多个数据流中的帧,客户端和服务器交错发送和传输这些帧的顺序就成为关键的性能决定因素。 为了做到这一点,HTTP/2 标准允许每个数据流都有一个关联的权重和依赖关系:

  • 可以向每个数据流分配一个介于 1 至 256 之间的整数
  • 每个数据流与其他数据流之间可以存在显式依赖关系

数据流依赖关系和权重的组合让客户端可以构建和传递优先级树,表明它倾向于如何接收响应。 反过来,服务器可以使用此信息通过控制 CPU、内存和其他资源的分配设定数据流处理的优先级,在资源数据可用之后,带宽分配可以确保将高优先级响应以最优方式传输至客户端

HTTP/2 内的数据流依赖关系通过将另一个数据流的唯一标识符作为父项引用进行声明;如果忽略标识符,相应数据流将依赖于“根数据流”。 声明数据流依赖关系指出,应尽可能先向父数据流分配资源,然后再向其依赖项分配资源。 换句话说,请先处理和传输响应 D,然后再处理和传输响应 C。

共享相同父项的数据流(即,同级数据流)应按其权重比例分配资源。 例如,如果数据流 A 的权重为 12,其同级数据流 B 的权重为 4,那么要确定每个数据流应接收的资源比例,请执行以下操作:

  • 将所有权重求和:4 + 12 = 16
  • 将每个数据流权重除以总权重:A = 12/16, B = 4/16

因此,数据流 A 应获得四分之三的可用资源,数据流 B 应获得四分之一的可用资源;数据流 B 获得的资源是数据流 A 所获资源的三分之一。

根据上图中的几个操作示例。 从左到右依次为:

  • 数据流 A 和数据流 B 都没有指定父依赖项,依赖于显式“根数据流”;A 的权重为 12,B 的权重为 4。因此,根据比例权重:数据流 B 获得的资源是 A 所获资源的三分之一。
  • 数据流 D 依赖于根数据流;C 依赖于 D。 因此,D 应先于 C 获得完整资源分配。 权重不重要,因为 C 的依赖关系拥有更高的优先级。
  • 数据流 D 应先于 C 获得完整资源分配;C 应先于 A 和 B 获得完整资源分配;数据流 B 获得的资源是 A 所获资源的三分之一。
  • 数据流 D 应先于 E 和 C 获得完整资源分配;E 和 C 应先于 A 和 B 获得相同的资源分配;A 和 B 应基于其权重获得比例分配。

[Q6:为什么HTTP/2总速度快?]

  • HTTP/1.1最多可以建立起6条TCP连接并发传输数据,而HTTP/2总速度更快
  • 从TCP连接的角度来说,6条连接就意味着多次建立连接,SSL握手等,而1条连接只需要一次就行了,对于普通请求而言建立连接和握手是很耗时的,而且6条TCP连接就要经历6次TCP慢启动的过程

参考资料