技术分享02 - WebSocket 原理

WebSocket 与 HTTP 的关系

WebSocket 协议在2008年诞生,2011年成为国际标准,现在所有浏览器都已经支持。WebSocket 的最大特点是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

我们都知道,HTTP 有 1.1 和 1.0 之说,也就是增加了所谓的 keep-alive :把多个 HTTP 请求合并为一个。

而 Websocket 其实是一个新协议,跟 HTTP 协议基本没有关系,只是为了兼容现有浏览器,所以在握手阶段使用了 HTTP 。也就是说,它是 HTTP 协议上的一种补充,可以通过这样一张图理解:有交集,但是并不是全部。

WebSocket与HTTP

WebSocket 是怎样的,有什么优点

持久化 和 主动性

首先,Websocket 是一个持久化的协议,相对于 HTTP 这种非持久的协议来说。

举个例子:

在 HTTP 1.0 中,发送了一个 requst,收到了一个 response ,那么这次的 HTTP 请求就结束了。

HTTP 1.1 就对它进行了改进,使得有一个 keep-alive,也就是说,在一个HTTP连接中,可以发送多个 request ,接收多个 response 。

因此,在 HTTP 中,request 的个数 === response 的个数。也就是说,一个 request 只能有一个 response 。而且这个 response 也是被动的,不能主动发起。

顺带说一下:

ajax 轮询

ajax 轮询的原理非常简单,就是让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。

客户端:有没有新信息(Request)
服务端:没有(Response)
客户端:有没有新信息(Request)
服务端:没有(Response)
客户端:有没有新信息(Request)
服务端:没有(Response)
客户端:有没有新消息(Request)
服务端:有啦,给你。(Response)
客户端:有没有新消息(Request)
服务端:没有(Response)

---- loop

long poll

long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话)。

也就是说,客户端发起请求后,如果没消息,就一直不返回 Response 给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

客户端:有没有新信息,没有的话就等有了才返回给我吧(Request)
等到有消息的时候…
服务器:来 给你(Response)

客户端:有没有新信息,没有的话就等有了才返回给我吧(Request)
等到有消息的时候…
服务器:来 给你(Response)

---- loop

从上面可以看出,其实这两种方式,都是在不断地建立HTTP连接,然后等待服务端处理。

这体现HTTP协议的另外一个特点,被动性

何为被动性呢?其实就是,服务端不能主动联系客户端,只能有客户端发起。所以,上面这两种都是非常消耗资源的:

  • ajax轮询 - 需要服务器有很快的处理速度和资源。(速度)

  • long poll - 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)

所以 ajax轮询 和 long poll 都有可能发生这种情况。

客户端:有新信息么?
服务端:正忙,请稍后再试(503 Server Unavailable)
客户端:有新信息么?
服务端:正忙,请稍后再试(503 Server Unavailable)

WebSocket

而对于 WebSocket来说,当服务器完成协议升级后(HTTP -> Websocket),服务端就可以主动推送信息给客户端啦。所以上面的情景可以做如下修改。而且,也可以看出 WebSocket 是持久化的。

客户端:啦啦啦,我要建立Websocket协议,需要的服务:chat,Websocket协议版本:17(HTTP Request)
服务端:ok,确认,已升级为Websocket协议(HTTP Protocols Switched)
客户端:麻烦你有信息的时候推送给我噢。。
服务端:ok,有的时候会告诉你的。
服务端:balabalabalabala
服务端:balabalabalabala
服务端:balabalabalabala

这样,只需要经过一次 HTTP 请求,就可以做到源源不断的信息传送了。(在程序设计中,这种设计叫做回调,即:有信息了再来通知我,而不是我每次跑来问你)

速度更快

其实,我们所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的**Handler(PHP等)**来处理。

简单地说,我们有一个非常快速的接线员(Nginx),他负责把问题转交给相应的客服(Handler)
本身接线员的速度基本上是足够的,但是每次都卡在客服那里,所以总是导致客服不够。

Websocket 就解决了这样一个难题。建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员再统一转交给客户。

这样就可以解决客服处理速度过慢的问题了。

有状态 - 消耗较少的资源

而且!HTTP 还是一个无状态的协议。

通俗的说就是,服务器因为每天要接待很多客户,还很健忘,你一挂电话,他就把你的东西全忘光了,把你的东西全丢掉了。你第二次还得再告诉服务器一遍。

在传统的方式中,要不断的建立,关闭HTTP协议。由于HTTP是无状态的,所以每次都要重新传输identity info(鉴别信息),来告诉服务端你是谁。
虽然接线员很快速,但是每次都要听这么一堆,效率也会有所下降的,同时还得不断把这些信息转交给客服,不但浪费客服的处理时间,而且还会在网路传输中消耗过多的流量/时间。

但是,Websocket只需要一次HTTP握手,也就是说整个通讯过程是建立在一次连接/状态中,服务端会一直知道你的信息,直到关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。

典型的 WebSocket 报文

请求报文

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

这段类似 HTTP 协议的握手请求中,实际上多了这么几个东西。

Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

其中,

Upgrade: websocket
Connection: Upgrade

就是 WebSocket 的核心了,相当于告诉 Apache 、 Nginx 等服务器:注意啦,我发起的请求要用 WebSocket 协议,帮我找到对应的助理处理。

Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

Sec-WebSocket-Key 是一个 Base64 encode 的值,这个是浏览器随机生成的,作用是验证服务器的身份(后面会说)。

Sec-WebSocket-Protocol 是一个用户定义的字符串,用来区分同一个 URL 下,不同的服务所需要的协议。

Sec-WebSocket-Version 是告诉服务器所使用的 WebSocket Draft (协议版本)。

在最初的时候,WebSocket 协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么 Firefox 和 Chrome 用的不是一个版本之类的,当初 WebSocket 协议太多可是一个大难题。

不过现在还好,已经定下来啦~大家都使用同一个版本:13

响应报文

服务器会返回下列报文,表示已经接受到请求, 成功建立 WebSocket 啦!

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

Upgrade: websocket
Connection: Upgrade

其中,这段就是 HTTP 最后负责的区域了,告诉客户端,我即将升级的是 WebSocket 协议!

Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

Sec-WebSocket-Accept 是经过服务器确认,并且加密过后的 Sec-WebSocket-Key 。

Sec-WebSocket-Protocol 则是表示最终使用的协议。

至此,HTTP 已经完成它所有工作了,接下来就会完全按照 WebSocket 协议进行了。

客户端简单示例

var ws = new WebSocket("wss://echo.websocket.org");

// 用于指定连接成功后的回调函数。
ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  ws.send("Hello WebSockets!");
};

// 用于指定收到服务器数据后的回调函数。
ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};

// 用于指定连接关闭后的回调函数。
ws.onclose = function(evt) {
  console.log("Connection closed.");
};   

其他特点

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

ws://example.com:80/some/path

img

参考资料