从前端开发看HTTP协议的应用
一、Chrome Developer Tool 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作为应用层协议,推荐并默认使用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步完成,但是数据完整性就得不到保证了。有兴趣的同学可以自行了解下,这里不做详细解释了。
三、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)协议:
(关于Chrome开发者工具Network Tab中的Connection ID列的解释,可以参考这里)
依旧是暴力操作,浏览器在短时间内发起大量的请求。可以看到在ID为2693483的这个TCP连接上,在这个链接的管道中并发处理了的所有的HTTP资源请求:它们的开始都不依赖上一个请求的结束,而且可以并行响应,即后面的请求响应不会等待前面请求响应完成。
再回头来看看HTTP1.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传输层本身的因串行传输而带来的阻塞是没解决的,TCP依旧是一个独木桥,仅仅是在应用层HTTP协议上进行了优化,但也正是这些重要的优化使HTTP2.0成为了全双工的协议,单连接多资源的方式克服了TCP慢启动带来的负面影响,更加有效地利用了TCP连接,使连接性能得到了极大的提升。也充分地利用了TCP协议的带宽来降低HTTP延迟,并且减少了连接的内存占用,单个连接的吞吐量增大,网略阻塞和丢包的恢复速度增快等。PS. 想要深入的理解HTTP2.0协议的读者,可以自行搜索一些权威资料,这里不再做深入介绍了。
对通过传统方式进行资源请求优化的影响:一旦HTTP2.0启用后,我们可能会根据它的特点去改变一些我们之前对于静态资源的处理,可以减少之前的前端方面在资源请求上的优化工作,特别是资源合并的以减少请求的手段,比如:压缩到一个js文件以减少HTTP请求、精灵图片、CSS合并等,前几年在移动端也曾经流行过通过LocalStorage缓存静态资源,以及Combo服务来合并HTTP请求做性能优化的方案,但在现在4G移动网络以及HTTP2.0的普及下,也已经没有太大的意义了。
HTTP2.0也能让请求都能携带一个31bit的优先值,0表示最高优先级,优先级随数值增大而降低。基于此值,客户端和服务器就可以提供以优先级来决定流的处理和发送顺序。
HTTP2.0还提供了Server Push的功能,能够主动将资源推给前端而不需要前端发起请求。举个例子:我的以往的JS和CSS等静态资源都是通过加载完HTML以后,再经过浏览器解析,再发出的请求,现在可以由服务端在HTML未加载或者解析完之前就可以推给客户端了,当页面发起请求的时候,它们已经在浏览器的缓存里了,这样一来就可以优化页面的加载速度。
要启用HTTP2.0都必须基于HTTPS(这是出于强推加密来防止网络攻击的目的,请注意:该项不是HTTP2.0协议的要求,而是浏览器实现的要求。),并在服务端完成有关配置。可以参考这里,传送门。
关于HTTP1.1的persisdent-connection(HTTP1.0 keep-alive的替代)功能的介绍,传送门。
这里再推荐一本书,有兴趣的同学可以阅读其中的11至14章节,传送门。
四、SPDY的出现
上个段落我们提到了HTTP2.0协议针对HTTP1.X版本的优化,但是HTTP2.0在2015年年中才定稿,在这之前要实现HTTP2.0的一些特性通常使用由Google进行推广的SPDY协议,主流的浏览器和主流网站的服务端也都支持该协议。该协议经历了四个草案,大量的基于HTTP1.X和SSL/TLS上的优化被IETF采用,作为HTTP2.0的重要功能点,可以说SPDY协议是HTTP2.0出现的关键前奏(HTTP2.0即基于SPDT v2)。
但其终归是基于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:这里体现出了协议(标准)和具体应用的实现上的差异。
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的详细介绍,有兴趣的同学可以搜搜相关资料。
五、承前启后的QUIC
如果你仔细阅读了上述的内容,你会发现HTTP协议最大的瓶颈其实是在传输层TCP协议本身上面,无论TCP上层的应用层协议怎么优化,也无法改变TCP本身过时的设计。所以Google技术推广部在通过SPDY扶正HTTP2以后,又在推基于传输层UDP协议的QUIC协议,有可能这就是未来的HTTP3.0协议,对迎接5G时代的来临极具意义。具体请看笔者转载的这篇文章。
QUIC将作为HTTP3.0的引路人。
六、WebSocket
在实际项目中webscoket协议的出现频率也是很高的,这里顺带说一句。websocket连接的建立也是先进行TCP握手建立TCP连接,再发起HTTP1.x请求进行握手,最后升级(切换)HTTP协议成websocket协议,进而建立websocket连接:
所以,请记住了:websocket的端口不要乱换哦,要不然连接会被网络中间件干掉,比如网络环境中有防火墙,只允许HTTP请求走80和443,你给websocket改个200端口,走HTTP建立连接这步就直接失败了!
而且发起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协议。如果不想深入了解协议的话,这里倒是有个不错的提问值得一看。
怎么写一个websocket服务器?可以看这里。
七、HTTP协议与TCP/IP协议之间是什么关系呢
就如同上面提到的一样,在WEB通信中,HTTP协议默认使用TCP/IP协议作为其在底层依赖的传输层协议,当然使用TCP/IP协议并不绝对的,依据协议规范,任何可靠的传输层内协议都可以被使用。如果能保证UDP的"可靠",它也可被作为HTTP协议的传输层依赖。(PS:HTTP3.0应该 就是走UDP了,毕竟TCP/IP的性能有限,在网络速度越来越快的,短链接越来越多的情况下,劣势越明显。)如果TCP/IP被比喻成发动机或者底盘之类的底层模块的话,那么HTTP协议就是基于这些底层模块而构建出来的可以方便使用的具备功能联合的汽车。通过使用HTTP协议进行网络通信的时候,我们不需要再关注底层的协议栈,只需要按照HTTP协议的请求-响应的约定进行通信即可。而我们使用XHR(Ajax)相当于在HTTP协议更上层的API封装,它提供了create, send 以及状态变化回调的各种功能函数,而不再需要自己从头摸索HTTP协议。就相当于汽车驾校一样,我们可以直接学习到一套由驾校提炼出来的开车方法和注意事项等来轻松通过考试,而不需要自己摸索怎么考过关。
八、顺带简述一下HTTPS的握手过程
先是单向认证的过程(客户端认证服务端)
1. 客户端向服务端发起请求,包括客户端生成的随机数Random1,和客户端可以支持的加密算法套件等(这一步被称为Client Hello)。
2. 服务端接收到以后,在服务端端生成随机数Random2,并从客户端发送的其支持的加密算法套件中选择一个服务端自己也支持的套件,作为后续生成会话加密秘钥的加密算法来加密会话信息。并将客户端申请的HTTPS证书.cert和公钥.key、Random2、选择的会话加密和摘要生成所需的算法套件返回给客户端(这一步被称为Server Hello),这里所有返回给客户端的东西都是明文传输的。(服务端获取CA证书后,有一个公钥、一个私钥,采用非对称加密方式,公钥用于交给客户端来加密,私钥不在网络中传递,只用于解密公钥加密的信息)
3. 客户端收到以后,首先对服务端证书进行验证,主要针对证书的CA颁发机构、证书域名是否和服务端域名一致、证书是否被吊销、证书是否过期等方面进行验证、证书的数字签名是否能被收到的公钥解密(这一步被称为Certificate Verify),如果验证成功,客户端自己再生成第三个随机数Random3,此时客户端已经有3个随机数了:Random1(来自客户端自己)、Random2(来服务端)、Random3(来自客户端自己),随即采用服务端选择的会话加密套件来加密这3个随机数,来生成会话秘钥(使用3个随机数的目的是为了降低被破解的风险,随机数1和2是明文传递的,3是加密传递的,被破解的风险低)。并用收到的公钥将Random3加密,这个加密后的随机数Random3被称为PreMaster key,然后客户端将加密后的Random3发送给服务端(这个发送过程称之为Client Key Exchange )。
4. 服务端收到以后,使用私钥将PreMaster key(也就是加密后的Random3)解密,得到明文的Random3,此时服务端也和上一步中的客户端一样已经有3个随机数了:Random1(来自客户端)、Random2(来服务端自己)、Random3(来自客户端),然后服务端采用之前选定的会话加密套件来加密这3个随机数,来生成会话秘钥,这个会话秘钥和上一步中在客户端中生成的会话秘钥是一样的,至此相同的两个会话秘钥就同时存在于客户端和服务端里面的,后续在应用层的中的对会话信息的加密过程其实就切换到了对称加密的方式了(秘钥只有这一个,客、服两端各一份)。到此会话秘钥就协商完成了,后面紧接着就是对该会话秘钥的验证。
5. 在上一步完成以后,客户端将之前步骤中的握手消息生成摘要(用的加密套件算法就是步骤2中服务端选的那个),然后将摘要用前两步中生成的会话秘钥加密并生成Encrypted Handshake Message(Client),并将其发送给服务端(即发送Client Finish消息),这是客户端的第一个加密信息。
6. 服务端接收后也会用之前生成的那一个会话秘钥来解密这个加密的摘要即Encrypted Handshake Message(Client),能解密出来并且摘要数据没问题(因为之前的握手过程客、服两端都有参与,他们都知道这个摘要里的握手消息是否是正确的),那就说明前面客、服两端各自生成的会话秘钥是有效的且一致的。然后服务端告诉客户端,在后面一定时间内,都用这个会话秘钥进行通信信息的加密(这一步被称之为Change Cipher Spec)。并且服务端也会将握手过程的消息通过选择的加密套件算法生成摘要,并用会话秘钥来加密并生成Encrypted Handshake Message(Server),这是服务端发出的第一条加密消息(即发送Server Finish消息)。
7. 客户端接收后会用会话秘钥解密,能解密出来并且摘要数据没问题,会话秘钥是有效的且一致的。到此,客户端、服务端都对该会话秘钥完成了验证。
8. 至此会话密钥协商和验证过程结束,可以开始在传输层TCP上发送加密的应用层数据了,后续针对会话信息的加密、解密都是采用那一个协商出的会话秘钥来进行,形式为对称加密。
PS:每一步的数字标号和抓包中的步骤并不完全一致,有简化。
单向认证的握手过程总结就是:
通过证书和公钥的传递和验证、随机数、摘要的传递等,先通过非对称加密来在客、服两端协商出同一个会话秘钥,然后再在客、服两端验证该会话秘钥的有效性和一致性,验证通过以后,后续会话的加密、解密都用该会话秘钥进行,加密方式变为对称加密。至于为什么不直接在网络上传递对称加密秘钥,那肯定就是为了防止被攻击者在网络上拦截获并取到,所以采用从非对称加密的形式分别在客、服两端本地来协商出一个相同的秘钥,该秘钥没在网络中传递过不会被劫持,再用该秘钥来对会话信息进行对称加密。
如果是双向认证的话,其实就是多了服务端对客户端的认证,多出的步骤如下:
1. 在单向认证的第3步中,客户端验证服务端证书通过以后,会将自己的证书和公钥发送给服务端。
2. 服务端对客户端发送的证书进行验证,通过以后,获取到客户端的公钥。
3. 在单向认证的第2步中,服务端返回选择的加密套件(明文),改用上一步获取到的客户端公钥进行加密再返回给客户端,客户端收到以后用其本地的私钥解密。
4. 其它步骤一致。
单向认证仅仅是服务端像客户端证明自己是真正的服务端而不是劫持者,双向认证就是不仅服务端需要证明自己,客户端也需要像服务端证明自己是真正客户而不是劫持者,就类似于我们能用网银支付一样,我们需要给服务端提供网银盾秘钥或者是动态的手机短信验证码来证明自己是一个真正的客户,而不是一个身份冒用者。
好了到此结束吧。
HTTP1.0、HTTP1.1、HTTP2.0之间还有很多的区别,每个版本之间的变化也很大,包括header压缩、keep-alive优化、二进制格式、多路复用等。本文只是围绕着实际项目中遇到的一些应用案例进行肤浅的介绍,如果对协议本身感兴趣,并渴望深入了解的同学可以直接阅读HTTP协议本身。