我们开始学习了应用层的协议,从HTTP协议开始
我们首先拿一个网站门户来看
假如登录了http://www.163.com,前面整体是一个URL,统一资源定位符,HTTP称为协议,www.163.com 是一个域名,表明了互联网的一个地址,我们URL有着非常详细的位置标注,例如http://www.163.com/index.html ,因为整体是统一的,有协议的,浏览器才知道如何去处理
HTTP请求的准备过程
浏览器会将www.163.com 这个域名发给DNS服务器,让其解释为IP地址,具体的流程就先不管,接下来我们HTTP基于了TCP协议,建立了TCP连接,怎么建立呢?就是所谓的三次握手,然后当前HTTP协议大部分都是1.1的,在1.1之中,开启了keep-alive的,建立了TCP连接,可以在多次请求中复用
HTTP请求的构建
我们要通过浏览器发送HTTP的请求
我们的报文分为了三个部分,第一部分是请求行,然后是首部,最后是正文部分
(1).请求行,我们的请求的HTTP版本为1.1,这里说的第一个,就是方法,方法能有的类型有
GET,去服务器获取一些资源,对于访问网页来说,可能获取的是一个网页,对于很多API,获取的可能是一个JSON字符串,比如我们有一个HTTP的API,我们在获取的时候,就会使用GET方法获取这个API,这个API可能返回JSON字符串,里面是一个列表,然后遍历获取对应数据
(2).然后是POST,这就是主动告知服务器的一些信息,而不是获取一些数据,告知的信息可以多种多样,一般是JSON,比如我们创建一个云服务器,就是通过了POST方法,将需要创建的信息放在一个JSON中,然后通过POST方法告诉服务器端
(3).或者使用PUT,指向指定的资源位置上传最新的内容,但是一般很少HTTP服务器可以上传文件,于是和POST变成了给服务器给予数据的方法
实际使用过程中,两者是有些区别的,POST是创建一个资源,PUT是修改一个资源
(4).还有就是DELETE,就是删除资源的
2.首部
首部是key:value的信息,保存一些HTTP相关的字段
例如Accept-Charset,表示客户端可以接受的字符集
Content-Type,表示正文的格式,比如我们使用POST请求,如果正文是JSON,就应该将这个值设置为json
然后首部里面还有缓存相关的信息,缓存常用于服务我们页面的静态资源
比如我们去浏览一个商品,商品的介绍一般不会改变,但是库存需要实时刷新,于是我们的更新就要选择性的去更新数据
在nginx中,对于静态资源,有Vanish缓存层,当缓存过期的时候,才去请求动态资源
在首部,我们的Cache-control是控制缓存的,当客户端发送的请求中包含了max-age指令的时,如果判定缓存层中资源的缓存时间比指定时间小,就可以接受缓存的资源,当指定max-age值为0,缓存层就要将请求转发给应用集群
If-Modified-Since也是关于缓存的,也就是,如果服务器的资源在某个时间之后更新了,客户端应该更新资源,但是如果在时间后没有更新,服务端返回一个304 Not Modified的响应
那么,我们就拼凑了Http的请求报文,那么浏览器会将其交给传输层,然后发送,其中的Socket的调用,会由浏览器管理,而非自己管理
HTTP请求的发送
我们进行了HTTP请求的发送,而HTTP是基于TCP的,于是面向连接去发送,通过了stream流的方式发送给了对方,到了TCP层,将二进制变为了一个个报文段给了服务器
然后每发送一个报文段,都需要对方一个ACK回应,保证报文可靠的到达了对方,如果没有回应,那么TCP会在这一层多次重试,这是TCP做的,然后TCP加上了目标地址和本地址,交给IP层传输,IP层利用ARP协议,找到MAC地址,然后发送出去
网关收到了包发现了MAC符合,取出了IP地址,根据路由协议找到了下一跳的路由器,获取下一跳的MAC地址,将包发给下一跳路由,这样包到了目标的服务器,然后解析TCP的头,发现序列号是不是我要的,如果是,就放入缓存中,然后返回一个ACK,不是就丢弃
TCP头里面有对应的端口号,然后会知道HTTP的服务器的应用监控的也是这个端口号,于是利用这个端口号将包发给了HTTP服务器,HTTP服务器的进城解析后,返回了对应的数据
HTTP返回包的构建
返回的报文是有一定格式,如果基于HTTP1.1的
主要就是状态码,这个会表示HTTP返回的结果,200代表着成功,404代表着服务器无法响应这个请求,短语中表明了原因
然后是返回首部的key:value
其中有一个key:Retry-After,表明客户端应该多久后尝试
Content-Type表示返回的类型,是否是HTML还是JSON
构造好了返回的HTTP报文,原路返回回去
然后客户端接受到了,从MAC层到IP层到TCP层,到HTTP层,发现返回的是200,就是正常的,然后将HTML拿出来,解析成一个网页
HTTP 2.0的问题
当然HTTP协议在不断的进化,现在已经有了HTTP2.0
HTTP1.0的通信是纯文本的,每次通信都要用完整的HTTP的头,而且不考虑pipeline的时候,就是每次发包收包都是一来一回的,在实时性和并发性都有问题的
现在的HTTP2.0会对HTTP头会进行一定的压缩,每次传递都会传输的大量的头,都变成了索引表中的索引
HTTP2.0将一个TCP的连接中请求,切分为多个流,每个流都有自己的ID,而且流可以是客户端发往服务端,也可以是服务端发往客户端
并且将传输的信息分为更小的消息和帧,并且进行二进制的编码,常见的有Header帧,传输Header内容,并且开启一个新的流,就是Data帧,传输正文实体,多个Data帧属于一个流
这样我们,可以将多个请求分到不同的流中,然后将请求内容拆成帧,进行二进制传输,这些帧可以打散乱序发送,然后根据每个帧的首部的流标识符重新组装
我们发送三个独立的请求,一个获取CSS,一个获取JS,一个获取图片,使用1,1就是串行的,使用2.0就是在一个连接中,同时发送多个
2.0将三个请求变成了三个流,将数据分成了帧,乱序的发送到一个TCP连接中
HTTP2.0成功解决了HTTP1.1的队首阻塞问题,同时不需要通过HTTP1.0的pipeline机制,来同步发送,而是将多个数据通过一个数据连接同时传输,加快了传输效率
然后就是QUIC协议
可能是未来的发展趋势,因为TCP本身的数据传输就是有顺序的,这使得即使使用了HTTP2.0,通过多个stream,让逻辑上一个TCP能够并行发送,但是中间并没有关联的数据,一前一后的发送,也会因为一个阻塞而导致后面的阻塞
于是我们可以采用更为简单的方式,UDP的连接方式发送,就是Google的QUIC协议
这个QUIC的协议,是具有自己的独特机制的
一:连接机制
因为TCP是由四元标识符标识的,分别是源IP,源端口,目的IP,目的端口,当一个元素发生了变化的时候,就需要断开重连,这样肯定不适合手机端的连接,因为手机端的连接不稳定,而且会出现WIFI和移动网络的切换,于是基于UDP可以解决这些问题,使用一个64位的随机数作为ID,这样IP或者端口变化的时候,只要ID不变,就可以不重新连接
二:重传机制
TCP为了保证可靠性,通过序号和应答机制,来解决了顺序问题和丢包问题
任何一个序号的包发过去,都需要在一定的时间内获得应答,不然就重发,这就是自适应重传算法,这个超市是通过RTT采样往返时间调整的
那么,TCP的计算时间是否准确呢?
加入我们发送了一个包,序号为100,发现过期后没有返回,只能在发送一个,然后知道返回了一个包,但是多次发送了100这个包,我们如何知道具体的RTT呢?
如果是按照返回的包是第一次发送的,就会把时间算长了,如果按照第二个发送的,就会算短了
于是QUIC进行了改进,在QUIC中,有一个序号同样的,但是每个序号只会被使用一次,一个包的序号是100,如果发现超时,再次发送就是101的序号,这样返回的包可以直接对应到了,RTT的计算爷更加准确
但是有一个问题,如何知道包100和包101发送的内容保证一样的呢?QUIC有一个offset的概念,QUIC是面向连接的,就像是TCP一样,是一个数据流,可以查看数据发送到了那个部分了,只要这个offset的包没有发过,就重发一次
机制三,无阻塞的多路复用
我们利用自定义的连接和重传机制,来解决上面的多路复用,而且一个QUIC可以创建多个straem流,发送多个HTTP请求,但是QUIC是基于UDP的,一个连接上面的多个stream之间没有依赖,加入stream2丢了一个udp的包,也不会影响到后面的stream3
机制四:自定义流量控制
TCP的流量控制是通过滑动窗口协议,QUIC也是通过window_update,告诉对端可以接收的字节数,但是QUIC的窗口是适应自己的多路复用机制的,不但在一个连接上控制窗口,还在一个连接中的每个stream控制了窗口
TCP协议中,接收端的窗口的起始点是下一个要接受并发送ACK的包,后面的包都到了,放在了缓存中,窗口也不能右移,因为TCP的ACK是基于序列号的累计应答,一旦ACK了一个序列号,就说明前面的都到了,但是前面的没到,后面的到了也不能直接ACK,因为要等一批,导致可能出现的超时重传问题
QUIC的ACK基于了offset,每个offset的包来了,进了缓存,就可以进行应答,应答后发送方就不会重发了,中间的空档没来,会自然超时后重发的,窗口的起始位置就是当前收到的最大offset,从这个offset到当前stream能够容纳的最大缓存,就是真正的窗口大小
本章小结
Http常用的几种方法,比如GET POST PUT DELETE的几个方法,以及重要的首部字段
HTTP2.0的改进,增加了多路复用和头压缩和分帧和二进制编码来提升了性能
QUIC协议通过基于UDP的方式,来定义了类似TCP的连接,重试,多路复用,流量控制,来进一步提升性能
课后思考
1.QUIC是一个精巧的协议,所以包含的机制肯定很多,能说一下嘛?
2.这一节讲了如何基于HTTP浏览网页,那么传输像是支付的信息,怎么办呢?
1.虽然QUIC很厉害,但是碍于设备的发展和老基站的不支持,仍然不能普及
具体点来说,QUIC还有其他的属性,一个是快速建立连接,一个是拥塞控制,QUIC协议默认使用了TCP协议的CUBIC 拥塞控制算法,
TCP的拥塞控制算法,是每当收到一个ACK的时候,都需要调整拥塞窗口的大小,对于RTT比较小的,窗口生长速度快
对于很多跨地区长的传输,RTT比较长,因为RTT的窗口调整策略,不太公平,因为,窗口增长慢,导致很多时候没涨到带宽的上限,数据就发完了,因而巨大的带宽就已经浪费掉了
CUBIC进行了不同的设计,窗口的增长函数取决于两次拥塞事件的时间间隔值,窗口增长独立于网络的RTT
CUBIC的窗口大小和变化的过程如图所示
丢包事件发生的时候,CUBIC会记录此时的拥塞窗口的大小,作为Wmax,接着,CUBIC会通过某个因子执行拥塞窗口的乘法减小,然后,沿着立方函数进行窗口的恢复
所以,恢复的速度是很快的,后来便从快速灰度的阶段进入了避免拥塞的阶段,当接近Wmax的时候,增长的速度变慢,直到在Wmax的时候达到了稳定点,增长速度为0,之后平稳的慢慢增长,探索新的最大窗口
keepalive模式如何理解,什么时候断开的,如何判断的呢?
在没有keepalive的模式下,每个HTTP都需要建立一个TCP的连接,并且使用一次就断开这个连接,使用了keepalive之后,再一次TCP的连接中,可以维护多份数据而不断开连接,减少TCP的连接建立次数,从而减少TIME_WAIT状态
长时间的TCP连接容易导致系统的资源无效占用,因为需要设置合理的keepalive timeout的时间,当HTTP产生的TCP连接在传输完最后一个响应后,需要等待keepalive timeout秒后,才能关闭这个连接
http1.0的队首阻塞,对于一个tcp连接,所有的1,0都放在队列中,进行串行化的发送
1.1的队首阻塞,允许一次性的发送多个http1.1的请求,不必等到前一个相应的收到,就能发送一下个,但是对于服务端来说,先接受的请求要先发送,导致请求处理时间越长,响应速度越慢
1.2利用一个tcp上,有多个stream,每个stream各自发送接受http请求,互不阻塞来解决了阻塞问题