gitbook/趣谈网络协议/docs/9410.md
2022-09-03 22:05:03 +08:00

19 KiB
Raw Permalink Blame History

第14讲 | HTTP协议看个新闻原来这么麻烦

前面讲述完**传输层**,接下来开始讲**应用层**的协议。从哪里开始讲呢就从咱们最常用的HTTP协议开始。

HTTP协议几乎是每个人上网用的第一个协议同时也是很容易被人忽略的协议。

既然说看新闻,咱们就先登录 http://www.163.com

http://www.163.com 是个URL叫作统一资源定位符。之所以叫统一是因为它是有格式的。HTTP称为协议www.163.com是一个域名表示互联网上的一个位置。有的URL会有更详细的位置标识例如 http://www.163.com/index.html 。正是因为这个东西是统一的,所以当你把这样一个字符串输入到浏览器的框里的时候,浏览器才知道如何进行统一处理。

HTTP请求的准备

浏览器会将www.163.com这个域名发送给DNS服务器让它解析为IP地址。有关DNS的过程其实非常复杂这个在后面专门介绍DNS的时候我会详细描述这里我们先不管反正它会被解析成为IP地址。那接下来是发送HTTP请求吗

不是的HTTP是基于TCP协议的当然是要先建立TCP连接了怎么建立呢还记得第11节讲过的三次握手吗

目前使用的HTTP协议大部分都是1.1。在1.1的协议里面默认是开启了Keep-Alive的这样建立的TCP连接就可以在多次请求中复用。

学习了TCP之后你应该知道TCP的三次握手和四次挥手还是挺费劲的。如果好不容易建立了连接然后就做了一点儿事情就结束了有点儿浪费人力和物力。

HTTP请求的构建

建立了连接以后浏览器就要发送HTTP的请求。

请求的格式就像这样。

HTTP的报文大概分为三大部分。第一部分是请求行,第二部分是请求的首部,第三部分才是请求的正文实体

第一部分:请求行

在请求行中URL就是 http://www.163.com 版本为HTTP 1.1。这里要说一下的,就是方法。方法有几种类型。

对于访问网页来讲,最常用的类型就是GET。顾名思义GET就是去服务器获取一些资源。对于访问网页来讲要获取的资源往往是一个页面。其实也有很多其他的格式比如说返回一个JSON字符串到底要返回什么是由服务器端的实现决定的。

例如在云计算中如果我们的服务器端要提供一个基于HTTP协议的API获取所有云主机的列表这就会使用GET方法得到返回的可能是一个JSON字符串。字符串里面是一个列表列表里面是一项的云主机的信息。

另外一种类型叫做POST。它需要主动告诉服务端一些信息而非获取。要告诉服务端什么呢一般会放在正文里面。正文可以有各种各样的格式。常见的格式也是JSON。

例如我们下一节要讲的支付场景客户端就需要把“我是谁我要支付多少我要买啥”告诉服务器这就需要通过POST方法。

再如在云计算里如果我们的服务器端要提供一个基于HTTP协议的创建云主机的API也会用到POST方法。这个时候往往需要将“我要创建多大的云主机多少CPU多少内存多大硬盘”这些信息放在JSON字符串里面通过POST的方法告诉服务器端。

还有一种类型叫PUT就是向指定资源位置上传最新内容。但是HTTP的服务器往往是不允许上传文件的所以PUT和POST就都变成了要传给服务器东西的方法。

在实际使用过程中这两者还会有稍许的区别。POST往往是用来创建一个资源的而PUT往往是用来修改一个资源的。

例如云主机已经创建好了我想对这个云主机打一个标签说明这个云主机是生产环境的另外一个云主机是测试环境的。那怎么修改这个标签呢往往就是用PUT方法。

再有一种常见的就是DELETE。这个顾名思义就是用来删除资源的。例如我们要删除一个云主机就会调用DELETE方法。

第二部分:首部字段

请求行下面就是我们的首部字段。首部是key value通过冒号分隔。这里面往往保存了一些非常重要的字段。

例如,Accept-Charset,表示客户端可以接受的字符集。防止传过来的是另外的字符集,从而导致出现乱码。

