一、WebSocket核心目的与基础原理

  1. 解决HTTP轮询痛点

    • 传统HTTP轮询需客户端频繁请求(如1秒1次)才能获取新数据,浪费带宽且延迟高。
    • WebSocket通过一次握手建立‌持久化全双工连接‌,服务器可主动推送数据,无需客户端轮询‌。
  2. 协议升级机制

    • 客户端发起HTTP握手请求,头部包含:

      Upgrade: websocket 
      Connection: Upgrade 
      Sec-WebSocket-Key: [随机密钥]
    • 服务器响应101 Switching Protocols完成协议升级,后续通信基于TCP层的二进制帧传输‌。


🔄 二、双工通信实现机制

  1. 全双工特性

    • 通信双方(客户端/服务器)可‌同时发送和接收数据‌,无需等待对方响应‌。
    • 对比HTTP半双工(请求→响应模式),大幅提升实时性(如聊天消息即时到达)。
  2. 数据帧结构

    • 数据以‌二进制帧(Binary Frames)‌ 传输,包含:
      • FIN位:标识消息结束
      • Opcode:数据类型(文本/二进制/控制帧)
      • Mask:客户端到服务端的数据掩码
      • Payload length:数据长度‌。

⚡ 三、性能优化关键技术

  1. 心跳机制(Heartbeat)

    • 定时发送Ping/Pong控制帧(空负载),检测连接存活状态,避免防火墙超时断开‌。
    • 断线时自动重连,保障通信连续性‌。
  2. 二进制传输优势

    • 支持直接传输ArrayBuffer/Blob二进制数据(如图片、视频流),避免JSON序列化开销‌。

    • 对比HTTP文本传输,节省带宽且解析更快‌。


🔐 四、安全与兼容性设计

  1. 加密与同源策略

    • wss://协议基于TLS加密(类似HTTPS),防止中间人攻击‌。
    • 遵守浏览器同源策略,禁止跨域非法连接‌。
  2. 降级兼容方案

    • 不支持WebSocket的环境(如旧浏览器)自动降级为HTTP长轮询(Long Polling)或SSE(Server-Sent Events)‌。

️ 五、实战应用场景

