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 属性,type 为 list 就是发送成员列表,type 为 message 就是普通消息。

浏览器端 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 只是一种比较简单的处理多连接的方式,更好的方式应该是需要客户端在发消息的时候也一起带上用户信息。