WebSocket 是一种网络通信协议。WebSocket 只需要建立一次连接,客户端和服务端之间就可以很方便的发送数据,最主要是服务端也可以主动发送数据,不需要每次都由客户端先发起请求。

HTTP 是一种单向通信方式,客户端发送请求,服务端响应回复,然后断开通信。如果你要使用 HTTP 获取一些实时数据,比如聊天消息、多人游戏数据之类的,你可能需要使用 AJAX 每隔几秒就向服务器请求一次数据,资源占用和延迟都会比较高。

HTTP 和 WebSocket 的使用

WebSocket 需要和 HTTP 配合使用,打开网页时,需要先通过 HTTP 请求到页面,发起 WebSocket 握手时,走的也是 HTTP 协议。

WebSocket 虽然好用,但也不是每个网站都适合。WebSocket 在成功连接后,客户端和服务端都会占用一部分资源,只要不断开连接,资源就会一直占用着。对于客户端来说问题不是太大,但是服务器需要同时服务很多客户端,如果资源不能及时释放,访问量大的时候可能会耗尽服务器资源。

一般如果不是需要实时获取数据的网站,都没有必要用 WebSocket。

浏览器端

下面简单写一下浏览器端使用 WebSocket:

// 创建 WebSocket 连接
const socket = new WebSocket('ws://localhost:7771');

// WebSocket 成功建立连接时触发
socket.addEventListener('open', () => {
  console.log('连接建立');
  // 给服务端发送消息
  socket.send('Hello');
});

// 收到服务端消息时触发
socket.addEventListener('message', ev => {
  // 输出接收到的数据
  console.log(ev.data);
});

// WebSocket 连接断开时触发
socket.addEventListener('close', () => {
  console.log('已断开');
});

// WebSocket 连接发生错误时触发
socket.addEventListener('error', error => {
  console.log(error);
});

new WebSocket 需要传入一个连接地址,WebSocket 的连接地址也是 ws:// 开头,后面的地址和端口号和 HTTP 地址是一样的。

在 WebSocket 成功连接后,可以随时调用 WebSocket 的 send 来向服务器发送消息,不需要向 HTTP 一样要设置请求头之类的。

Node.js 服务端

浏览器端的 WebSocket 也需要在 HTTP 服务器环境下才能使用,直接打开 HTML 文件也是不能用的。

Node.js 创建 HTTP 服务我这里就不写了,要快速创建 HTTP 服务可以用 Express,关于 Express 的使用可以看 Node.js Web 框架 Express 的基本使用

Node.js 处理 WebSocket 需要使用 ws 模块,下面使用 npm 安装 ws:

npm install ws --save

下面直接使用 Node.js 创建 WebSocket 服务:

const ws = require('ws');

// 创建 WebSocket 服务
const socketServer = new ws.WebSocketServer({port: 7771});

// 有客户端连入时触发
socketServer.on('connection', (wsc) => {
  console.log('有客户端连入');
  // 向客户端发送消息
  wsc.send('Hello');

  // 收到客户端消息时触发
  wsc.on('message', message => {
    // 在控制台输出收到的数据
    console.log(`收到:${message}`);
    // 向客户端发送数据
    wsc.send(`你是不是说:${message}`);
  });

  // 连接断开时触发
  wsc.on('close', () => {
    console.log('和客户端断开连接');
  });
});

有客户端连入时,回调函数可以接收一个 wsc 客户端连接,给客户端连接绑定 message 事件可以接收消息,在客户端断开连接时,可以触发 close 事件。

处理多个 WebSocket 连接

上面的 Node.js 在触发 connection 事件后没有单独保存 wsc 客户端连接,在有多个客户端连入的情况下,我只能主动给最后一个连入的客户端发消息,不能主动给其他客户端发消息。

下面修改一下 Node.js,可以给所有客户端发消息,也可以给指定客户端发消息:

const ws = require('ws');

// 创建 WebSocket 服务
const socketServer = new ws.WebSocketServer({port: 7771});
const clientList = new Map();  // 用来存储客户端连接和ID

// 有客户端连入时触发
socketServer.on('connection', (wsc) => {
  // 随机生成一串字符串作为客户端ID
  const id = Math.random().toString(36).substring(2, 10);
  // 把客户端ID和连接存入 clientList
  clientList.set(id, wsc);

  // 给所有客户端发消息
  clientList.forEach(client => {
    client.send(`新增 ${id},当前连接数 ${clientList.size}`);
  });

  // 收到客户端消息时触发
  wsc.on('message', (message) => {
    // 在控制台输出收到的数据和客户端ID
    console.log(`收到 ${id} 的:${message}`);
  });

  // 连接断开时触发
  wsc.on('close', () => {
    // 删除 clientList 的客户端连接
    clientList.delete(id);
    console.log(`${id} 断开连接`);
  });
});