场景 优势体现
实时聊天/在线游戏 毫秒级双向消息同步
金融交易平台 实时股价推送,延迟100[5][13][7][10][<ms
IoT设备监控 设备状态实时上报与控制

📌 总结关键原理链条

HTTP握手升级 --> TCP持久连接 --> 二进制帧传输 --> 全双工通信 --> 服务端主动推送 --> 客户端实时响应 --> 低延迟高并发

通过协议升级告别轮询,以二进制帧实现高效实时通信‌

实操环节:

以下是使用Express和Socket.IO实现WebSocket多人聊天室的完整方案,结合核心模块与实战代码:


一、基础架构设计

  1. 技术选型

    • Express‌:提供HTTP服务基础框架
    • Socket.IO‌:封装WebSocket协议,支持自动重连、房间管理等功能‌
    • 客户端库‌:socket.io-client实现浏览器端通信‌
  2. 核心流程

    客户端连接 --> Socket.IO握手 --> 建立持久连接 --> 双向通信 --> 消息广播 --> 房间管理

服务端搭建流程:

1.先创建一个文件夹作为服务端代码目录,创建后使用vscode打开并在vscode中在当前文件夹目录下打开一个新的命令行窗口,通过 npm init -y 来初始化package.json文件,为后续引入express和socket.io npm包做准备

2.通过 npm i express socket.io nodemon  来下载服务端需要依赖的三个包,express是用来搭建nodejs服务器的,socket.io封装了websocket 相关的底层API,支持会话自动重连、房间管理等功能。nodemon是用来在命令行窗口通过 nodemon server.js 来启动node服务器的,并支持热更新,当server.js文件发生更改时,node服务会自动重新启动。节省每次都需要node server.js手动重启服务器的时间。

3.在当前文件夹下创建server.js服务端文件。

const express = require('express');
const http = require('http');
const socketIO = require('socket.io');

const app = express();
const server = http.createServer(app);
// io对象管控所有客户端连接与服务器消息交互
const io = socketIO(server);

// 存储在线用户
const users = new Map();

// 当有客户端连接到服务器上时触发
io.on('connection', socket => {
  // 每个客户端登陆后都会被服务器自动分配一个socket对象,用来接收对应客户端发送的消息,每个socket对象都有一个id属性作为唯一区分标志
  console.log(`新用户连接,${socket.id}`);
  io.emit('total', users.size) // io对象发送消息时所有连接到服务器的客户端都可以接收到
  // 用户登录处理,此socket对象只监听管理自己对应的那个客户端发送的消息
  socket.on('login', userName => {
    users.set(socket.id, userName);
    io.emit('total', users.size) // io对象发送消息时所有客户端都可以接收到
    console.log(users.entries());
    // 向其他链接到socket服务器的客户端用户发送广播消息,排除自己客户端,只是发送给客户端,服务端所有socket对象不会收到相应事件
    socket.broadcast.emit('broadcastLogin', `${userName}加入了聊天室`)
  })

  // 监听对应客户端发送的socketMessage事件
  socket.on('socketMessage', msg => {
    const user = users.get(socket.id);
    console.log(`${user}收到了message消息,消息内容为${msg}`);
    // io对象将此客户端发送到服务器socket对象上的消息广播给所有客户端,包括自己的客户端
    // 每个socket对象只一一对应一个客户端,想要广播消息就需要通过io对象的io.emit方法或者socket.broadcast.emit方法
    io.emit('allMessage', {user, msg, size: users.size});
  })

  // 断开链接
  socket.on('disconnect', (reason) => {
    const user = users.get(socket.id);
    console.log(`${user}断开了连接,断开原因:${reason}`);
    users.delete(socket.id);
    io.emit('total', users.size) // io对象发送消息时所有客户端都可以接收到
  })

  socket.on('logout', _ => {
    // 断开与客户端的链接,且不会自动重连
    socket.disconnect();
    const user = users.get(socket.id);
    console.log(`${user}退出登录`);
    users.delete(socket.id);
    io.emit('total', users.size) // 所有客户端都可以接收到
  })
})

server.listen(3000, () => {
  console.log('Server is running on localhost://3000');
})

完成后通过命令行窗口进入到server.js文件所在目录下,通过nodemon server.js启动websocket服务器,当看到命令行窗口输出“Server is running on localhost://3000”时代表服务器端启动成功。

通过“npm create vite@latest --template vue”快速创建一个vue模板工程,创建完成后进入对应工程目录下,通过npm i 安装依赖,还需要额外 npm i socket.io-client 安装socket.io客户端npm包依赖,依赖安装完成后替换APP.vue文件中的内容为如下内容。

<template>
  <div>当前用户:{{ user }},共{{ total }}人参与聊天</div>
  <!-- <div>接收到的服务器端的消息</div> -->
  <div class="msg-container">
    <div v-for="item in msgArr" :class="{'right': item.isSelf, 'left': !item.isSelf}">
      <p>{{!item.isSelf ? item.user: '我'}}</p>
      <p>{{ item.msg }}</p>
    </div>
  </div>
  <div>登录::
    <input v-model="username" @keyup.enter="login">
  </div>

  <div>发消息::
    <input v-model="message" @keyup.enter="sendMsg">
  </div>

  <div @click="logout">退出登录</div>
</template>

<script setup lang="ts">
  import { io } from 'socket.io-client';
  import { ref } from 'vue';

  // 和websocket服务器建立连接,此socket对象用来和服务端进行消息交互,管理客户端所有的消息接受与发送
  const socket = io('http://localhost:3000', {
    transports:['websocket'],
    reconnectionAttempts: 3
  });
  const msg = ref();
  const username = ref();
  const message = ref();
  const user = ref();
  const msgArr = ref([])
  const total = ref();

  const login = () => {
    socket.emit('login', username.value)
    user.value = username.value;
    username.value = '';
  }

  // 连接服务器成功
  socket.on('connect', () => {
    console.log('websocket connect success!')
  })

  // 与服务器连接断开
  socket.on('disconnect', (reason) => {
    console.log('websocket disconnect', reason)
  })

  socket.on('total', (num)=> {
    total.value = num;
  })

  socket.on('allMessage', info => {
    console.log('get msg from server', info)
    if(!info.msg){
      return;
    }
    if(info.user === user.value){
      msgArr.value.push({msg: info.msg, isSelf: true, user: info.user, size: info.size})
    } else {
      msgArr.value.push({msg: info.msg, isSelf: false, user: info.user, size: info.size})
    }
    console.log('msgArr.value消息列表', msgArr.value)
  })

  // 连接服务器失败
  socket.on('connect_error', err => {
    console.error('socket connect fail', err)
  })

  
  socket.on('broadcastLogin', (loginInfo) => {
    console.log('客户端::', loginInfo);
  })

  const sendMsg = () => {
    socket.emit('socketMessage', message.value);
    message.value = '';
  }

  const logout = () => {
    socket.emit('logout', user.value);
    user.value = '';
    total.value = total.value - 1;
  }

  socket.on('reconnecting', n => {
    console.log(`第${n}次重新连接`)
  })

  socket.on('reconnect_failed', n => {
    console.log(`重连失败了...`)
  })
</script>


<style scoped>
.msg-container{
  width: 300px;
  border: 2px solid #999;
  /* clear:both; */
}
.left{
  width: 100%;
  text-align: left;
}
.right{
  width: 100%;
  text-align: right;
}
</style>

通过npm run dev启动该工程,打开多个页面访问对应前端服务的访问地址,可以通过界面的登录按钮与发消息按钮进行登录和消息发送,同时可在对应页面的console控制台和nodejs服务启动的命令行窗口查看相应日志。

爆肝不易,若觉得这篇文章对您有帮助,可以通过文章底部的"打赏功能"请作者喝杯咖啡☕继续肝下一篇!

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