如果你想要写一个WebSocket 服务器,首先需要读懂对应的网络协议 RFC6455,不过这对于一般人来说有些 “晦涩”,英文且不说,还得咬文嚼字理解 网络编程 含义。
好在 WebSocket 技术出现比较早,所以早就有人翻译了完整的 RFC6455中文版,网上也有很多针对该协议的剖析文章,很多文章里还有现成的实现代码可以参考,所以说实现一个简单的 WebSocket 服务并非难事。
在学习本文内容之前,我认为很有必要简单了解一下Web端即时通讯技术的“过去”和“现在”,因为新时代的开发者(没有经历过短轮询、长轮询、Comet技术的这波人),很难理解WebSocket对于Web端的即时通讯技术来说,意味着什么。
所谓“忆苦思甜”,了解了Web端即时通讯技术的过去,方知WebSocket这种技术的珍贵
自从Web端即时通讯的概念提出后,“实时”性便成为了Web开发者们津津乐道的话题。实时化的Web应用,凭借其响应迅速、无需刷新、节省网络流量的特性,不仅让开发者们眼前一亮,更是为用户带来绝佳的网络体验。
但很多开发者可能并不清楚,旧时代的Web端“实时”通信,主要基于 Ajax的拉取和Comet的推送。
大家都知道Ajax,这是一种借助浏览器端JavaScript实现的异步无刷新请求功能:要客户端按需向服务器发出请求,并异步获取来自服务器的响应,然后按照逻辑更新当前页面的相应内容。
但是这仅仅是拉取啊,这并不是真正的“实时”:缺少服务器端的自动推送!
因此,我们不得不使用另一种略复杂的技术 Comet,只有当这两者配合起来,这个Web应用才勉强算是个“实时”的Web端应用!
随着HTML5标准的出现,WebSocket技术横空出世,随着HTML5标准的广泛普及,越来越多的现代浏览器开始全面支持WebSocket技术了。
至于WebSocket,我想大家或多或少都听说过。
WebSocket是一种全新的协议。它将TCP的Socket(套接字)应用在了web page上,从而使通信双方建立起一个保持在活动状态连接通道,并且属于全双工(双方同时进行双向通信)。
事实是:WebSocket协议是借用HTTP协议的101 switch protocol来达到协议转换的,从HTTP协议切换成WebSocket通信协议。
再简单点来说,它就好像将 Ajax 和 Comet 技术的特点结合到了一起,只不过性能要高并且使用起来要方便的多(方便当然是之指在客户端方面了)。
如果要自己写一个 WebSocket 服务,主要有两个难点:
1)熟练掌握 WebSocket 的协议,这个需要多读现有的解读类文章(下面会给出参考文章);
2)操作二进制数据流,在 Node.js 中需要对Buffer这个类稍微熟悉些。
同时还需要具备两个基础知识点:
1)网络编程中使用 大端字节序 表示大于一字节的数据,称之为 网络字节序 (不晓得大小端的,推荐阅读《史上最通俗大小端字节序详解》);
2)了解最高有效位(MSB, Most Significant Bit),不太清楚的,可以参考《LSB最低有效位和MSB最高有效位》。即时通讯聊天软件app开发可以加蔚可云的v:weikeyun24咨询
具体的做法如下,推荐先阅读以下几篇参考文章:
1)《学习WebSocket协议—从顶层到底层的实现原理(修订版)》:作者本身就用Node.js实现过一遍,知识点讲解挺透彻,适合优先阅读;
2)《WebSocket详解(一):初步认识WebSocket技术》:是一系列的文章,从浅入深,配有丰富的图文;
3)《WebSocket从入门到精通,半小时就够!》:全文以Q&A形式组织,要点都解读到了,还涉及了建立连接、交换数据、帧格式及网络安全等;
4)《MDN - Writing WebSocket servers》:MDN 官方教程,读一遍没啥坏处。
然后开始写代码。
在实现过程中的大部分代码可以从下面几篇文章中找到并借鉴(copy):
1)nodejs 实现:简化版本的从这儿借鉴过来的;
2)学习WebSocket协议—从顶层到底层的实现原理(修订版)。
阅读完上面的文章,你会有发现一个共同点,就是在实现 WebSockets 过程中,最最核心的部分就是 解析 或者 生成 Frame(帧)。
在完成上面几个方面的知识储备之后,而且大多有现成的代码,所以自己边抄边写一个 Websocket 服务端实现并不算太难。
对于 WebSocket 初学者,请务必阅读以上参考文章,对 Websocket 协议有大概的了解之后再继续本文剩下部分的阅读,否则很有可能会觉得我写得云里雾里,不知所云。
调用所写的 WebSocket 类
站在使用者的角度,假设我们已经完成 WebSocket 类了,那么应该怎么使用?
客户端通过 HTTPUpgrade请求,即101 Switching Protocol 到 HTTP 服务器,然后由服务器进行协议转换。
在 Node.js 中我们通过 http.createServer 获取 http.server 实例,然后监听 upgrade 事件,在处理这个事件。
代码解读3:Frame 帧数据的处理
WebSocket 客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。
这 this._processBuffer() 部分代码逻辑就是用来解析帧数据的,所以它是实现 WebSocket 代码的关键;(该方法里面用到了大量的位操作符以及 Buffer 类的操作)
操作码(Opcode)
Opcode 即 操作代码,Opcode 的值决定了应该如何解析后续的数据载荷(data payload)
根据 Opcode 我们可以大致将数据帧分成两大类:数据帧 和 控制帧。
数据帧,目前只有 3 种,对应的 opcode 是:
0x0:数据延续帧
0x1:utf-8文本
0x2:二进制数据;
0x3 - 0x7:目前保留,用于后续定义的非控制帧。
控制帧,除了上述 3 种数据帧之外,剩下的都是控制帧:
0x8:表示连接断开
0x9:表示 ping 操作
0xA:表示 pong 操作
0xB - 0xF:目前保留,用于后续定义的控制帧
分片的意义主要是两方面:
1)主要目的是允许当消息开始但不必缓冲该消息时发送一个未知大小的消息。如果消息不能被分片,那么端点将不得不缓冲整个消息以便在首字节发生之前统计出它的长度。对于分片,服务器或中间件可以选择一个合适大小的缓冲,当缓冲满时,再写一个片段到网络;
2)另一方面分片传输也能更高效地利用多路复用提高带宽利用率,一个逻辑通道上的一个大消息独占输出通道是不可取的,因此多路复用需要可以分割消息为更小的分段来更好的共享输出通道。
根据 FIN 的值来判断,是否已经收到消息的最后一个数据帧:
1)FIN=1 表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理;
2)FIN=0,则接收方还需要继续监听接收其余的数据帧。
opcode在数据交换的场景下,表示的是数据的类型:
1)0x01 表示文本,永远是 utf8 编码的;
2)0x02 表示二进制;
3)0x00 比较特殊,表示 延续帧(continuation frame),顾名思义,就是完整消息对应的数据帧还没接收完。
代码里,我们需要检测FIN的值,如果为 0 说明有分片,需要记录第一个 FIN 为 0 时的opcode值,缓存到this.frameOpcode属性中,将载荷缓存到 this.frames 属性中。
发送数据帧
上面讲的都是接收并解析来自客户端的数据帧,当我们想给客户端发送数据帧的时候,也得按协议来。
这部分操作相当于是上述_processBuffer方法的逆向操作,在代码里我们使用encodeMessage方法(为了简单起见,我们发送给客户端的数据没有经过掩码处理)将发送的数据分装成数据帧的格式,然后调用socket.write方法发送给客户端。