再如,Content-Type是指正文的格式。例如我们进行POST的请求如果正文是JSON那么我们就应该将这个值设置为JSON。

这里需要重点说一下的就是缓存。为啥要使用缓存呢?那是因为一个非常大的页面有很多东西。

例如,我浏览一个商品的详情,里面有这个商品的价格、库存、展示图片、使用手册等等。商品的展示图片会保持较长时间不变,而库存会根据用户购买的情况经常改变。如果图片非常大,而库存数非常小,如果我们每次要更新数据的时候都要刷新整个页面,对于服务器的压力就会很大。

对于这种高并发场景下的系统,在真正的业务逻辑之前,都需要有个接入层,将这些静态资源的请求拦在最外面。

这个架构的图就像这样。

其中DNS、CDN我在后面的章节会讲。和这一节关系比较大的就是Nginx这一层它如何处理HTTP协议呢对于静态资源有Vanish缓存层。当缓存过期的时候才会访问真正的Tomcat应用集群。

在HTTP头里面Cache-control是用来控制缓存的。当客户端发送的请求中包含max-age指令时如果判定缓存层中资源的缓存时间数值比指定时间的数值小那么客户端可以接受缓存的资源当指定max-age值为0那么缓存层通常需要将请求转发给应用集群。

另外,If-Modified-Since也是一个关于缓存的。也就是说如果服务器的资源在某个时间之后更新了那么客户端就应该下载最新的资源如果没有更新服务端会返回“304 Not Modified”的响应那客户端就不用下载了也会节省带宽。

到此为止我们仅仅是拼凑起了HTTP请求的报文格式接下来浏览器会把它交给下一层传输层。怎么交给传输层呢其实也无非是用Socket这些东西只不过用的浏览器里这些程序不需要你自己写有人已经帮你写好了。

HTTP请求的发送

HTTP协议是基于TCP协议的所以它使用面向连接的方式发送请求通过stream二进制流的方式传给对方。当然到了TCP层它会把二进制流变成一个个报文段发送给服务器。

在发送给每个报文段的时候都需要对方有一个回应ACK来保证报文可靠地到达了对方。如果没有回应那么TCP这一层会进行重新传输直到可以到达。同一个包有可能被传了好多次但是HTTP这一层不需要知道这一点因为是TCP这一层在埋头苦干。

TCP层发送每一个报文的时候都需要加上自己的地址即源地址和它想要去的地方即目标地址将这两个信息放到IP头里面交给IP层进行传输。

IP层需要查看目标地址和自己是否是在同一个局域网。如果是就发送ARP协议来请求这个目标地址对应的MAC地址然后将源MAC和目标MAC放入MAC头发送出去即可如果不在同一个局域网就需要发送到网关还要需要发送ARP协议来获取网关的MAC地址然后将源MAC和网关MAC放入MAC头发送出去。

网关收到包发现MAC符合取出目标IP地址根据路由协议找到下一跳的路由器获取下一跳路由器的MAC地址将包发给下一跳路由器。

这样路由器一跳一跳终于到达目标的局域网。这个时候最后一跳的路由器能够发现目标地址就在自己的某一个出口的局域网上。于是在这个局域网上发送ARP获得这个目标地址的MAC地址将包发出去。

目标的机器发现MAC地址符合就将包收起来发现IP地址符合根据IP头中协议项知道自己上一层是TCP协议于是解析TCP的头里面有序列号需要看一看这个序列包是不是我要的如果是就放入缓存中然后返回一个ACK如果不是就丢弃。

TCP头里面还有端口号HTTP的服务器正在监听这个端口号。于是目标机器自然知道是HTTP服务器这个进程想要这个包于是将包发给HTTP服务器。HTTP服务器的进程看到原来这个请求是要访问一个网页于是就把这个网页发给客户端。

HTTP返回的构建

HTTP的返回报文也是有一定格式的。这也是基于HTTP 1.1的。

状态码会反映HTTP请求的结果。“200”意味着大吉大利而我们最不想见的就是“404”也就是“服务端无法响应这个请求”。然后短语会大概说一下原因。

接下来是返回首部的key value

