【Nodejs】使用Nodejs构建一个简单的WebSocket服务

767 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情

前言

上节课我们介绍了利用 Nodejs 的 dgram 模块构建基于 UDP 协议的服务,这节我们介绍一下 WebSocket 协议。Nodejs 非常适合用于构建 WebSocket:

  • WebSocket 客户端基于事件的编程模型与 Nodejs 中自定义事件十分相似。
  • WebSocket 实现了客户端与服务器端之间的长连接,而 Nodejs 事件驱动的方式十分擅长与大量的客户端保持高并发连接。

因此,讲到 Nodejs 的网络编程就不得不提一下 WebSocket。接下来,我们将利用 Nodejs 来构建一个简单的 WebSocket 服务,并介绍一些 WebSocket 的基本知识。

什么是 WebSocket

WebSocket 是一种在单个TCP连接上进行全双工通信的协议。它最早是作为HTML5重要特性而出现的,在 W3CIETF 的推动下,形成 RFC 6455 规范。简单来说,WebSocket 就是一个基于tcp的全双工协议。

而与 HTTP 相比,它有以下好处:

  • 它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。
  • 是一个持久化的协议,客户端与服务器端只建立一个TCP连接,可以使用更少的连接。
  • 有更轻量级的协议头,减少数据传送量。
  • WebSocket服务器端可以推送数据到客户端,这远比 HTTP 请求响应模式更灵活、更高效

现代浏览器大多都支持 WebSocket 协议。

在 WebSocket 推出之前,想要实现双向通信,一般采用的是长轮询(long-polling)和iframe流。

长轮询 long-polling

长轮询的原理是客户端向服务器端发起请求,服务器端只在超时或有数据响应时断开连接res.end();客户端在收到数据或者超时后重新发起请求。

//服务器
const http = require('http');
const server = createServer();

server.on('request', (req, res) => {
  let timer = setInterval(() => {
    let seconds = new Date().getSeconds()
    if (seconds == 50) {
      res.end(Date.now())
      clearInterval(timer)
    }
  }, 1000)
})
app.listen(3000);

Iframe

在HTML页面里嵌入一个隐藏的 iframe ,然后将这个 iframe 的 src属性 设为对一个长连接的请求,服务器端就能源源不断地往客户推送数据。

WebSocket 服务

使用 WebSocket 的话,网页客户端只需一个TCP连接即可完成双向通信,在服务器端与客户端频繁通信时,无须频繁断开连接和重发请求。

相比于HTTP, WebSocket更接近于传输层协议,它并没有在HTTP的基础上模拟服务器端的推送,而是在TCP上定义独立的协议。如图,WebSocket 协议主要分为两个部分:握手和数据传输。握手部分是基于HTTP实现的。

握手

客户端在建立连接时,通过HTTP发起请求报文,其中包含 UpgradeConnection 字段,表示请求服务器端升级协议为 WebSocket。

Sec-WebSocket-Key的值是随机生成的Base64编码的字符串。服务端收到该字段后会与密钥相连,然后通过 sha1 hash散列算法进行计算,将得到结果进行 base64 编码,最后返回给服务端。你可以使用这段demo代码体验一下:

const crypto = require('crypto');

const readline = require('readline').createInterface({
  input: process.stdin,
  output: process.stdout,
});

readline.question(`please input key: `, key => {
  const val = crypto.createHash('sha1').update(key).digest('base64');
  console.log(`outcome: ${key}, ${val}!`);
  readline.close();
});

服务器端在处理完请求后,响应如下报文:

这是告诉客户端正在更换协议,更新应用层协议为WebSocket协议,并在当前的套接字连接上应用新协议。客户端将会检查 Sec-WebSocket-Accept 字段,如果检查通过,将开始进行数据传输。而一旦WebSocket握手成功,服务器端与客户端将会呈现对等的效果,都能接收和发送消息。

数据传输

在握手完成后,双方的连接将不再进行HTTP的交互,而是开始WebSocket的数据帧协议,实现客户端与服务器端的数据交换。

客户端将会触发执行 open 事件:

socket.on("open", () => {
    // TODO: you can send message
});

当客户端调用 send() 发送数据时,服务器端触发 message事件;当服务器端调用 send() 发送数据时,客户端的 message事件 触发。当我们调用 send() 发送一条数据时,协议可能将这个数据封装为一帧或多帧数据,然后逐帧发送。

下面,我们简单实现一个基于 WebSocket 通信的服务端和客户端,代码如下:

// server.js
const WebSocket = require('ws');
 
const server = new WebSocket.Server({ port: 12010 });
server.on('connection', (ws) => {
  console.log('client connected');

  ws.on('message', (data, isBinary) => {
      console.log('Message from client ', isBinary ? data : data.toString());
  
      setTimeout(() => {
        ws.send('delay sendback message');
      }, 2000);
    });
});
// clent.js
const WebSocket = require("ws");
 
const socket = new WebSocket("ws://localhost:12010/");
socket.on("open", () => {
    console.log("connect success !!!!");
    socket.send("HelloWorld");
});
 
socket.on("error", function(err) {
    console.log("error: ", err);
});
 
socket.on("close", function() {
    console.log("close");
});
 
socket.on("message", (data) => {
    console.log(data.toString());
});

其它

关于 WebSocket 的原理,其实还包括 JavaScript 中 WebSocket 类的构建、WebSocket数据帧、事件等内容,在此我们并不赘述其原理,想要了解的朋友可以参考

总结

Nodejs 基于事件驱动的方式使得它应对 WebSocket 这类长连接的应用场景可以轻松地处理大量并发请求。尽管 Nodejs 并没有内置 WebSocket 的库,但是社区的 ws模块 封装了 WebSocket 的底层实现,你可以通过 ws模块 轻松的创建一个 WebSocket 服务。

另外,在打印输出数据的时候,你可能会注意到,我们传输的数据是 Buffer 的形式。因此接下来我们将讨论一下 Nodejs 中的 Buffer 类。