1、Chrome Developer Network Tabhtml
Cheome Developer做为如今前端开发者最经常使用的开发调试工具,其具备前端能够涉及到的各方面的强大功能,为咱们的开发和定位问题提供了极大地便利。其中Network Tab是至关经常使用的一个功能板块。经过它的XHR、JS、CSS、Img等子Tab咱们能够捕获到全部基于应用层的HTTP/HTTPS协议的网络请求,能够查看到该次请求和响应的全部头信息和内容。前端
Network Tab webpack
展现了针对每个HTTP请求的全部属性,包括:web
其中Connection ID为传输层TCP协议的链接ID。关于这点会在下一个章节提到。express
Headers主要展现了这次请求的状态,还有请求和响应的头部信息,头部信息是HTTP交互双方进行做业的依据:npm
Headers中的大多数Key对于有经验开发者来讲并不陌生,不须要在这里介绍了。但仍是须要提到两个key:后端
content-type做为描述交互内容数据MIME格式的key意义至关重大,咱们在实际开发中发出请求缺接收到不到任何东西,若是请求其余部分没问题的话,极可能就是由于先后端的content-type不匹配的缘由致使的。api
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请求存在的问题,并据此解决问题。
2、客户端与服务端经过HTTP协议的交互过程
在HTTP协议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链接创建的。
其余标识符好比FIN、PSH、RST等能够参考这里,
而断开链接时须要4次挥手,多1次是由于在主动要求断开链接的那一方不知道被动断开链接的那一方是否还有数据没有传输完毕,被动断开链接的那一方须要把回复已接收断开消息和数据已经发送完毕分到2步内进行,不管真实状况下,在接收到主动方要断开链接的消息时,还有没有数据须要发送,这2步都必须分开。主动方会一直等待被动方发送FIN码。其实也能够用3步完成,可是数据完整性就得不到保证了。有兴趣的同窗能够自行了解下,这里不作详细解释了。
3、HTTP因前序请求阻塞而致使后续请求无法发起的问题
笔者目前开发的这个项目早期底层和服务端没有作缓存优化的时候,从底层Go的接口返回数据给Node.js层,Node.js层再返回给前端界面。在底层接口没优化的时候,一些操做是现场调用脚本,若是脚本执行耗时长,或者由于网络抖动缘由致使底层分布式集群各节点之间通讯及慢,接口响应速度从几十毫秒、几百毫秒到几秒甚至更长时间不等。前端是基于React.j的SPA应用,每一个界面为了数据的准确性,在进入界面后会当即请求数据,而且后台还根据了当前路由维持了一个每15s更新数据的CronJob,定时刷新这个界面的数据。若是暴力的切换路由改变界面能够在短期内建立大量的HTTP请求。在HTTP1.1下的性能表现极为糟糕,阻塞状况严重。在Chrome等浏览器中,针对同一个域下的HTTP1.1请求同时建立6条TCP链接,每条链接结束之后才能释放出来给对另一个资源的请求来使用。虽然和HTTP1.0相比,在性能上已有较大提高,可是并无本质的改变。以本项目为例,若是当瞬间发起满10个请求后,只有前6个请求可以分配6个不一样的HTTP链接进行处理,后续4个请求只有等待这6个请求有任何一个释放HTTP链接资源之后,才能继续。也就是说前6个请求中若是最少耗时都在1s,那么后4个请求的最少Pending时间都在1s。并且接口的请求都是同一个域,走同一个API网关,没法经过像相似于请求CDN资源同样,来把资源请求分散到不一样的域下。在笔者暴力的操做下,这简直是噩梦:
以getsnapshot这个接口为例,在不阻塞的状况下,其大体须要84ms来完成请求:
然而在发生阻塞后:
在串行响应的加持下,额...好恐怖。
开启了webpack-dev-server的HTTPS(经过spdy模块启服务)后,浏览器默认启用HTTP/2(HTTP2.0)协议:
依旧是暴力操做,浏览器在短期内发起大量的请求。能够看到在ID为2693483的这个TCP链接上,并发处理了的全部的HTTP资源请求,并且它们的开始时间点并非依赖上一个请求的,并且能够并行响应,即后面的请求响应不会等待前面请求响应完成。
而对于HTPP1.X的keep-alive带来的优化:在必定时间内,一个域下只要第一次创建HTTP链接成功,也就是3次握手成功,那么后面的HTTP再也不新创建TCP链接管道,均使用该次链接创建成功之后的管道。避免了没必要要的链接创建的握手过程以及断开链接的挥手过程的耗时。实现了HTTP的持久链接和管道流水线(pipelining)。再加上浏览器对于HTTP1.1协议处理,都会针对同一个域开辟6个TCP链接,必定程度上缓解了浏览器端的资源加载压力。
HTTP1.X虽然解决了HTTP0.X的一些问题,但它在效率上还存在有一些问题:
1. 串行的响应:即使浏览器可以同时在一个链接的HTTP流水线管道里发起多个请求,服务器也可以在这个管道里响应多个请求,可是请求在服务器端依旧是按照顺序给出响应的,也就是说浏览器端接收数据的时候,必须是按照发起时的顺序来接收。并且浏览器对管道流水线的的支持并非太好,要么不支持,要么默认关闭的,须要手动设置开启。对于请求性能和带宽的利用率提升并未带来实质性变化。对于这一点浏览器提供的单域名6个TCP/HTTP链接的优化还能够必定程度上缓解压力,可是短期内针对同一域名的请求发起了太多,响应也较慢,阻塞仍是注定会发生的。
2. 请求-响应的数量太多的限制:大多数浏览器HTTP1.x对同一个域一个时间段内的请求-响应数量是有限制的,通常为6个,这致使浏览器对网络带宽和服务器资源的利用没法最大化。
既然服务器不能并行响应,那么仅仅在浏览器上可以并行发起请求还有什么意义呢?也就是说HTTP1.X即使有了keep-alive的加持,它不能算做是全双工的协议,只能算半双工的协议。
3.对客户端和服务端性能消耗大:浏览器在HTTP1.X上提供的单域名6个TCP/HTTP链接优化,为了维持这6条链接,会致使在请求两方的机器上都会有额外的性能开销。
请看如下经过HTTP1.1协议发情请求的截图:
瞬间并行发起5个请求,浏览器在HTTP1.1协议下作了最大优化:即很对同一的请求同时开启多个(6个)TCP链接,并在它们上面建立了对应数量的可复用而且是持久化的HTTP链接来处理并发的多个HTTP请求,避免因串行响应而形成的队头阻塞。
当咱们间隔必定时间去发起请求,每次都复用的是同一个TCP链接:
这已是在HTTP1.1协议下,不修改业务逻辑的条件下能达到的最大优化,还想要性能提升,恐怕就必须使用到一些合并资源请求、CDN资源分发等等方法了。
而HTTP2.0协议改变了上述问题1的串行方式,容许多个请求并行和并发。容许在一个HTTP链接内发起多个的请求-响应,而且是多流并行的,却又不依赖创建多个TCP链接。数据经过TCP层进行传输的时候,引入了二进制数据帧、流的概念,抛弃了HTTP1.X的基于文本格式的数据传输方式,由于这种方式必须按照顺序进行请求-响应。转而使用二进制帧来对数据进行归类,在帧的头部注入流的标识符,这样浏览器收到数据以后,经过标识符再将不一样流的数据合并在一块儿,能够并行错乱或者分级优先地发送(好比遇到图片和JS都一块儿请求的时候,能够给JS资源请求一个较高的有限值,使它被优先处理)。在服务端经过流的标识符进行从新归类和组装。极大地提升了发送效率,实现了并行且非阻塞的多路复用。说直白一点:不管客户端仍是服务器端,均可以一边并行发送数据一边并行接收数据。这也就是解决了上面提到的问题1.
对于数据帧内部来讲,HTTP1.X的请求-响应首部被放在了HEADERS帧里面,内容被放到了DATA帧里面。
对于问题2解决:HTTP2.0对同一域名下全部请求都是基于流的,就是说在同一域名下,无论在客户端上存在有多少资源的访问,从理论上讲也能够只创建一个HTTP链接的(实际上就是这样的),经过流来区分不一样的请求的数据,因此这一个链接就能完成整个页面的资源加载和后期的数据请求,而不用担忧并发请求-响应的时候会不会出现数据错乱,笔者认为这是相对于HTTP1.X的本质改变。服务器的开销得以减小,处理能力获得大幅提高。
既然只有一个链接的,那么对资源的开销问题也会获得大大的缓解,也就解决了问题3。
但HTTP2.0并非使传输层TCP变成了并行的链接,TCP传输层自己的因串行传输而带来的阻塞是没解决的,仅仅是在应用层HTTP协议上进行了优化,但也正是这些重要的优化使HTTP2.0成为了全双工的协议,单链接多资源的方式克服了TCP慢启动带来的负面影响,更加有效地利用了TCP链接,使链接性能获得了极大的提高。也充分地利用了TCP协议的带宽来下降HTTP延迟,而且减小了链接的内存占用,单个链接的吞吐量增大,网略阻塞和丢包的恢复速度增快等。PS. 想要深刻的理解HTTP2.0协议的读者,能够自行搜索一些权威资料,这里再也不作深刻介绍了。
对经过传统方式进行资源请求优化的影响:一旦HTTP2.0启用后,咱们可能会根据它的特色去改变一些咱们以前对于静态资源的处理,能够减小以前的前端方面在资源请求上的优化工做,特别是资源合并的以减小请求的手段,好比:压缩到一个js文件以减小HTTP请求、精灵图片、CSS合并等,这些彻底均可以放开了,这样作不会再有太大的实际意义。
4、SPDY的出现
上个段落咱们提到了HTTP2.0协议针对HTTP1.X版本的优化,可是HTTP2.0在2015年年中才定稿,在这以前要实现HTTP2.0的一些特性一般使用由Google进行推广的SPDY协议,该协议经历了四个草案,大量的基于HTTP1.X和SSL/TLS上的优化被IETF采用,做为HTTP2.0的重要功能点,能够说SPDY协议是HTTP2.0出现的关键前奏。但其终归是基于HTTP1.X的扩展,除了有相似HTTP2.0的性能提高外,也会有一些HTTP1.X不可克服的问题:好比因队头阻塞而使传输速度受限制。对于资源请求量小的网站性能提高并不明显。在安全性方面,SPDY创建在TLS之上,URL scheme也是https,这点和HTTP2.0相同。随着HTTP2.0的定稿不少浏览器也都开始抛弃了SPDY,改成支持HTTP2.0,包括Chrome。笔者曾经遇到一个有意思的问题,就是本文中前个段落提到的在webpack-dev-server开启了HTTPS,增长了请求并发的性能。但在最新版本的FireFox下访问,webpack-dev-server的进程会报错,致使npm start进程死掉。看报错日志是由底层stream包报上来的一直到spy包,也就是浏览器端和Server端的传输协议不太匹配,致使对在Server端对流的操做失败了。看了webpack-dev-server的源码他是用express起的server服务,若是配置项HTTPS被设置为Ture,就生成一些fake的证书之类的,用spdy起服务,不然直接用普通的http模块起服务。这个脚手架是好久之前的了,它使用的spdy模块的协议草案版本可能和当前最新的FireFox已经不能适配了。但在Chrome中是OK的,多是自家支持地比较好。找到一个13年老版本的FirFox,进入参数配置界面,关于SPDY部分有如下参数,可供参考:
对于HTTP2.0、SPDY、HTTPS在实际实施上,有些方面须要注意:
1. HTTP2.0是能够不基于HTTPS的,也就是能够明文传输,可是在目前几大厂商的浏览器的实现里面,都是基于TLS来支持HTTP2.0,因此要实施HTTP2.0,必须先部署HTTPS,但这一点不必定一直是正确的,随服务端的不一样部署策略和浏览器版本更新,都有可能不一样。
PS:这里体现出了协议(标准)和具体应用的实现上的差别。就拿HTTP协议自己来讲,协议是无状态的,但应用在使用协议的时候,能够在不改变协议的前提下,利用协议预留的加强方式加强协议的功能,好比在客户端请求头引入cookie,在服务端响应头引入session,搭配使用,多个HTTP请求的请求之间就有状态了,突破了协议自己。因此协议与真实应用上的效果,是有差别的。
2. HTTPS不依赖HTTP2.0,能够经过HTTP1.1创建链接,可是后续或切换协议为HTTP2.0。
3. SPDY依赖HTTPS,因此若是使用SPDY模块启用服务,须要HTTPS相关的准备。webpack-dev-sever要启动HTTP2.0支持,也只能经过设置env文件HTTPS=true来经过spdy模块来启动Express服务,不然直接经过http模块启动。
4. 对于不兼容HTTP2.0的浏览器,像Nginx这类服务器,会自动降级为HTTP1.1,以适配浏览器端。
这里再也不作SPDY的详细介绍,有兴趣的同窗能够搜搜相关资料。
5、承前启后的QUIC
若是你仔细阅读了上述的内容,你会发现HTTP协议最大的瓶颈实际上是在传输层TCP/IP协议自己上面,不管TCP/IP上层的应用层协议怎么优化,也没法改变TCP/IP自己过期的设计。因此Google技术推广部在经过SPDY扶正HTTP2之后,又在推基于传输层UDP协议的QUIC协议,有可能这就是将来的HTTP3.0协议,对迎接5G时代的来临极具意义。具体请看笔者转载的这篇文章。
6、WebSocket
在实际项目中webscoket协议的出现频率也是很高的,这里顺带说一句。websocket链接的创建也是先进行TCP握手创建TCP链接,再发起HTTP1.x请求进行握手,最后升级(切换)HTTP协议成websocket协议,进而创建websocket链接:
并且发起websocket链接的客户端的html静态文件不须要放在HTTP的服务器下面,能够直接和websocket服务器创建链接。也就是说能够直接经过file://协议在浏览器打开一个html文件,在该html内部的<script>脚本里面能够直接请求server端创建websocket链接。
可是按照IETF的websocket协议规范RFC6455,握手请求必须由HTTP协议发出,再进行协议的upgrade,好比:
GET /chat HTTP/1.1
Host: 192.168.12.67:8001
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dFhlIXNhbXBsZSBub22jZM==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
注意:13版本的握手协议和旧版本是有区别的。
what?这个html文件没有被HTTP服务器serve,是经过file://协议打开的,竟然还能发出HTTP请求并且没有跨域?为何跨域了呢,由于file://协议访问是没有域的,location.host为空。因此后面连着跟了一个/,致使3斜线连在一块儿了。
从上面的wireshark抓包结果来看,TCP握手成功之后确实是成功发起了HTTP请求的,再升级(切换)HTTP协议成websocket协议的,HTTP的Code为101(Switching Protocols)。这个多是浏览器对websocket API作了特殊处理吧,浏览器彷佛代为发出了到websocket服务器的HTTP跨域请求用于websocket创建前的握手,或者是说websocket服务器在握手阶段是支持HTTP的跨域请求?无论是啥缘由,应该都是为了遵照IETF的RFC6455协议规范。
能够看出,websocket协议和HTTP处于平级,都是应用层的协议,并非websocket是基于HTTP的,或者HTTP是基于websocket的。只是websocket借鉴了HTTP协议的规范用来创建链接,websocket链接一旦经过HTTP协议创建成功后,HTTP协议即被抛弃掉了,后续的数据传输都是经过websocket协议了,它的握手能够被HTTP服务器解释为一个升级请求。所以它们之间有必定交集。而且在传输层上都默认依赖TCP/IP协议。
7、HTTP协议与TCP/IP协议之间是什么关系呢
就如同上面提到的同样,在WEB通讯中,HTTP协议默认使用TCP/IP协议做为其在底层依赖的传输层协议,固然使用TCP/IP协议并不绝对的,依据协议规范,任何可靠的传输层内协议均可以被使用。若是能保证UDP的"可靠",它也可被做为HTTP协议的传输层依赖。若是TCP/IP被比喻成发动机或者底盘之类的底层模块的话,那么HTTP协议就是基于这些底层模块而构建出来的能够方便使用的具有功能联合的汽车。经过使用HTTP协议进行网络通讯的时候,咱们不须要再关注底层的协议栈,只须要按照HTTP协议的请求-响应的约定进行通讯便可。而咱们使用XHR(Ajax)至关于在HTTP协议更上层的API封装,它提供了create, send 以及状态变化回调的各类功能函数,而再也不须要本身从头摸索HTTP协议。就至关于汽车驾校同样,咱们能够直接学习到一套标准的开车(=.=)流程轻松经过考试,而不须要本身摸索怎么考过关。
好了到此结束吧。
HTTP1.0、HTTP1.一、HTTP2.0之间还有不少的区别,每一个版本之间的变化也很大,包括header压缩、keep-alive优化、二进制格式、多路复用等。本文只是对在实际项目中遇到的一些应用案例进行介绍,若是对协议自己感兴趣,能够直接阅读协议规范。