☰
Coding Train WebSockets 教程 12.3:用 Socket.io 打通 Node.js 服务器与 p5.js 客户端
2026/10/13 7:00:57 网站建设 项目流程
  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

本篇对应 Coding Train《WebSockets》系列(_Tutorials/12-websockets/index.md)的第 3 期视频,核心任务是:在已经用 Express 搭好静态服务器的 Node.js 应用中引入 socket.io,同时让浏览器里的 p5.js 客户端建立同一条实时双向通道。读完本文,你将掌握 socket.io 在服务端与客户端的接入方式、自定义事件收发(emit/on)、广播(broadcast)机制,并能直接运行仓库中配套的 Node/sockets 示例,为下一步实现"多人共享画布"打好基础。

一、系列定位:本节在整个教程中的角色

该系列围绕一个明确目标展开:使用 Node.js、p5.js 与 socket.io(WebSockets)在多个浏览器会话之间创建共享的 HTML5 画布。整条学习路径在仓库中按视频顺序组织:

章节文档内容
12.1_Tutorials/12-websockets/12.1-introduction-to-node.mdNode.js 基础与运行环境
12.2_Tutorials/12-websockets/12.2-using-express-with-node.md用 Express 托管静态文件
12.3(本篇)_Tutorials/12-websockets/12.3-socket-io.md把 socket.io 同时接入服务器与客户端,建立连接
12.4_Tutorials/12-websockets/12.4-shared-drawing-canvas.md整合全部组件,实现共享画布与消息广播

本节(12.3)处于"通管道"的关键位置:12.1、12.2 解决了"服务器能跑、静态页面能出"的问题,本节则引入实时双向通信层,12.4 再基于它完成业务功能。文档标题 "Connecting Client to Server with Socket.io" 直白点明了本节唯一的主题——把客户端和服务器"连起来"。

本节配套代码目录为 Node/sockets,包含 4 个文件:

Node/sockets/ ├── package.json # 依赖声明与入口 ├── server.js # Express + socket.io 服务器 └── public/ ├── index.html # 引入 socket.io / p5.js 客户端库 └── sketch.js # p5.js 客户端:连接、收发事件、画点

二、服务端:把 socket.io 挂到已有 HTTP 服务器上

1. 依赖声明与入口配置

Node/sockets/package.json 声明了两个运行时依赖:

  • express: ^4.13.4—— 提供 HTTP 服务与静态文件托管;
  • socket.io: ^2.4.0—— 提供 WebSocket 实时通信能力。

main字段指向server.js,即服务器入口。安装依赖只需在本地副本目录执行:

npm install

2. 服务器骨架:Express 先跑起来

Node/sockets/server.js 的前半部分是 12.2 已经讲过的 Express 标准骨架:

// Using express: http://expressjs.com/ var express = require('express'); // Create the app var app = express(); // Set up the server // process.env.PORT is related to deploying on heroku var server = app.listen(process.env.PORT || 3000, listen); // This call back just tells us that the server has started function listen() { var host = server.address().address; var port = server.address().port; console.log('Example app listening at http://' + host + ':' + port); } app.use(express.static('public'));

几个值得注意的细节:

  • 端口策略:app.listen(process.env.PORT || 3000)优先读取环境变量process.env.PORT,缺省回退到3000。源码注释明确说明这是为部署在 Heroku 等 PaaS 平台准备的——云平台通常会注入一个动态端口,本地开发则固定用 3000。
  • 静态托管:app.use(express.static('public'))把public目录下的文件作为静态资源直接提供给浏览器,因此客户端只需要index.html和sketch.js两个文件,无需任何构建步骤。
  • 启动回调:listen()在服务器就绪时打印实际监听地址与端口,用于确认进程已正常运行。

3. 关键一步:require('socket.io')(server)

在 Express 服务器就绪之后,第 25 行是本节的核心动作(Node/sockets/server.js):

// WebSocket Portion // WebSockets work with the HTTP server var io = require('socket.io')(server);

这里必须把已经创建的 HTTPserver对象传给 socket.io 工厂函数,而不是新开一个独立端口。原因在于:WebSocket 连接以 HTTP 握手为前提,通过Upgrade请求从同一端口升级为长连接。这样客户端访问http://localhost:3000拉取静态页面的同时,也能在同一个端口上建立 WebSocket 通道,服务端无需再维护第二套监听。源码注释 "WebSockets work with the HTTP server" 正是对这一设计意图的说明。

4.connection事件:每个客户端一个socket对象

接下来注册全局连接回调(Node/sockets/server.js):