这里面,Retry-After表示告诉客户端应该在多长时间以后再次尝试一下。“503错误”是说“服务暂时不再和这个值配合使用”。

在返回的头部里面也会有Content-Type表示返回的是HTML还是JSON。

构造好了返回的HTTP报文接下来就是把这个报文发送出去。还是交给Socket去发送还是交给TCP层让TCP层将返回的HTML也分成一个个小的段并且保证每个段都可靠到达。

这些段加上TCP头后会交给IP层然后把刚才的发送过程反向走一遍。虽然两次不一定走相同的路径但是逻辑过程是一样的一直到达客户端。

客户端发现MAC地址符合、IP地址符合于是就会交给TCP层。根据序列号看是不是自己要的报文段如果是则会根据TCP头中的端口号发给相应的进程。这个进程就是浏览器浏览器作为客户端也在监听某个端口。

当浏览器拿到了HTTP的报文。发现返回“200”一切正常于是就从正文中将HTML拿出来。HTML是一个标准的网页格式。浏览器只要根据这个格式展示出一个绚丽多彩的网页。

这就是一个正常的HTTP请求和返回的完整过程。

HTTP 2.0

当然HTTP协议也在不断的进化过程中在HTTP1.1基础上便有了HTTP 2.0。

HTTP 1.1在应用层以纯文本的形式进行通信。每次通信都要带完整的HTTP的头而且不考虑pipeline模式的话每次的过程总是像上面描述的那样一去一回。这样在实时性、并发性上都存在问题。

为了解决这些问题HTTP 2.0会对HTTP的头进行一定的压缩将原来每次都要携带的大量key value在两端建立一个索引表对相同的头只发送索引表中的索引。

另外HTTP 2.0协议将一个TCP的连接中切分成多个流每个流都有自己的ID而且流可以是客户端发往服务端也可以是服务端发往客户端。它其实只是一个虚拟的通道。流是有优先级的。

HTTP 2.0还将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码。常见的帧有Header帧用于传输Header内容并且会开启一个新的流。再就是Data帧用来传输正文实体。多个Data帧属于同一个流。

通过这两种机制HTTP 2.0的客户端可以将多个请求分到不同的流中,然后将请求内容拆成帧,进行二进制传输。这些帧可以打散乱序发送, 然后根据每个帧首部的流标识符重新组装,并且可以根据优先级,决定优先处理哪个流的数据。

我们来举一个例子。

假设我们的一个页面要发送三个独立的请求一个获取css一个获取js一个获取图片jpg。如果使用HTTP 1.1就是串行的但是如果使用HTTP 2.0,就可以在一个连接里,客户端和服务端都可以同时发送多个请求或回应,而且不用按照顺序一对一对应。

HTTP 2.0其实是将三个请求变成三个流将数据分成帧乱序发送到一个TCP连接中。

HTTP 2.0成功解决了HTTP 1.1的队首阻塞问题同时也不需要通过HTTP 1.x的pipeline机制用多条TCP连接来实现并行请求与响应减少了TCP连接数对服务器性能的影响同时将页面的多个数据css、js、 jpg等通过一个数据链接进行传输能够加快页面组件的传输速度。

QUIC协议的“城会玩”

HTTP 2.0虽然大大增加了并发性但还是有问题的。因为HTTP 2.0也是基于TCP协议的TCP协议在处理包时是有严格顺序的。

当其中一个数据包遇到问题TCP连接需要等待这个包完成重传之后才能继续进行。虽然HTTP 2.0通过多个stream使得逻辑上一个TCP连接上的并行内容进行多路数据的传输然而这中间并没有关联的数据。一前一后前面stream 2的帧没有收到后面stream 1的帧也会因此阻塞。

于是就又到了从TCP切换到UDP进行“城会玩”的时候了。这就是Google的QUIC协议接下来我们来看它是如何“城会玩”的。

机制一:自定义连接机制

我们都知道一条TCP连接是由四元组标识的分别是源 IP、源端口、目的 IP、目的端口。一旦一个元素发生变化时就需要断开重连重新连接。在移动互联情况下当手机信号不稳定或者在WIFI和 移动网络切换时,都会导致重连,从而进行再次的三次握手,导致一定的时延。

