- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
本篇对应 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.md | Node.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 install2. 服务器骨架: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验证要点:
- 单页自测:打开
http://localhost:3000,拖拽鼠标应看到白色圆点轨迹,浏览器控制台交替打印sendmouse: x y(发送)与Got: x y(接收)。 - 跨页验证:再开一个新标签页访问同一地址(或直接复制标签页)。在任意一个页面拖拽,另一个页面应实时出现蓝色圆点轨迹——这就是"多个浏览器会话共享画布"的最小雏形。
- 服务端日志:终端里能看到每次连接的
We have a new client: <id>、每条消息的Received: 'mouse' x y,以及关闭页面时的Client has disconnected。 - 本地环境提示:
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)
相关推荐
基于 Node.js、Express、socket.io 与 p5.js 的多人共享协作画布实战指南(Coding Train WebSockets 教程)
基于 Node.js、Express、socket.io 与 p5.js 的多人共享协作画布实战指南(Coding Train WebSockets 教程) 导
教程文档示例工程基于 Node.js、Express、Socket.io 与 p5.js 构建多人共享绘图画布:Coding Train WebSockets 系列实战指南
基于 Node.js、Express、Socket.io 与 p5.js 构建多人共享绘图画布:Coding Train WebSockets 系列实战指南 本
教程文档示例工程BepisPlugins:解锁Illusion游戏无限可能的7大必备插件集合 🎮
BepisPlugins:解锁Illusion游戏无限可能的7大必备插件集合 🎮 你是否厌倦了Illusion/Illgames旗下游戏的有限功能?想要为你的
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考