本文介绍基于Node.js和Socket.io构建P2P实时消息系统的实现方案。涵盖连接管理、离线消息存储至Redis、心跳检测及核心API如emit和on的使用。通过对比轮询与长连接机制,详细解析服务端与客户端代码逻辑,解决高并发下的消息推送与状态维护问题。
常见的消息通知场景包括多种类型。
站内通知通常分为几类(括号内为针对当前项目情况的分析,读者可忽略):
- 公告 Announcement:涵盖通道加入新组织、某组织或用户上传数据摘要、系统凌晨版本更新等事件。
- 提醒 Remind:用户之间、系统与用户之间的通知。
- 资源订阅提醒:当关注的数据摘要内容更新、产生评论等情况时触发。
- 资源发布提醒:包括自己发布的数据摘要被评论、被关注或被申请交易时的通知。
- 系统提醒:用户之间或系统与用户之间的常规提醒。
- 私信 Mailbox:类似聊天室功能,目前暂无此需求。
实现思路与步骤等
除了使用消息队列MQ外,另一种方案是采用WebSocket协议。该协议支持全双工通信(full-duplex)和长连接(PersistentConnection),相比HTTP协议,它是一种持久化的通信方式。
主要开发步骤包括:
- 绑定连接:建立用户账号与WebSocket之间的连接。
- 管理连接:对建立的连接进行维护和管理。
- 收发消息:涉及数据格式定义及读取等具体实现。
实现过程中需要注意的关键点包括:
- 长连接的心跳激活处理,以维持连接有效性。
- 服务端调优,以实现高并发量客户端同时在线(单机服务器理论上可支持百万级并发长连接)。
- 群发消息功能的实现。
- 服务端维持多用户状态的机制。
- 从WebSocket中获取HttpSession以执行用户相关操作。
具体实现思路如下:
- 前端使用WebSocket与服务端创建连接时,将用户ID传递给服务端。服务端将用户ID与channel(服务器与客户端之间的连接通道)关联存储,并将该channel加入channel组。
- 若需向所有用户发送消息,直接执行channel组的writeAndFlush()方法。
- 若需向指定用户发送消息,根据用户ID查询对应的channel,然后执行writeAndFlush()方法。
- 前端获取服务端推送的消息后,将消息内容展示在文本域中。
其他方法介绍
轮询:客户端定时向服务器发送Ajax请求,服务器接收后立即返回响应并关闭连接。优点:后端程序编写简单。缺点:大部分请求无用,浪费带宽和服务器资源。实例:适用于小型应用。
长轮询:客户端向服务器发送Ajax请求,服务器保持连接直到有新消息才返回响应并关闭连接,客户端处理完响应后再发送新请求。优点:无消息时不频繁请求,资源消耗小。缺点:服务器保持连接消耗资源,返回数据顺序无保证,维护困难。Comet异步ashx实例:WebQQ、Hi网页版、Facebook IM。
长连接:在页面嵌入隐藏iframe,将其src属性设为长连接请求或使用xhr请求,服务器端源源不断向客户端输入数据。优点:消息即时到达,无无用请求;管理相对简便。缺点:服务器维护长连接增加开销。实例:Gmail聊天。
Flash Socket:页面内嵌入使用Socket类的Flash程序,JavaScript通过调用该Flash提供的Socket接口与服务器端通信,收到信息后控制页面显示。优点:实现真正的即时通信而非伪即时。缺点:客户端需安装Flash插件;非HTTP协议,无法自动穿越防火墙。实例:网络互动游戏。
技术实现与相关包介绍
包介绍
Node.js不同于其他服务器,对不同连接不支持进程和线程操作,因此开发此类功能时需选择合适的包。
使用WebSocket协议的包众多,此处先介绍一种常用包nodejs-websocket。网评认为其使用较为繁琐,故未选用。该包依赖底层C++和Python环境,支持以Node作为客户端访问。需注意的是,nodejs-websocket纯粹使用WebSocket协议,使用时需自行编写心跳检测以判断用户在线状态。
本文采用socket.io,其使用简单且功能强大,支持集成WebSocket服务器端与Express3框架。它无需单独进行心跳检测(相对而言),因其封装了轮询机制和实时通信,当WebSocket连接断开时会尝试重连,可能消耗资源。此外还有node-websocket-server等库(此处直接忽略)。
技术实现
在实现前,考虑到向指定用户发送WebSocket消息时对方可能不在线,采取如下处理策略:
- 若接收者在线,则存储至Redis并实时发送消息。
- 否则将消息存储到Redis,待用户登录上线后主动推送未读消息。
Socket.io的客户端和服务端均提供on()和emit()两个核心函数,可轻松实现双向通信。
- emit:触发一个事件,第一个参数是事件名称,第二个参数是要发送到另一端的数据,第三个参数是一个回调函数用来确认对方的接收信息(也可以说时回执),可忽略。
- socket.emit:信息传输对象为当前socket对应的client,各client socket相互独立。
- socket.broadcast.emit:信息传输对象为所有client,排除当前socket对应的client。
- io.sockets.emit:信息传输对象为所有client。
- on:注册事件,用于监听emit触发的事件。
服务端
直接上代码:
'use strict';
// 维护socket连接的代码
const { addSocketId, getSocketId, deleteSocketId } = require('../../../utils/socket/socketId');
// 保存消息
const message = require('../saveMessage');
// socket连接许可验证
const { socketAuth } = require('../../../middleware/socket/index')
// socket接口,传入/bin/www.js
function init(io) {
/**
* @description: 为每个传入执行的功能Socket,并且接收套接字和可选地将执行延迟到下一个注册的中间件的参数
*/
io.use((socket, next) => {
if (socket.request.headers.cookie) return next();
next(new Error('Authentication error'));
});
io.on('connection', function(socket) {
/**
* @description: 用户登录,则保存用户连接的相关信息,并从redis拉取未读消息,推送给该用户
*/
socket.on('user_login', function(socketInfo) {
if(!socketInfo.userId) {
// io.sockets.to(socketInfo['socketId']).emit('disconnect', '');
return;
}
// 将用户与socket插入数据库中
addSocketId(socketInfo);
if (process.env.NODE_ENV === 'development') {
displayUserInfo(socketInfo);
};
// 推送所有消息
message.pushMessage(socketInfo['userId']).then(pushData => {
io.sockets.to(socketInfo['socketId']).emit('push_message', pushData);
});
});
/**
* @description: 发给某用户交易通知(在线实时通知,并存储至redis)
*/
socket.on('todo', function(todoData) {
// 存入redis
message.addMessage(todoData);
// 检测用户是否在线
message.isOnline(todoData['receiver_id']).then(isOnline => {
// 用户在线则通信
if (isOnline == true) {
getSocketId(todoData['receiver_id']).then(socketId => {
io.sockets.to(socketId).emit('todo_message', todoData);
});
};
});
});
// TODO: 需要提醒前端在关闭窗口之前先断开连接(窗口刷新之前应该不需要)
/**
* @description: 断开连接
*/
socket.on('disconnect', function() {
// 从数据库中删除连接
deleteSocketId(socket.id);
// 判断当前是否是开发环境
if (process.env.NODE_ENV === 'development') {
displayUserInfo();
}
});
});
}
function displayUserInfo(user) {
console.log(`当前登录用户信息:${user}`);
return;
}
module.exports = {
init
};上述代码主要创建了connection事件,其下包含user_login、todo、disconnect等事件,这些事件下又创建或监听了其他事件。user_login事件主要监听前端用户登录成功,若用户上线,则将Redis内已读未读消息分类后推送给客户端。todo事件判断用户在线后实时传递消息,需注意使用io.sockets.to(socketId).emit(eventname, eventdata)实现P2P消息传送,其中socketId为接收消息用户的WebSocket连接ID。客户端需监听emit()参数中的eventname事件。disconnect事件在用户登出或刷新页面导致WebSocket连接断开时,从维护的socket连接组中删除该用户的连接信息。
此外,在连接到connection事件前,有一个中间件io.use((socket, next) => {},用于判断连接是否有效(带有cookie的主动连接)。
然后在/bin/www.js中引入io:
#!/usr/bin/env node
// 模块依赖
var app = require('../app');
var http = require('http');
const socketIndex = require('../src/routes/socket/index/socket');
// 从环境中取端口,应用到express
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
// 创建http服务(将express注册到http中)
server = http.createServer(app);
// 监听
var io = require('socket.io')(server, {
cors: {
origin: '*'
}
// path: '/socket' // 重新定义socket连接路径
});
// 全局声明
global.io = io;
// socket的程序文件下引入io
socketIndex.init(io);其中,引入函数init()即上一段代码中的init函数,传入参数为服务端入口创建的io服务。io服务需传入cors参数以解决跨域问题,若需更改WebSocket连接地址,可使用path参数,其值为在原WebSocket连接地址后追加的内容。
客户端
首先创建一个socket对象,io()的第一个参数为链接服务器的URL,默认为window.location(需修改为服务端URL,包括对应模块或权限指定的路径,即path参数)。
到此这篇关于nodejs+socket.io实现p2p消息实时发送的项目实践的文章就介绍到这了,更多相关node socket.io实现p2p实时发送内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!