- Socket.IO 教程
- Socket.IO - 首页
- Socket.IO - 概述
- Socket.IO - 环境配置
- Socket.IO - Hello world
- Socket.IO - 事件处理
- Socket.IO - 广播
- Socket.IO - 命名空间
- Socket.IO - 房间
- Socket.IO - 错误处理
- Socket.IO - 日志记录和调试
- Socket.IO - 内部机制
- Socket.IO - 聊天应用示例
- Socket.IO 有用资源
- Socket.IO 快速指南
- Socket.IO - 有用资源
- Socket.IO - 讨论
Socket.IO 快速指南
Socket.IO - 概述
Socket.IO 是一个用于实时 Web 应用的 JavaScript 库。它使 Web 客户端和服务器之间能够进行实时、双向通信。它包含两部分——在浏览器中运行的客户端库和用于 node.js 的服务器端库。这两个组件具有相同的 API。
实时应用
实时应用 (RTA) 是一种在用户感知为即时或当前的时间段内运行的应用程序。
一些实时应用程序的示例包括:
即时通讯软件——例如 WhatsApp、Facebook Messenger 等聊天应用。您无需刷新应用/网站即可接收新消息。
推送通知——当有人在 Facebook 上标记您时,您会立即收到通知。
协作应用程序——例如 Google Docs,允许多人同时更新同一文档并将更改应用于所有人的实例。
在线游戏——例如反恐精英、使命召唤等游戏,也是实时应用程序的一些示例。
为什么选择 Socket.IO?
使用流行的 Web 应用堆栈(如 LAMP (PHP))编写实时应用程序传统上非常困难。它涉及轮询服务器以查找更改,跟踪时间戳,并且速度比应有的速度慢得多。
套接字传统上是大多数实时系统构建的解决方案,它在客户端和服务器之间提供双向通信通道。这意味着服务器可以将消息推送到客户端。每当发生事件时,服务器都会获取它并将其推送到相关连接的客户端。
Socket.IO 非常流行,微软 Office、Yammer、Zendesk、Trello 等众多组织都使用它来构建强大的实时系统。它是GitHub 上最强大的JavaScript 框架之一,也是最依赖的 NPM(Node 包管理器)模块。Socket.IO 还拥有庞大的社区,这意味着很容易找到帮助。
ExpressJS
我们将使用 Express 构建 Socket.IO 将与之协同工作的 Web 服务器。可以使用任何其他 Node 服务器端框架,甚至 Node HTTP 服务器。但是,ExpressJS 使定义路由和其他内容变得容易。要了解更多关于 Express 的信息并对其有一个基本的了解,请访问 – ExpressJS 教程。
Socket.IO - 环境配置
要开始使用Socket.IO进行开发,您需要安装Node和npm(Node 包管理器)。如果您没有这些,请访问Node 设置以在您的本地系统上安装 Node。通过在终端中运行以下命令来确认 Node 和 npm 是否已安装。
node --version npm --version
您应该获得类似于以下的输出:
v14.17.0
6.14.13 打开您的终端,并在终端中输入以下内容以创建一个新文件夹并输入以下命令:
$ mkdir test-project $ cd test-proect $ npm init
它会问你一些问题;请按以下方式回答:
这将创建一个'package.json node.js'配置文件。现在我们需要安装Express和Socket.IO。要安装这些并将它们保存到package.json 文件,请在您的终端中,进入项目目录输入以下命令:
npm install --save express socket.io
最后一件事情是,我们应该不断重新启动服务器。当我们进行更改时,我们需要一个名为nodemon的工具。要安装 nodemon,请打开您的终端并输入以下命令:
npm install -g nodemon
每当您需要启动服务器时,请勿使用node app.js,而应使用nodemon app.js。这将确保您无需在更改文件时重新启动服务器。它加快了开发过程。
现在,我们已经设置好了开发环境。现在让我们开始使用 Socket.IO 开发实时应用程序。
Socket.IO - Hello world
在下一章中,我们将讨论使用 Socket.IO 库和 ExpressJS 的基本示例。
示例
首先,创建一个名为app.js的文件,并输入以下代码以设置 Express 应用程序:
var app = require('express')(); var http = require('http').Server(app); app.get('/', function(req, res){ res.sendFile('E:/test/index.html'); }); http.listen(3000, function(){ console.log('listening on *:3000'); });
我们需要一个index.html文件来提供服务,创建一个名为 index.html 的新文件,并在其中输入以下代码:
<!DOCTYPE html> <html> <head><title>Hello world</title></head> <body>Hello world</body> </html>
要测试它是否有效,请转到终端并使用以下命令运行此应用程序:
nodemon app.js
这将在 localhost:3000 上运行服务器。转到浏览器并输入 localhost:3000 进行检查。如果一切顺利,页面上将打印一条消息“Hello World”。
以下是另一个示例(这需要 Socket.IO),每当用户访问此页面时,它都会记录“用户已连接”,每当有人离开/关闭此页面时,它都会记录“用户已断开连接”。
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.sendFile('E:/test/index.html'); }); //Whenever someone connects this gets executed io.on('connection', function(socket){ console.log('A user connected'); //Whenever someone disconnects this piece of code executed socket.on('disconnect', function () { console.log('A user disconnected'); }); }); http.listen(3000, function(){ console.log('listening on *:3000'); });
require('socket.io')(http) 创建一个附加到 http 服务器的新 socket.io 实例。io.on 事件处理程序使用 socket 对象处理其中的连接、断开连接等事件。
我们已设置服务器以在连接和断开连接时记录消息。我们现在必须包含客户端脚本并在其中初始化 socket 对象,以便客户端可以在需要时建立连接。该脚本由我们的 io 服务器在'/socket.io/socket.io.js'处提供。
完成上述过程后,index.html 文件将如下所示:
<!DOCTYPE html> <html> <head><title>Hello world</title></head> <script src="/socket.io/socket.io.js"></script> <script> var socket = io(); </script> <body>Hello world</body> </html>
如果您现在转到 localhost:3000(确保您的服务器正在运行),您将在浏览器中看到打印的Hello World。现在检查您的服务器控制台日志,它将显示以下消息:
A user connected
如果您刷新浏览器,它将断开 socket 连接并重新创建。您可以在控制台日志中看到以下内容:
A user connected A user disconnected A user connected
Socket.IO - 事件处理
Sockets 基于事件工作。有一些保留事件,可以使用服务器端的 socket 对象访问这些事件。
它们是:
- 连接
- 消息
- 断开连接
- 重新连接
- Ping
- 加入和
- 离开。
客户端 socket 对象还为我们提供了一些保留事件,它们是:
- 连接
- Connect_error
- Connect_timeout
- Reconnect 等。
现在,让我们看一个使用 SocketIO 库处理事件的示例。
示例 1
In the Hello World example, we used the connection and disconnection events to log when a user connected and left. Now we will be using the message event to pass message from the server to the client. To do this, modify the io.on ('connection', function(socket)) as shown below –var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.sendFile('E:/test/index.html'); }); io.on('connection', function(socket){ console.log('A user connected'); // Send a message after a timeout of 4seconds setTimeout(function(){ socket.send('Sent a message 4seconds after connection!'); }, 4000); socket.on('disconnect', function () { console.log('A user disconnected'); }); }); http.listen(3000, function(){ console.log('listening on *:3000'); });
这将在客户端连接四秒钟后向我们的客户端发送一个名为message(内置)的事件。socket 对象上的 send 函数与 'message' 事件关联。
现在,我们需要在客户端处理此事件,为此,请将 index.html 页面的内容替换为以下内容:
<!DOCTYPE html> <html> <head><title>Hello world</title></head> <script src="/socket.io/socket.io.js"></script> <script> var socket = io(); socket.on('message', function(data){document.write(data)}); </script> <body>Hello world</body> </html>
我们现在正在客户端处理“message”事件。当您现在在浏览器中访问该页面时,您将看到以下屏幕截图。
4 秒钟后服务器发送 message 事件后,我们的客户端将处理它并产生以下输出:
注意——我们在这里发送了一段文本字符串;我们也可以在任何事件中发送对象。
Message 是 API 提供的内置事件,但在实际应用中并没有多大用处,因为我们需要能够区分事件。
为了允许这样做,Socket.IO 使我们能够创建自定义事件。您可以使用socket.emit函数创建和触发自定义事件。以下代码发出一个名为testerEvent的事件:
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.sendFile('E:/test/index.html'); }); io.on('connection', function(socket){ console.log('A user connected'); // Send a message when setTimeout(function(){ // Sending an object when emmiting an event socket.emit('testerEvent', { description: 'A custom event named testerEvent!'}); }, 4000); socket.on('disconnect', function () { console.log('A user disconnected'); }); }); http.listen(3000, function(){ console.log('listening on localhost:3000'); });
要在客户端处理此自定义事件,我们需要一个监听 testerEvent 事件的监听器。以下代码在客户端处理此事件:
<!DOCTYPE html> <html> <head><title>Hello world</title></head> <script src="/socket.io/socket.io.js"></script> <script> var socket = io(); socket.on('testerEvent', function(data){document.write(data.description)}); </script> <body>Hello world</body> </html>
这将与我们之前的示例一样工作,在这种情况下,事件为 testerEvent。当您打开浏览器并转到 localhost:3000 时,您将看到:
Hello world
四秒钟后,将触发此事件,浏览器将文本更改为:
A custom event named testerEvent!
示例 2
我们也可以从客户端发出事件。要从客户端发出事件,请使用 socket 对象上的 emit 函数。
<!DOCTYPE html> <html> <head><title>Hello world</title></head> <script src="/socket.io/socket.io.js"></script> <script> var socket = io(); socket.emit('clientEvent', 'Sent an event from the client!'); </script> <body>Hello world</body> </html>
要处理这些事件,请在服务器上的 socket 对象上使用on 函数。
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.sendFile('E:/test/index.html'); }); io.on('connection', function(socket){ socket.on('clientEvent', function(data){ console.log(data); }); }); http.listen(3000, function(){ console.log('listening on localhost:3000'); });
因此,现在如果我们转到 localhost:3000,我们将得到一个名为clientEvent的自定义事件被触发。此事件将由服务器通过记录以下内容来处理:
Sent an event from the client!
Socket.IO - 广播
广播意味着向所有已连接的客户端发送消息。广播可以在多个级别进行。我们可以向所有已连接的客户端发送消息,向命名空间上的客户端发送消息,以及向特定房间中的客户端发送消息。要向所有客户端广播事件,我们可以使用io.sockets.emit方法。
注意——这将向所有已连接的客户端发出事件(甚至可能是触发此事件的套接字)。
在此示例中,我们将向所有用户广播已连接客户端的数量。更新app.js文件以包含以下内容:
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.sendFile('E:/test/index.html'); }); var clients = 0; io.on('connection', function(socket){ clients++; io.sockets.emit('broadcast',{ description: clients + ' clients connected!'}); socket.on('disconnect', function () { clients--; io.sockets.emit('broadcast',{ description: clients + ' clients connected!'}); }); }); http.listen(3000, function(){ console.log('listening on localhost:3000'); });
在客户端,我们只需要处理广播事件:
<!DOCTYPE html> <html> <head><title>Hello world</title></head> <script src="/socket.io/socket.io.js"></script> <script> var socket = io(); socket.on('broadcast',function(data){ document.body.innerHTML = ''; document.write(data.description); }); </script> <body>Hello world</body> </html>
如果您连接四个客户端,您将获得以下结果:
这是向所有人发送事件。现在,如果我们想向所有人发送事件,但不是导致该事件的客户端(在上一个示例中,它是新客户端连接导致的),我们可以使用socket.broadcast.emit。
让我们向新用户发送欢迎消息,并向其他用户广播他/她加入的消息。因此,在您的 app.js 文件中,在客户端连接时向他发送欢迎消息,并将已连接客户端数量广播给其他人。
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.sendFile('E:/test/index.html'); }); var clients = 0; io.on('connection', function(socket){ clients++; socket.emit('newclientconnect',{ description: 'Hey, welcome!'}); socket.broadcast.emit('newclientconnect',{ description: clients + ' clients connected!'}) socket.on('disconnect', function () { clients--; socket.broadcast.emit('newclientconnect',{ description: clients + ' clients connected!'}) }); }); http.listen(3000, function(){ console.log('listening on localhost:3000'); });
以及处理此事件的 html:
<!DOCTYPE html> <html> <head><title>Hello world</title></head> <script src="/socket.io/socket.io.js"></script> <script> var socket = io(); socket.on('newclientconnect',function(data){ document.body.innerHTML = ''; document.write(data.description); }); </script> <body>Hello world</body> </html>
现在,最新的客户端会收到欢迎消息,而其他客户端会收到当前连接到服务器的客户端数量。
Socket.IO - 命名空间
Socket.IO 允许您为套接字“命名空间”,这本质上意味着分配不同的端点或路径。这是一个有用的功能,可以最大限度地减少资源(TCP 连接)的数量,同时通过引入通信通道之间的分离来分离应用程序中的关注点。多个命名空间实际上共享相同的 WebSockets 连接,从而节省了服务器上的套接字端口。
命名空间是在服务器端创建的。但是,客户端通过向服务器发送请求来加入它们。
默认命名空间
根命名空间 '/' 是默认命名空间,如果客户端在连接到服务器时未指定命名空间,则客户端将加入该命名空间。使用客户端 socket 对象进行的所有服务器连接都连接到默认命名空间。例如:
var socket = io();
这将客户端连接到默认命名空间。此命名空间连接上的所有事件都将由服务器上的io 对象处理。所有之前的示例都使用默认命名空间与服务器进行通信。
自定义命名空间
我们可以创建自己的自定义命名空间。要在服务器端设置自定义命名空间,我们可以调用 'of' 函数:
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.sendFile('E:/test/index.html');}); var nsp = io.of('/my-namespace'); nsp.on('connection', function(socket){ console.log('someone connected'); nsp.emit('hi', 'Hello everyone!'); }); http.listen(3000, function(){ console.log('listening on localhost:3000'); });
现在,要将客户端连接到此命名空间,您需要将命名空间作为参数提供给客户端的io 构造函数调用以创建连接和套接字对象。
例如,要连接到上述命名空间,请使用以下 HTML:
<!DOCTYPE html> <html> <head><title>Hello world</title></head> <script src="/socket.io/socket.io.js"></script> <script> var socket = io('/my-namespace'); socket.on('hi',function(data){ document.body.innerHTML = ''; document.write(data); }); </script> <body></body> </html>
每次有人连接到此命名空间时,他们都会收到一个 'hi' 事件,显示消息“Hello everyone!”。
Socket.IO - 房间
在每个命名空间内,您还可以定义套接字可以加入和离开的任意通道。这些通道称为房间。房间用于进一步分离关注点。房间也像命名空间一样共享相同的套接字连接。使用房间时需要注意的一点是,它们只能在服务器端加入。
加入房间
您可以调用套接字上的join方法将套接字订阅到给定的通道/房间。例如,让我们创建名为'room-<room-number>'的房间并加入一些客户端。一旦此房间已满,就创建另一个房间并将客户端加入到那里。
注意 - 我们目前在默认命名空间(即 '/')上执行此操作。您也可以以相同的方式在自定义命名空间中实现此功能。
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.sendfile('index.html'); }); var roomno = 1; io.on('connection', function(socket){ socket.join("room-"+roomno); //Send this event to everyone in the room. io.sockets.in("room-"+roomno).emit('connectToRoom', "You are in room no. "+roomno); }) http.listen(3000, function(){ console.log('listening on localhost:3000'); });
只需在客户端处理此connectToRoom事件。
<!DOCTYPE html> <html> <head><title>Hello world</title></head> <script src="/socket.io/socket.io.js"></script> <script> var socket = io(); socket.on('connectToRoom',function(data){ document.body.innerHTML = ''; document.write(data); }); </script> <body></body> </html>
现在,如果您连接三个客户端,则前两个客户端将收到以下消息:
You are in room no. 1
离开房间
要离开房间,您需要调用 leave 函数,就像您在套接字上调用 join 函数一样。
例如 - 要离开房间 'room-1',
socket.leave("room-"+roomno);
Socket.IO - 错误处理
到目前为止,我们一直在本地服务器上工作,这几乎不会给我们带来与连接、超时等相关的错误。但是,在现实生活中的生产环境中,处理此类错误至关重要。因此,我们现在将讨论如何在客户端处理连接错误。
客户端 API 为我们提供了以下内置事件:
Connect - 客户端成功连接时。
Connecting - 客户端正在连接过程中时。
Disconnect - 客户端断开连接时。
Connect_failed - 与服务器的连接失败时。
Error - 从服务器发送错误事件时。
Message - 服务器使用send函数发送消息时。
Reconnect - 成功重新连接到服务器时。
Reconnecting - 客户端正在重新连接过程中时。
Reconnect_failed - 重新连接尝试失败时。
为了处理错误,我们可以使用我们在客户端创建的 out-socket 对象来处理这些事件。
例如 - 如果我们的连接失败,我们可以使用以下代码再次连接到服务器:
socket.on('connect_failed', function() { document.write("Sorry, there seems to be an issue with the connection!"); })
Socket.IO - 日志记录和调试
Socket.IO 使用一个非常著名的调试模块,该模块由 ExpresJS 的主要作者开发,称为 debug。早期的 Socket.IO 习惯于将所有内容记录到控制台,这使得调试问题相当困难。在 v1.0 版本发布后,您可以指定要记录的内容。
服务器端
查看可用信息的最佳方法是使用 * -
DEBUG=* node app.js
这将为您的服务器控制台着色并输出发生的所有事件。例如,我们可以考虑以下屏幕截图。
客户端
将此粘贴到控制台,单击回车并刷新页面。这将再次将与 Socket.io 相关的所有内容输出到您的控制台。
localStorage.debug = '*';
您可以限制输出以使用以下命令获取来自套接字的传入数据的调试信息。
localStorage.debug = 'socket.io-client:socket';
如果您使用第二个语句记录信息,则可以看到如下所示的结果:
有一篇关于 socket.io 调试的非常好的博客文章这里。
Socket.IO - 内部机制
在本章中,我们将讨论回退、使用 Socket.IO 进行连接、事件和消息。
回退
Socket.IO 具有许多底层传输机制,这些机制处理由于跨浏览器问题、WebSocket 实现、防火墙、端口阻塞等而产生的各种约束。
尽管 W3C 为 WebSocket API 定义了一个规范,但它在实现方面仍然不足。Socket.IO 为我们提供了回退机制,可以处理此类问题。如果我们使用原生 API 开发应用程序,则必须自己实现回退。Socket.IO 按以下顺序涵盖大量回退:
- WebSockets
- FlashSocket
- XHR 长轮询
- XHR 多部分流
- XHR 轮询
- JSONP 轮询
- iframe
使用 Socket.IO 连接
Socket.IO 连接始于握手。这使得握手成为协议的一个特殊部分。除了握手之外,协议中的所有其他事件和消息都通过套接字传输。
Socket.IO 旨在与 Web 应用程序一起使用,因此假定这些应用程序始终能够使用 HTTP。正是由于这个原因,Socket.IO 握手通过 HTTP 使用握手 URI(传递给 connect 方法)上的 POST 请求进行。
事件和消息
WebSocket 原生 API 只发送消息。Socket.IO 在这些消息之上提供了一个附加层,这允许我们创建事件,并再次帮助我们通过分离发送的不同类型的消息来轻松开发应用程序。
原生 API 只以纯文本形式发送消息。Socket.IO 也解决了这个问题。它为我们处理数据的序列化和反序列化。
我们有一个官方的 Web 客户端 API。对于其他客户端,例如原生手机、其他应用程序客户端,我们也可以使用以下步骤使用 Socket.IO。
步骤 1 - 需要使用上面讨论的相同连接协议建立连接。
步骤 2 - 消息需要与 Socket.IO 指定的格式相同。此格式使 Socket.IO 能够确定消息的类型和消息中发送的数据以及对操作有用的某些元数据。
消息格式为:
[type] : [id ('+')] : [endpoint] (: [data]
上述命令中的参数解释如下:
Type 是一个一位整数,指定它是哪种类型的消息。
ID 是消息 ID,一个用于确认的增量整数。
Endpoint 是消息要传递到的套接字端点……
Data 是要传递到套接字的关联数据。对于消息,它被视为纯文本;对于其他事件,它被视为 JSON。
在下一章中,我们将编写一个 Socket.IO 聊天应用程序。
Socket.IO - 聊天应用示例
既然我们已经熟悉了 Socket.IO,让我们编写一个聊天应用程序,我们可以使用它在不同的聊天室中聊天。我们将允许用户选择用户名并允许他们使用用户名聊天。所以首先,让我们设置我们的 HTML 文件来请求用户名:
<!DOCTYPE html> <html> <head><title>Hello world</title></head> <script src="/socket.io/socket.io.js"></script> <script> var socket = io(); </script> <body> <input type="text" name="name" value="" placeholder="Enter your name!"> <button type="button" name="button">Let me chat!</button> </body> </html>
现在我们已经设置了 HTML 来请求用户名,让我们创建服务器来接受来自客户端的连接。我们将允许用户使用setUsername事件发送他们选择的用户名。如果用户存在,我们将通过userExists事件进行响应,否则使用userSet事件。
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.sendFile('E:/test/index.html');}); users = []; io.on('connection', function(socket){ console.log('A user connected'); socket.on('setUsername', function(data){ if(users.indexOf(data) > -1){ users.push(data); socket.emit('userSet', {username: data}); } else { socket.emit('userExists', data + ' username is taken! Try some other username.'); } }) }); http.listen(3000, function(){ console.log('listening on localhost:3000'); });
当人们单击按钮时,我们需要将用户名发送到服务器。如果用户存在,我们将显示错误消息;否则,我们将显示消息屏幕:
<!DOCTYPE html> <html> <head><title>Hello world</title></head> <script src="/socket.io/socket.io.js"></script> <script> var socket = io(); function setUsername(){ socket.emit('setUsername', document.getElementById('name').value); }; var user; socket.on('userExists', function(data){ document.getElementById('error-container').innerHTML = data; }); socket.on('userSet', function(data){ user = data.username; document.body.innerHTML = '<input type="text" id="message">\ <button type="button" name="button" onclick="sendMessage()">Send</button>\ <div id="message-container"></div>'; }); function sendMessage(){ var msg = document.getElementById('message').value; if(msg){ socket.emit('msg', {message: msg, user: user}); } } socket.on('newmsg', function(data){ if(user){ document.getElementById('message-container').innerHTML +='<div><b>' + data.user + '</b>: ' + data.message + '</div>' } }) </script> <body> <div id="error-container"></div> <input id="name" type="text" name="name" value="" placeholder="Enter your name!"> <button type="button" name="button" onclick="setUsername()">Let me chat!</button> </body> </html>
现在,如果您使用相同的用户名连接两个客户端,它将给出如下所示的错误消息:
一旦您提供了可接受的用户名,您将进入一个带有消息框和发送消息按钮的屏幕。现在,我们必须处理并将消息定向到连接的客户端。为此,请修改您的 app.js 文件以包含以下更改:
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); app.get('/', function(req, res){ res.sendFile('E:/test/index.html');}); users = []; io.on('connection', function(socket){ console.log('A user connected'); socket.on('setUsername', function(data){ console.log(data); if(users.indexOf(data) > -1){ socket.emit('userExists', data + ' username is taken! Try some other username.'); } else { users.push(data); socket.emit('userSet', {username: data}); } }); socket.on('msg', function(data){ //Send message to everyone io.sockets.emit('newmsg', data); }) }); http.listen(3000, function(){ console.log('listening on localhost:3000'); });
现在将任意数量的客户端连接到您的服务器,为他们提供用户名并开始聊天!在以下示例中,我们连接了名为 Ayush 和 Harshit 的两个客户端,并从这两个客户端发送了一些消息: