准备
- 浏览器将域名发送给DNS服务器,解析成IP地址
- HTTP是基于TCP协议的,目前使用的HTTP协议版本大部分都是1.1
- HTTP 1.1默认开启了Keep Alive机制,这样建立的TCP连接,可以在多次请求中复用
HTTP 1.1
请求构建
HTTP请求的报文分为三大部分:请求行、首部、实体
1 | GET / HTTP/1.1 |
方法
方法 | 用途 |
---|---|
GET | 获取资源 |
POST | 创建资源 |
PUT | 修改资源 |
DELETE | 删除资源 |
首部字段
- 首部是Key-Value,通过冒号分隔
- Accept-Charset:表示客户端可以接受的字符集
- Content-Type:正文的格式
缓存
高并发系统中,在真正的业务逻辑之前,都需要有个接入层(Nginx),将这些静态资源的请求拦截在最外面
- 对于静态资源,有Varnish缓存层,当缓存过期的时候,才会真正访问Tomcat应用集群
- Cache-Control
- 当客户端发送的请求中包含max-age指令时
- 如果判定缓存层中,资源的缓存时间数值比指定的max-age小,那么客户端可以接受缓存的资源
- 当指定max-age值为0,那么缓存层需要将请求转发给应用集群
- 当客户端发送的请求中包含max-age指令时
- If-Modified-Since
- 如果服务器的资源在某个时间之后更新了,那么客户端就应该下载最新的资源
- 如果没有更新,服务端会返回
304 Not Modified
的响应,那么客户端无需下载,节省带宽
请求发送
- HTTP协议是基于TCP协议的,使用面向连接的方式发送请求,通过stream二进制流的方式传给对方
- 到了TCP层,会把二进制流变成一个个的报文段发送给服务器
- 发送的每个报文段,都需要对方返回一个ACK,来保证报文可靠地到达了对方
- 如果没有返回ACK,TCP层会进行重传,保证可达
- 同一个包有可能被传了很多次,HTTP层是无感知的,只是TCP层在埋头苦干
- TCP发送每个报文段时,都需要加上源地址和目标地址,并将这两个信息放到IP头,交给IP层进行传输
- IP层检查目标地址与自己是否在同一个局域网
- 如果是,通过ARP协议来请求这个目标地址对应的Mac地址,然后将源Mac地址和目标Mac地址放入Mac头,发送出去
- 如果不是,需要发送到网关,还是需要发送ARP协议来获取网关的MAC地址
- 网关收到包后发现MAC地址符合,取出目标IP地址,根据路由协议找到下一跳路由器的MAC地址,发送出去
- 路由器一跳一跳终于到达了目标局域网,最后一跳的路由器发现目标IP地址在自己的某个出口的局域网上
- 在这个局域网上发送ARP协议,获得目标IP地址的MAC地址,将包发出去
- 目标机器发现MAC地址符合,将包收起来,发现IP地址符合,解析IP头,发现是TCP协议
- 解析TCP头,里面有序号,检查该序号是否需要,如果需要就放入缓存中,然后返回一个ACK,不需要就丢弃
- TCP头里有端口号,HTTP服务器正在监听该端口号,目标机器将该包发送给HTTP服务器处理
响应构建
HTTP响应的报文分为三大部分:状态行、首部、实体
1 | HTTP/1.1 200 OK |
- 状态行会返回HTTP请求的结果
- 首部的Content-Type,表示返回正文的类型
响应返回
- 构造好HTTP返回报文,交给TCP层,让TCP层将返回的HTML分成一个个报文段,并保证每个报文段都可靠到达
- 这些报文段加上TCP头后会交给IP层,然后把刚才请求发送的过程反向走一遍
- 客户端发现MAC地址和IP地址符合,就会上交给TCP层处理
- TCP层根据序号判断是否是自己需要的,如果是,就会根据TCP头中的端口号,发送给相应的进程(浏览器)
- 浏览器作为客户端会监听某个端口,当浏览器拿到HTTP报文后,会进行渲染
HTTP 2.0
- HTTP 1.1在应用层以纯文本的形式进行通信,每次通信都要带完整的HTTP头部,这样在实时性和并发性上都会存在问题
- 为了解决这些问题,HTTP 2.0会对HTTP的头部进行一定的压缩
- 将原来每次都要携带的大量Key-Value在两端都建立一个索引表,对相同的Key-Value只发送索引表中的索引
- 流 + 帧
- HTTP 2.0协议将一个TCP连接切分成多个流
- 每个流都有ID标识
- 流是双向的,可以是客户端发往服务端,也可以是服务端发往客户端
- 流只是一个虚拟的通道
- 流具有优先级
- HTTP 2.0协议将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码
- 常用的帧有Header帧和Data帧
- Header帧:用户传输Header,并且会开启一个新的流
- Data帧:用来传输Body,多个Data帧属于同一个流
- 通过流+帧这两种机制
- HTTP 2.0的客户端可以将多个请求分到不同的流中,然后将请求内容拆分成帧,进行二进制传输
- 帧可以打散乱序发送,然后根据每个帧首部的流标识符重新组装,并且根据优先级,决定优先处理哪个流的数据
- HTTP 2.0协议将一个TCP连接切分成多个流
实例
- 假设一个页面要发送三个独立的请求,分别获取css、js和jpg,如果使用HTTP 1.1就是串行的
- 如果使用HTTP 2.0,可以在一个TCP连接里客户端和服务端都可以同时发送多个请求
- HTTP 2.0其实是将三个请求变成三个流,将数据分成帧,乱序发送到同一个TCP连接中
- HTTP 2.0成功解决了HTTP 1.1的队首阻塞问题
- HTTP 1.1:只能严格串行地返回响应,不允许一个TCP连接上的多个响应数据交错到达
- HTTP 2.0采用流+帧的机制来实现并行请求和响应
- HTTP 1.1:需要借助Pipeline机制(多条TCP连接)
- HTTP 2.0减少了TCP连接数对服务器性能的影响,同时将页面的多个HTTP请求通过一个TCP连接进行传输,加快页面渲染


