了解 Socket.io 的基础知识
本文最初发布在我的网站上。如果您喜欢,可以在我的博客上找到一些有趣的文章。
最近做了大量的 API 和微服务开发,我一直在尝试寻找一些快速演示的灵感来提升我的技能。在我正在进行的一个项目中,我计划添加一个根据用户活动实时更新的 feed。我不确定该怎么做,一开始我考虑使用 RabbitMQ,但快速搜索后,我找到了 WebSockets 和Socket.io。如果你想了解什么是 WebSocket,可以观看这个超短视频来理解其基本概念。
我没有直接为我的项目构建用户动态,而是决定先构建一个快速聊天演示。虽然有很多文章和视频讲解了如何使用 socket.io 创建聊天,但大多数都没有详细解释所有相关部分是如何协同工作的,或者只是一些可以在本地运行的小演示,无法“部署”到生产环境中。因此,我参考了所有这些示例来构建我的聊天,并记录了所有不清楚的地方,最后以可部署到服务器的方式构建了它(甚至还创建了一个 Docker 镜像!)。以下是我所有的笔记。
聊天服务器和客户端的职责
我们的聊天应用服务器将承担以下职责:
- 向用户提供 HTML/CSS/JS 客户端文件
- 启动Socket.io连接
- 向客户端提供 socket.io 库(可选,因为客户端也可以从 CDN 加载它)
- 向所有连接的客户端广播事件(如新的聊天消息)
当用户从浏览器连接到我们的服务器时,他将收到 HTML/CSS/JS 客户端文件,这些文件将:
- 加载 socket.io 客户端库(从我们的服务器或 CDN)
- 与我们服务器上运行的 Socket.io 建立连接
- 要求用户输入他的姓名,以便我们在聊天中识别他
- 向运行在我们服务器上的 Socket.io 发送事件或从其接收事件
- 通过 JavaScript 将我们自己的消息添加到聊天中
聊天服务器详细信息
首先要用“npm init”启动我们的 Node.js 项目,因为稍后我们需要安装依赖项。我们可以使用 Node 的 http 模块创建一个静态服务器,向客户端发送任何类型的文件,在我们的例子中是 html、css 和 js。我在 Mozilla 文档中找到了这个示例,它正是我想要的。没有框架,只是一个能够发送 html、css、js、图片等等的 http 服务器。他们还逐行解释了它的工作原理,所以我就不赘述了。我将服务器代码放在一个名为 server.js 的文件中。由于我将使用名为“client”的文件夹,因此我仅对 Mozilla 示例中的端口号和文件读取路径进行了更改:
var filePath = './client' + request.url;
console.log(filePath)
if (filePath == './client/') {
filePath = './client/index.html';
}
下一步是使用“ npm i socket.io ”安装 socket.io 依赖项,将其包含在我们的 server.js 文件中,并在检测到连接时记录一些内容:
var io = require('socket.io')(app);
// starts socket
io.on('connection', function (socket) {
console.log('Socket.io started.....')
// Manage all socket.io events next...
socket.on('new-connection', (data) => {
// captures event when new clients join
console.log(`new-connection event received`)
.........
})
});
我还介绍了如何捕获名为“new-connection”的事件,目前该事件只会在控制台中打印一些内容。现在让我们转到客户端。
聊天客户端详情
如前所述,我将所有客户端文件(html、css 和 js)放在名为client 的文件夹中。index.html 文件非常简单:
- 在标头中,我们从 CDN 加载 socket.io 客户端库,尽管我也包含了从我们自己的服务器加载它的代码
- 同样在标题中,我们加载了 script.js 文件。
- 正文仅包含一个用于存放所有聊天消息的 div 容器和一个用于提交新消息的表单。
您可以在此 GitHub Gist或直接在repo中找到 index.html 文件的代码。
在客户端的script.js文件中,我做的第一件事是通过 socket.io 从客户端连接到服务器。由于我在 script.js 文件之前加载了 socket.io 库,因此我可以使用io() 函数创建一个连接到服务器的套接字,并使用emit()函数发送一个名为“new-connection”的基本事件以及用户名:
/**
* Use io (loaded earlier) to connect with the socket instance running in your server.
* IMPORTANT! By default, socket.io() connects to the host that
* served the page, so we dont have to pass the server url
*/
var socket = io();
//prompt to ask user's name
const name = prompt('Welcome! Please enter your name:')
// emit event to server with the user's name
socket.emit('new-connection', {username: name})
此时,如果我使用“ node server.js ”启动服务器并打开浏览器,我会收到提示,输入名称后,我将连接到套接字服务器并在服务器控制台中看到如下内容:
$ npm start
> chatsocket.io@1.0.0 start /d/Projects/chatSocket.io
> node server.js
HTTP Server running at http://127.0.0.1:3000/
request /
./client/
request /script.js
./client/script.js
request /style.css
./client/style.css
Socket.io started.....
request /favicon.ico
./client/favicon.ico
到目前为止,我能够:
- 启动一个静态服务器,发送客户端文件并打开 socket.io 连接
- 通过 socket.io 将客户端连接到服务器并发出名为“new-connection”的事件
- 捕获服务器中的“new-connection”事件并将其打印到控制台
完成聊天应用程序唯一缺少的是:
- 能够将消息与用户姓名关联起来
- 添加我们发送的消息到聊天容器 div
- 向服务器发出包含已发送消息的事件
- 将服务器收到的聊天消息广播给所有连接的客户端
我们可以在客户端的script.js文件中将发送的消息添加到 chat-container div 中。我们只需添加一个事件监听器来捕获表单提交事件,并在每次提交时创建一个新的 div 来包含 chat-container 中的消息。由于接收其他用户的消息时也需要执行此操作,因此我创建了一个名为addMessage(data, type)的函数,可以多次调用。此外,我还会触发一个名为“new-message”的事件,向服务器发送一个包含消息和客户端套接字 ID 的对象。
// get elements of our html page
const chatContainer = document.getElementById('chat-container')
const messageInput = document.getElementById('messageInput')
const messageForm = document.getElementById('messageForm')
messageForm.addEventListener('submit', (e) => {
// avoid submit the form and refresh the page
e.preventDefault()
// check if there is a message in the input
if(messageInput.value !== ''){
let newMessage = messageInput.value
//sends message and our id to socket server
socket.emit('new-message', {user: socket.id, message: newMessage})
addMessage({message: newMessage}, 'my' )
//resets input
messageInput.value = ''
}
})
// receives two params, the message and if it was sent by you
// so we can style them differently
function addMessage(data, type){
const messageElement = document.createElement('div')
messageElement.classList.add('message')
if(type === 'my'){
messageElement.classList.add('my-message')
messageElement.innerText = `${data.message}`
}else if(type === 'others'){
messageElement.classList.add('others-message')
messageElement.innerText = `${data.user}: ${data.message}`
}else{
messageElement.innerText = `${data.message}`
}
// adds the new div to the message container div
chatContainer.append(messageElement)
}
请注意,我还为消息添加了不同的样式,具体取决于它们是属于用户还是从其他人那里收到的。
下一步是在我们的server.js中正确处理 'new-connection' 和 'new-message' 事件。在 'new-connection' 事件中,我将客户端的套接字 ID 和用户名存储为名为users 的对象中的键值对。然后在 'new-message' 事件中,我使用收到的套接字 ID 找到对应的用户名,并使用broadcast()函数将消息信息发送给除最初发出该事件的客户端之外的所有已连接的客户端。
// we'll store the users in this object as socketId: username
const users = {}
var io = require('socket.io')(app);
// starts socket
io.on('connection', function (socket) {
console.log('Socket.io started.....')
// Manage all socket.io events next...
socket.on('new-connection', (data) => {
console.log(`new-connection event ${data.username}`)
// adds user to list
users[socket.id] = data.username
socket.emit('welcome', { user: data.username, message: `Welcome to this Socket.io chat ${data.username}` });
})
socket.on('new-message', (data) => {
console.log(`new-message event ${data}`);
// broadcast message to all sockets except the one that triggered the event
socket.broadcast.emit('broadcast-message', {user: users[data.user], message: data.message})
});
});
完成最后几步后,我便拥有了一个功能齐全的聊天应用程序,并且可以通过在本地打开多个浏览器来对其进行测试:
到目前为止,我唯一没有讲到的是样式(你可以在style.css 文件中找到)和一些小的验证,比如确保用户不能发送空消息。你可以在 GitHub 上的这个 repo中找到完整的代码。它还包含一个 Dockerfile,你可以使用 Docker 构建镜像并将其部署到任何地方 🙃 或者,如果你只是想在线试用,请访问此链接。
对于这个应用,我只需要使用 emit() 和 broadcast() 函数,但 Socket.io 包含更多功能,例如为套接字分配命名空间,使其拥有不同的端点、创建房间,甚至将其与 Redis 集成。您可以在文档中找到所有这些功能的示例。
希望这能帮助您了解 WebSockets 以及 Socket.io 的工作原理。
编码愉快!