io.sockets.on('connection', // We are given a websocket object in our function function (socket) { console.log("We have a new client: " + socket.id); socket.on('mouse', function(data) { // Data comes in as whatever was sent, including objects console.log("Received: 'mouse' " + data.x + " " + data.y); // Send it to all other clients socket.broadcast.emit('mouse', data); } ); socket.on('disconnect', function() { console.log("Client has disconnected"); }); } );

逐点解读:

  • io.sockets.on('connection', callback):每当有客户端建立连接时触发。回调参数socket代表当前这一个客户端的专属连接对象,每个用户都会拿到独立的一份。
  • socket.id:socket.io 为每个连接分配的唯一标识,服务端用它区分不同的客户端。示例里每次新客户端接入都会打印一行We have a new client: <id>。
  • socket.on('mouse', function(data) {...}):监听名为mouse的自定义事件。data是客户端发来的任意数据——可以是字符串、数字,也可以是如源码注释所说"whatever was sent, including objects"的完整 JavaScript 对象。这里收到的就是客户端传上来的{x, y}鼠标坐标对象。
  • socket.broadcast.emit('mouse', data):这是本节最核心的广播语义——把mouse事件连同数据转发给除发送者之外的所有其他客户端。发送者自己不需要这条回显,因为它的本地画布已经画过了。
  • socket.on('disconnect', ...):监听该客户端的断开事件,便于服务端做清理与日志记录。

三、客户端:在浏览器里建立 socket 连接

1. 引入 socket.io 客户端库

客户端第一步是在 HTML 里加载 socket.io 的浏览器端库(Node/sockets/public/index.html):

<script type="text/javascript" src="https://cdn.socket.io/socket.io-1.4.5.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/p5@1.4.1/lib/p5.min.js"></script> <script type="text/javascript" src="sketch.js"></script>

页面依次加载:socket.io 客户端库、p5.js 核心库、本地sketch.js。注意 socket.io 客户端库必须先于sketch.js 加载,否则io全局对象在setup()中不可用。

2. 建立连接:io.connect(url)

在 Node/sockets/public/sketch.js 的setup()中建立连接(第 11-13 行):

// Keep track of our socket connection var socket; function setup() { createCanvas(400, 400); background(0); // Start a socket connection to the server // Some day we would run this server somewhere else socket = io.connect('http://localhost:3000'); ... }
  • io.connect('http://localhost:3000')显式指定服务器地址。源码注释 "Some day we would run this server somewhere else" 提示:一旦部署到远程主机,这里应替换为实际域名或 IP,而非写死本地地址。
  • 连接建立是异步的,返回的socket对象即后续所有收发操作的门面。

3. 接收事件:socket.on(event, callback)

紧接着注册mouse事件的接收回调(第 16-25 行):

socket.on('mouse', // When we receive data function(data) { console.log("Got: " + data.x + " " + data.y); // Draw a blue circle fill(0,0,255); noStroke(); ellipse(data.x, data.y, 20, 20); } );

语义与服务端完全对称:监听名为mouse的事件,收到数据后读取data.x、data.y,在对应坐标画一个半径为 20 的蓝色圆点。蓝色圆点 = 来自其他客户端(经服务器转发)的鼠标轨迹,这是识别消息是否"走了一圈"回来的直观视觉信号。

4. 发送事件:socket.emit(event, data)

发送侧由 p5.js 的mouseDragged()驱动(第 32-53 行):

function mouseDragged() { // Draw some white circles fill(255); noStroke(); ellipse(mouseX,mouseY,20,20); // Send the mouse coordinates sendmouse(mouseX,mouseY); } // Function for sending to the socket function sendmouse(xpos, ypos) { // We are sending! console.log("sendmouse: " + xpos + " " + ypos); // Make a little object with and y var data = { x: xpos, y: ypos }; // Send that object to the socket socket.emit('mouse',data); }

要点:

  • 本地先画:mouseDragged()先在本机画一个白色圆点,再把坐标发出去——白色圆点 = 自己的鼠标轨迹。
  • 数据序列化:坐标被打包成{x, y}对象后通过socket.emit('mouse', data)发送。socket.io 在底层自动完成 JSON 序列化,服务端收到的data就是可直接访问属性的对象。
  • 事件名约定:客户端emit('mouse')与服务端socket.on('mouse')、以及服务端broadcast.emit('mouse')与客户端socket.on('mouse'),四处的字符串'mouse'必须完全一致,事件名是双方约定的"频道"。

四、完整消息链路:一次拖拽发生了什么

把服务端与客户端串起来,一次跨浏览器拖拽的完整链路是:

客户端 A 拖拽 │ mouseDragged() 触发 │ 本地画白色圆点 │ sendmouse(mouseX, mouseY) 构造 {x, y} ▼ socket.emit('mouse', data) ──────────► 服务器 socket.on('mouse', data) │ 打印 Received: 'mouse' x y ▼ socket.broadcast.emit('mouse', data) │ ┌─────────────────────────────────────┘ ▼ 客户端 B / C 的 socket.on('mouse', data) │ 打印 Got: x y ▼ 在 (data.x, data.y) 画蓝色圆点
  • A 自己:只看到白色轨迹(本地绘制);
  • B、C 等所有其他客户端:看到来自 A 的蓝色轨迹(服务器转发);
  • 服务器:全程只做接收与转发,同时打印日志用于调试。

这一"白点本地画、蓝点远端画"的双色约定,让多标签页测试时能一眼确认实时链路是否打通。

五、运行与验证

在本地副本中按以下步骤运行(仓库本身是只读归档,需先将其克隆或复制到本地):

# 1. 进入示例目录 cd Node/sockets # 2. 安装依赖 npm install # 3. 启动服务器 node server.js # 输出类似:Example app listening at http://:::3000 # 4. 浏览器访问 http://localhost:3000

验证要点:

  1. 单页自测:打开http://localhost:3000,拖拽鼠标应看到白色圆点轨迹,浏览器控制台交替打印sendmouse: x y(发送)与Got: x y(接收)。
  2. 跨页验证:再开一个新标签页访问同一地址(或直接复制标签页)。在任意一个页面拖拽,另一个页面应实时出现蓝色圆点轨迹——这就是"多个浏览器会话共享画布"的最小雏形。
  3. 服务端日志:终端里能看到每次连接的We have a new client: <id>、每条消息的Received: 'mouse' x y,以及关闭页面时的Client has disconnected。
  4. 本地环境提示:io.connect('http://localhost:3000')仅适用于本机开发;若页面通过其他域名/端口访问,需同步修改客户端连接地址。

六、进阶对比:broadcast与"发给所有人"

本节示例只用到了socket.broadcast.emit()(发给除发送者外的所有客户端)。仓库中另一个更完整的实战示例 CodingChallenges/CC_032.2_agar.io_sockets/Node(agar.io 风格多人游戏)展示了 socket.io 另外两种消息模式的组合用法:

// 服务器维护全局状态 var blobs = []; setInterval(heartbeat, 33); function heartbeat() { io.sockets.emit('heartbeat', blobs); // 发给所有人,包括发送者 }

对比三种发送方式(参考 Node/sockets/server.js 与 CC_032.2 的 server.js):

调用方式语义适用场景
socket.emit(...)只发给当前这一个客户端服务端对单客户端的应答
socket.broadcast.emit(...)发给除发送者外的所有其他客户端共享画布中的"转发别人的轨迹"
io.sockets.emit(...)发给当前连接的所有客户端(含发送者)全局状态同步、心跳广播

agario 示例还展示了服务端持有权威状态(blobs数组)的做法:客户端通过socket.emit('start')注册自己、socket.emit('update')持续上报位置,服务器用setInterval(heartbeat, 33)(约 30fps)定时把全体状态推给所有客户端。这种"客户端上报 + 服务器广播"的模式,正是从本节单事件转发迈向多人在线同步的典型演进路径。

七、版本与兼容性注意事项

以仓库实际内容为准,本节示例存在两处版本口径,需要特别留意:

  • 客户端 CDN:Node/sockets/public/index.html 引入的是socket.io-1.4.5.js(socket.io 1.x 时代的浏览器端库);
  • 服务端依赖:Node/sockets/package.json 声明的是socket.io: ^2.4.0(socket.io 2.x)。

客户端库版本与服务器依赖版本分属两个大版本区间,实际运行时可能存在协议握手兼容性差异。从源码结构看,这是归档仓库中示例代码的原始状态;若要在新项目中复刻本教程,建议将客户端 CDN 与服务器端 socket.io 统一到同一大版本(例如都使用 2.x 对应的客户端库),以保证连接稳定。同理,express ^4.13.4、p5.js 1.4.1 等版本也以仓库当前声明为准。

八、后续延伸

  • 12.4 共享画布:本节的连接与事件机制将在 _Tutorials/12-websockets/12.4-shared-drawing-canvas.md 中被整合为完整的多人协作画布,并系统讲解"如何在客户端之间广播消息"。
  • 前置基础:若对 p5.js 还不熟悉,可先学习 beginners/p5js 入门系列;数据与异步回调相关背景可参考 _Tutorials/10-working-with-data。
  • 实战范本:CodingChallenges/CC_032.2_agar.io_sockets/Node 展示了同一套"Express + socket.io + p5.js"技术栈在多人实时游戏中的应用,包含服务器状态维护、心跳广播与客户端位置同步的完整实现。

至此,客户端与服务器之间的实时通道已经打通:服务器用require('socket.io')(server)在同一端口上挂载 WebSocket 能力,客户端用io.connect()建立连接,双方以emit/on按自定义事件名收发数据,并用broadcast实现跨客户端转发。下一步,就可以在这个通道上装载真正的业务逻辑了。

  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载
上一篇:uWebSockets.js中的Pub/Sub模式实现详解
下一篇:技术视角:OpenCore Legacy Patcher如何重塑旧Mac的生命周期管理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询