QUIC
- HTTP 2.0虽然大大增加了并发性,但依然是基于TCP协议,而TCP协议处理包时是有严格顺序的
- 当其中一个数据包出现问题,TCP连接需要等待这个数据包完成重传后才能继续进行
- HTTP 2.0通过流+帧的机制实现了逻辑上的并行,但实际还是会受限于TCP协议
- 样例:一前一后(序号),前面Stream 2的帧没有收到,后面Stream 1的帧也会因此阻塞
- 即多个Stream之间是有依赖关系的
- 基于这个背景催生了Google的QUIC协议(Quick UDP Internet Connections),应用场景:Gmail
- 可参考HTTP-over-QUIC to be renamed HTTP/3
- QUIC协议通过基于UDP来自定义类似TCP的连接、重传、多路复用、流量控制技术,进一步提高性能
自定义连接机制
- 标识TCP连接:<源IP,源端口,目标IP、目标端口>,一旦一个元素发生变化,就需要断开重连(三次握手,有一定时延)
- 基于UDP的QUIC协议,不再以四元组标识一个连接,而是采用64位的随机数(ID)
- UDP是无连接的,当IP或者端口变化时,只要ID不变,就不需要重新建立连接
自定义重传机制
- TCP为了保证可靠性,通过序号和应答机制,来解决顺序问题和丢包问题
- 任何一个序号的包发出去,都要在一定的时间内得到应答,否则一旦超时,就会重发该序号的包
- 超时时间是通过采样往返时间RTT不断调整的(即自适应重传算法),但存在采样不准确的问题
- 样例:发送一个序号为100的包,发现没有返回,于是重传,然后收到ACK101,此时怎么计算RTT?
- QUIC也有序列号,是递增的,任何一个序列号的包只发送一次
- 发送一个包,序号为100,发现没有返回,于是重传,序号变成了101
- 如果收到ACK100,就是对第1个包的响应,如果收到ACK101,就是对第2个包的响应,因此RTT的计算相对准确
- 如何知道包100和包101发送的内容是一样的,QUIC定义了offset的概念
- QUIC是面向连接的,跟TCP一样,是一个数据流,发送的数据在这个数据流里面是有偏移量offset的

无阻塞的多路复用
- 有了自定义的连接和重传机制,就可以解决HTTP 2.0的多路复用问题(阻塞)
- 与HTTP 2.0一样,同一条QUIC连接上可以创建多个Stream,来发送多个HTTP请求
- QUIC是基于UDP的,一个QUIC连接上的多个Stream之间是没有依赖关系的
- 假如前面Stream 2丢了一个UDP包,后面跟着Stream 3的一个UDP包
- 虽然Stream 2丢失的那个UDP需要重传,但Stream 3的UDP包可以直接发送给用户,不会被阻塞
自定义流量控制
- TCP的流量控制是通过滑动窗口协议
- QUIC的流量控制也是通过window_update,来告诉发送端它可以接受的字节数
- QUIC的窗口是适应自己的多路复用机制的:不仅在一个连接上控制窗口,还在每个Stream上控制窗口
- 在TCP协议中,接收端窗口的起始点:下一个要接收并且ACK的包
- TCP的ACK机制是基于序列号的累计应答:哪怕后面的包先到,并且已经放在缓存里,窗口也不能右移
- 这样就会导致后面的包虽然到了,但由于不能ACK,也有可能超时重传,浪费带宽
- QUIC的ACK是基于offset的
- 每个offset的包来了,放进了缓存,就可以ACK了,ACK后对应的包就不会重发,中间的空档会等待到来或者重发即可
- 窗口的起始位置为当前收到的最大offset,从该offset到当前Stream所能容纳的最大缓存,为真正的窗口大小
- 显然,这种方式更加准确
- 而整个连接的窗口,需要统计所有的Stream的窗口