这在TCP是没有办法的但是基于UDP就可以在QUIC自己的逻辑里面维护连接的机制不再以四元组标识而是以一个64位的随机数作为ID来标识而且UDP是无连接的所以当IP或者端口变化的时候只要ID不变就不需要重新建立连接。

机制二:自定义重传机制

前面我们讲过TCP为了保证可靠性通过使用序号应答机制,来解决顺序问题和丢包问题。

任何一个序号的包发过去,都要在一定的时间内得到应答,否则一旦超时,就会重发这个序号的包。那怎么样才算超时呢?还记得我们提过的自适应重传算法吗?这个超时是通过采样往返时间RTT不断调整的。

其实在TCP里面超时的采样存在不准确的问题。例如发送一个包序号为100发现没有返回于是再发送一个100过一阵返回一个ACK101。这个时候客户端知道这个包肯定收到了但是往返时间是多少呢是ACK到达的时间减去后一个100发送的时间还是减去前一个100发送的时间呢事实是第一种算法把时间算短了第二种算法把时间算长了。

QUIC也有个序列号是递增的。任何一个序列号的包只发送一次下次就要加一了。例如发送一个包序号是100发现没有返回再次发送的时候序号就是101了如果返回的ACK 100就是对第一个包的响应。如果返回ACK 101就是对第二个包的响应RTT计算相对准确。

但是这里有一个问题就是怎么知道包100和包101发送的是同样的内容呢QUIC定义了一个offset概念。QUIC既然是面向连接的也就像TCP一样是一个数据流发送的数据在这个数据流里面有个偏移量offset可以通过offset查看数据发送到了哪里这样只要这个offset的包没有来就要重发如果来了按照offset拼接还是能够拼成一个流。

机制三:无阻塞的多路复用

有了自定义的连接和重传机制我们就可以解决上面HTTP 2.0的多路复用问题。

同HTTP 2.0一样同一条QUIC连接上可以创建多个stream来发送多个 HTTP 请求。但是QUIC是基于UDP的一个连接上的多个stream之间没有依赖。这样假如stream2丢了一个UDP包后面跟着stream3的一个UDP包虽然stream2的那个包需要重传但是stream3的包无需等待就可以发给用户。

机制四:自定义流量控制

TCP的流量控制是通过滑动窗口协议。QUIC的流量控制也是通过window_update来告诉对端它可以接受的字节数。但是QUIC的窗口是适应自己的多路复用机制的不但在一个连接上控制窗口还在一个连接中的每个stream控制窗口。

还记得吗在TCP协议中接收端的窗口的起始点是下一个要接收并且ACK的包即便后来的包都到了放在缓存里面窗口也不能右移因为TCP的ACK机制是基于序列号的累计应答一旦ACK了一个序列号就说明前面的都到了所以只要前面的没到后面的到了也不能ACK就会导致后面的到了也有可能超时重传浪费带宽。

QUIC的ACK是基于offset的每个offset的包来了进了缓存就可以应答应答后就不会重发中间的空档会等待到来或者重发即可而窗口的起始位置为当前收到的最大offset从这个offset到当前的stream所能容纳的最大缓存是真正的窗口大小。显然这样更加准确。

另外还有整个连接的窗口需要对于所有的stream的窗口做一个统计。

小结

好了,今天就讲到这里,我们来总结一下:

  • HTTP协议虽然很常用也很复杂重点记住GET、POST、 PUT、DELETE这几个方法以及重要的首部字段

  • HTTP 2.0通过头压缩、分帧、二进制编码、多路复用等技术提升性能;

  • QUIC协议通过基于UDP自定义的类似TCP的连接、重试、多路复用、流量控制技术进一步提升性能。

接下来,给你留两个思考题吧。

  1. QUIC是一个精巧的协议所以它肯定不止今天我提到的四种机制你知道它还有哪些吗

  2. 这一节主要讲了如何基于HTTP浏览网页如果要传输比较敏感的银行卡信息该怎么办呢

欢迎你留言和我讨论。趣谈网络协议,我们下期见!