上面的 clientList 使用 Map 存储了客户端连接和客户端 ID,我如果要给指定客户端发消息可以使用 clientList.get(id).send('message') ,只需要在 Map 的 get 传入 ID 就能找到指定客户端。

实现一个简易的多人聊天室

下面使用 WebSocket 实现一个简易聊天室,只要有新的客户端连入或离开,所有客户端的成员列表都会更新,有客户端发送消息,所有客户端也都能看到。

Node.js 服务端:

const ws = require('ws');

// 创建 WebSocket 服务
const socketServer = new ws.WebSocketServer({port: 7771});
const clientList = new Map();  // 用来存储客户端连接和ID

// 有客户端连入时触发
socketServer.on('connection', (wsc) => {
  // 随机生成一串字符串作为客户端ID
  const id = Math.random().toString(36).substring(2, 10);
  // 把客户端ID和连接加入 clientList
  clientList.set(id, wsc);

  // 给所有客户端发送成员列表
  sendMessage({type: 'list', list: Array.from(clientList.keys())});

  // 收到客户端消息时触发
  wsc.on('message', (message) => {
    // 把收到的消息、ID、时间发给所有连接的客户端
    sendMessage({
      type: 'message',
      message: message.toString(),
      id: id,
      time: Math.round(new Date().getTime() / 1000),
    });
  });

  // 连接断开时触发
  wsc.on('close', () => {
    // 删除 clientList 的客户端连接
    clientList.delete(id);
    // 给所有客户端发送成员列表
    sendMessage({type: 'list', list: Array.from(clientList.keys())});
  });
});

// 给所有已连接的客户端发送消息
function sendMessage(message) {
  // 把传入的消息转换为 JSON 字符串
  message = JSON.stringify(message);
  clientList.forEach(client => {
    client.send(message);
  });
}

我的 Node.js 给客户端发送的是转换为 JSON 字符串的对象,每个对象都包含 type 属性,typelist 就是发送成员列表,typemessage 就是普通消息。

浏览器端 HTML:

<!--消息输入--> 
<input type="text" id="message-input">
<!--提交-->
<button type="button" id="submit-btn" disabled>发送</button>
<!--消息列表-->
<div>消息:</div>
<ul id="message-list"></ul>
<!--成员列表-->
<div>成员列表</div>
<ul id="user-list"></ul>

浏览器端 JavaScript:

const messageInput = document.querySelector('#message-input');  // 消息输入
const submitBtn = document.querySelector('#submit-btn');  // 消息提交
const messageList = document.querySelector('#message-list');  // 消息列表
const userList = document.querySelector('#user-list');  // 成员列表

// 创建 WebSocket 连接
const socket = new WebSocket('ws://localhost:7771');

// WebSocket 成功建立连接时触发
socket.addEventListener('open', () => {
  // 取消发送按钮的禁用状态
  submitBtn.disabled = false;
});

// 收到服务端消息时触发
socket.addEventListener('message', ev => {
  // 获取消息
  const message = JSON.parse(ev.data);
  // 根据消息类型判断是普通聊天消息还是成员数量变更
  if (message.type === 'message') {
    // 创建 li 列表项
    const li = document.createElement('li');
    // 设置时间、ID、聊天消息
    li.innerHTML = `${message.time} ${message.id}:${message.message}`;
    // 把创建的列表插入到消息列表
    messageList.appendChild(li);
  }else {
    // 清空成员列表
    userList.innerHTML = '';
    message.list.forEach(item => {
      userList.innerHTML += `<li>${item}</li>`;
    });
  }
});

// WebSocket 连接断开时触发
socket.addEventListener('close', () => {
  // 禁用发送按钮
  submitBtn.disabled = true;
  alert('与服务器断开连接');
});

// WebSocket 连接发生错误时触发
socket.addEventListener('error', error => {
  // 禁用发送按钮
  submitBtn.disabled = true;
  alert('无法连接到服务器');
});

// 发送按钮点击
submitBtn.addEventListener('click', () => {
  // 消息输入框为空就不再往下执行
  if (messageInput.value === '') return false;
  // 发送消息
  socket.send(messageInput.value);
  // 清空消息输入
  messageInput.value = '';
});

最终实现的简易聊天室如下:

WebSocket简易聊天室,使用两个浏览器互相聊天

上面的给客户端分配 ID 只是一种比较简单的处理多连接的方式,更好的方式应该是需要客户端在发消息的时候也一起带上用户信息。