一, Chrome Developer Network Tab
Cheome Developer 作为现在前端开发者最常用的开发调试工具, 其具有前端可以涉及到的各方面的强大功能, 为我们的开发和定位问题提供了极大地便利. 其中 Network Tab 是相当常用的一个功能板块. 通过它的 XHR,JS,CSS,Img 等子 Tab 我们可以捕获到所有基于应用层的 HTTP/HTTPS 协议的网络请求, 可以查看到该次请求和响应的所有头信息和内容.
Network Tab
展示了针对每一个 HTTP 请求的所有属性, 包括:
其中 Connection ID 为传输层 TCP 协议的连接 ID. 关于这点会在下一个章节提到.
Headers 主要展示了此次请求的状态, 还有请求和响应的头部信息, 头部信息是 HTTP 交互双方进行作业的依据:
Headers 中的大多数 Key 对于有经验开发者来说并不陌生, 不需要在这里介绍了. 但还是需要提到两个 key:
content-type 作为描述交互内容数据 MIME 格式的 key 意义相当重大, 我们在实际开发中发出请求缺接收到不到任何东西, 如果请求其他部分没问题的话, 很可能就是因为前后端的 content-type 不匹配的原因导致的.
referer 作为描述请求发起者所属域的 key, 也是非常有用的.
1. 通过它我们可以对网站进行访问量统计;
2. 可以对任何资源的访问做域的限制 (防盗链), 比如说: 我引用一个 QQ 空间的图片 URL 放到我自己 HTTP 服务器 serve 的网页的 < img /> 上, 当我访问该页面的时候并没有拿到这个图片, 取而代之的是一个访问受限制的站位图片. 也就是说 QQ 空间的服务器在接收到资源请求的时候, 是对 referer 做了检测的, 如果非 QQ 空间的页面发起的请求是无法正常获取到目标图片的. referer 本身是个错误的单词, 正确写法应该为 referrer, 译为介绍人, 描述了是在哪个域下进行请求资源或者跳转到某个 URL 的操作. 后来为了向下兼容 HTTP 协议, 这个错误的单词一直没有被修改.
需要注意的是: 当我们直接从浏览器地址栏访问某资源时, 此时 referer 为空, 因为此时并不存在有真正的介绍人, 这是一个凭空产生的请求, 并不是从其他任何地方链过去的.
Response 展示了服务端响应的内容, Preview 是根据 Headers 中的双方的 Content-Type 的 MIME 类型加工后的方便开发者浏览的带格式的数据内容:
Cookie 展示了在此次请求中浏览器 Headers 中所带 Cookie, 以及 HTTP 服务器端对浏览器端 Cookie 的设置:
Timing 整个请求从准备发出到结束的生命周期时序:
对于有经验的开发者来, 从 Headers,Preview 与 Response,Cookie 中能获取到相当有用的信息. 对于 Timing Tab, 它更接近底层, 展示了浏览器端发起一个 HTTP 请求的全过程, 按照 Chrome 官方解释, Timing 中各阶段描述如下:
1. Queuing(排队中)
如果一个请求排队, 则表明:
1)请求被渲染引擎推迟, 因为它被认为比关键资源 (如脚本 / 样式) 的优先级低. 这经常发生在 images(图像) 上.
2)这个请求被搁置, 在等待一个即将被释放的不可用的 TCP socket.
3)这个请求被搁置, 因为浏览器限制. 在 HTTP 1 协议中, 每个源上只能有 6 个 TCP 连接, 这个问题将在下一面的章节中提到.
4)正在生成磁盘缓存条目(通常非常快).
2.Stalled/Blocking (停止 / 阻塞)
发送请求之前等待的时间. 它可能因为进入队列的任何原因而被阻塞. 这个时间包括代理协商的时间.
3.Proxy Negotiation (代理协商)
与代理服务器连接协商花费的时间
4.DNS Lookup (DNS 查找)
执行 DNS 查找所用的时间. 页面上的每个新域都需要完整的往返 (roundtrip) 才能进行 DNS 查找. 当本地 DNS 缓存没有的时候, 这个时间可能是有一段长度的, 但是比如你一旦在 host 中设置了 DNS, 或者第二次访问, 由于浏览器的 DNS 缓存还在, 这个时间就为 0 了.
5.Initial Connection / Connecting (初始连接 / 连接)
建立连接所需的时间, 包括 TCP 握手 / 重试和协商 SSL.
6.SSL
完成 SSL 握手所用的时间, 如果是 HTTPS 的话
7.Request Sent / Sending (请求已发送 / 正在发送)
发出网络请求所花费的时间. 通常是几分之一毫秒.
8.Waiting (TTFB) (等待)
等待初始响应所花费的时间, 也称为 `Time To First Byte`(接收到第一个字节所花费的时间). 这个时间除了等待服务器传递响应所花费的时间之外, 还捕获到服务器发送数据的延迟时间. 这些情况可能会导致高 TTFB:1. 客户端和服务器之间的网络条件差; 2. 服务器端程序响应很慢.
9.Content Download / Downloading (内容下载 / 下载)
接收响应数据所花费的时间. 从接收到第一个字节开始, 到下载完最后一个字节结束.
通过对请求发出和响应的每个阶段的理解, 我们就能分析出当前 HTTP 请求存在的问题, 并据此解决问题.
二, 客户端与服务端通过 HTTP 协议的交互过程
在 HTTP 协议 RFC2616 http://www.ietf.org/rfc/rfc2616 的描述中, HTTP 作为应用层协议, 推荐并默认使用 TCP/IP 作为传输层协议, 且其他任何可靠的传输层协议也都可以被 HTTP 协议采用和使用. 也就是说假如 UDP 是 "可靠" 的, HTTP 也可以走在 UDP 上面. 目前市面上流行的浏览器的 HTTP 请求普遍遵守这个原则并采用 TCP/IP 作为传输层协议.
下面是捕获的一个对通过 XMLHttpRequest 对 https://localhost:3000/api/syncsystemstatus 发起的 HTTPS GET 请求:
在上个章节中有提到 Connection ID 是 TCP 连接的 ID, 表明了此次资源的请求是通过哪一个 TCP 连接完成的.
通常情况下我们使用 Fiddler,Charles 或者 Chome Developer 工具只能对 HTTP/HTTPS 请求抓包, 这里我们使用 WireShark 对更底层的协议连接进行封包抓取, 并分析上面所提到的这个连接从建立到结束的整个过程. WireShark 抓包截图如下:
说明: 由于笔者使用 webpack 的 dev-server 给 localhost:3000 做了正向代理, 并开启了 HTTPS, 由于服务器并未开启 HTTPS, 所以 dev-server 到服务器并不是 HTTPS 而是 HTTP1.1,192.168.11.94 就是 dev-server 的 IP, 可以将其看作 localhost:3000, 也就是客户端浏览器. 192.168.100.101 为 dev-server 正向代理到的目的地, 也是请求要发送到的 HTTP 服务器. 简单来讲该例子就是从浏览器 (192.168.11.14) 通过 XMLHttpRequest 对象发起了一个到服务器 (192.168.100.101) 的 HTTP1.1 请求.
客户端和服务器交互过程如下:
No.x 号为 WireShark 封包列表中最左侧的列, 记录每个封包在该次抓取中的编号, 并依次递增.
No.1: 浏览器 (192.168.11.94) 向服务器 (192.168.100.101) 发出连接请求, 并发送 SYN 包, 进入 SYN_SEND 状态, 等待服务器确认. 这是 TCP 三次握手的第一次.
No.2: 服务器 (192.168.100.101) 响应了浏览器 (192.168.11.94) 的请求, 确认浏览器的 SYN(ACK=J+1), 并且自己也发送 SYN 包也就是 SYN+ACK 包, 要求浏览器进行确认, 此时了服务器进入 SYN_RECV 状态. 这是 TCP 三次握手的第二次.
No.3: 浏览器 (192.168.11.94) 响应了服务器 (192.168.100.101) 的 SYN+ACK 包, 向服务器发送确认包 ACK(ACK=K+1), 此包发送完毕, 浏览器和服务器进入 ESTABLISHED 状态, 这是 TCP 三次握手的第三次, 握手完成, TCP 连接成功建立.
No.4: 浏览器 (192.168.11.94) 发出一个 HTTP 请求到服务器(192.168.100.101).
No.5: 服务器 (192.168.100.101) 收到浏览器 (192.168.11.94) 发出的请求, 并确认, 然后开始发送数据.
No.6: 服务器 (192.168.100.101) 发送状态响应码 200 到浏览器(192.168.11.94), 表示数据传输成功并且完毕, content-type 表明响应的内容文本需要被解析为 JSON 格式, OK 结束. 此时我们开发者通过判断 XHR 的 readyState 为 4 以及 status 为 200 就可以得到服务器完整的返回数据并应用在前端逻辑或页面展示上了.
对应第一章节中提到的 Chrome Developer Network 的请求时序图:
1. 发起第一个请求并完成连接的建立: No.1 至 No.4 对应时序图中的第 5 步至第 7 步. XHR 的 readyState 为 0-2, 初始化请求, 发送请求并建立连接,
2. 基于 TCP 连接的建立, 通过 HTTP 协议进行数据传输: No.5 对应时序图中的第 8 步至第 9 步, XHR 的 readyState 为 3, 正在交互中, 开始数据. 数据传输完毕后, readyState 为 4,status 为 200.
对于 Fetch 对象发起的请求也是如此的, 只不过 Fetch 基于 Promise 封装, readyState 和 status 可以理解为是内部控制的, 来决定 resolve 和 reject 的情况. 笔者的项目其实是使用 Fetch 的, 只是这里用 XMLHttpRequest 对象也就是 Ajax 来说明, 容易理解一些.
针对 No.1 至 No.3 的 TCP 的三次握手示意图:
SYN:Synchronize Sequence Numbers 同步序列编号.
SYN_SEND: 请求连接, 当你要访问其它的计算机的服务时首先要发个同步信号给该端口, 此时状态为 SYN_SENT, 如果连接成功了就变为 ESTABLISHED.
ACK:Acknowledgement 确认字符. 在数据通信中, 接收站发给发送站的一种传输类控制字符. 表示发来的数据已确认接收无误. 在 TCP/IP 协议中, 如果接收方成功的接收到数据, 那么会回复一个 ACK 数据. 通常 ACK 信号有自己固定的格式, 长度大小, 由接收方回复给发送方.
No.4 才是是 HTTP 的包, 这表明 HTTP 连接是基于 TCP 连接建立的.
三, 因前序请求阻塞而导致后续请求没法发起的问题
笔者目前开发的这个项目, 从底层 Go 的接口返回数据给 Node.js 层, Node.js 层再返回给前端界面. 在底层接口没优化的时候, 一个请求完成最少都要耗时 500ms, 平均都在 800ms 左右, 更有甚者达到了 1s 多. 前端是基于 React.j 的 SPA 应用,, 每个界面为了数据的准确性, 在进入界面后会立即请求数据, 并且后台还维持了一个每 15s 更新数据的 CronJob. 如果暴力的切换路由改变界面可以在短时间内创建大量的 HTTP 请求. 在 HTTP1.1 下的性能表现极为糟糕, 阻塞情况严重. HTTP1.1 默认只能同时创建 6 条 TCP 连接, 每条连接结束以后才能释放出来给对另外一个资源的请求来使用. 虽然和 HTTP1.0 相比, 在性能上已有较大提升, 但是并没有本质的改变. 以本项目为例, 如果当瞬间发起满 10 个请求后, 只有前 6 个请求能够分配 6 个不同的 TCP 连接进行处理, 后续 4 个请求只有等待这 6 个请求有任何一个释放 TCP 连接资源以后, 才能继续. 也就是说前 6 个请求中如果最少耗时都在 1s, 那么后 4 个请求的最少 Pending 时间都在 1s. 在笔者这位暴躁老哥的操作下, 这简直是噩梦:
可以发现阻塞现象相当严重, 而且每个 HTTP 请求会创建一个独立的 TCP 连接进行处理, 请求完成以后再关闭, 再为下一次请求创建一个新的 TCP 连接, 资源开销极大.
以 getsnapshot 这个接口为例, 在不阻塞的情况下, 其大致需要 84ms 来完成请求:
然而在发生阻塞后:
额... 好恐怖.
改用 HTTPS 后, 浏览器默认启用 HTTP2.0 协议:
在笔者的暴力操作下, 浏览器在短时间内发起大量的请求. 可以看到 ID 为 2693483 的这个 TCP 连接并发处理了的所有的资源请求, 并且一直保持 open 状态. 可见在 HTTP2.0 下相对于 HTTP1.X, 并发处理请求的数量和吞吐量都被提升到了一个完全不同的量级上. 极大节省了创建 TCP 连接的开销, 并且提升了对网络带宽资源的利用率. 有 HTTP2.0 多路复用功能的支持, 浏览器对大量的并发请求的处理顺畅多了.
好了到此结束吧.
HTTP1.0,HTTP1.1,HTTP2.0 之间还有很多的区别, 每个版本之间的变化也很大, 包括 header 压缩, keep-alive 优化, 二进制格式支持等. 有兴趣的读者可以在网上搜索相关资料进行深入学习, 本业也只是对在实际项目中遇到的一些案例进行介绍, 而不是对 HTTP 协议本身的讲解.
来源: https://www.cnblogs.com/rock-roll/p/8981511.html