node.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > node.js > node socket.io实现p2p实时发送

nodejs+socket.io实现p2p消息实时发送的项目实践

作者:闲坐含香咀翠

本文主要介绍了nodejs+socket.io实现p2p消息实时发送,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

常见的消息通知:

常见的站内通知类别(括号里是对自己目前项目出现情况的分析,读者忽略):

实现思路与步骤等

除了用消息队列MQ以外,笔者想到的是使用websocket协议实现,该协议为全双工通信full-duplex,长连接PersistentConnection,相比http来说是种持久化协议。

其中主要的开发步骤有:

其中,需要注意的点有:

具体实现思路:

其他方法介绍

轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。 优点:后端程序编写比较容易。 缺点:请求中有大半是无用,浪费带宽和服务器资源。 实例:适于小型应用。

长轮询:客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。 优点:在无消息的情况下不会频繁的请求,耗费资小。 缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护。 Comet异步的ashx, 实例:WebQQ、Hi网页版、Facebook IM。

长连接:在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用xhr请求,服务器端就能源源不断地往客户端输入数据。 优点:消息即时到达,不发无用请求;管理起来也相对便。 缺点:服务器维护一个长连接会增加开销。 实例:Gmail聊天

Flash Socket:在页面中内嵌入一个使用了Socket类的 Flash 程序JavaScript通过调用此Flash程序提供的Socket接口与服务器端的Socket接口进行通信,JavaScript在收到服务器端传送的信息后控制页面的显示。 优点:实现真正的即时通信,而不是伪即时。 缺点:客户端必须安装Flash插件;非HTTP协议,无法自动穿越防火墙。 实例:网络互动游戏。

技术实现与相关包介绍

包介绍

nodejs不像其他的服务器,对于不同的连接,不支持进程和线程操作,写这类功能的时候就需要找更合适的包。

使用WebSocket协议的包有好多,这里我先讲一种常用的包是nodejs-websocket包,网评说使用较为繁琐,这里就没使用。它需要依赖于底层的C++,Python的环境,支持以node做客户端的访问。当然了,这里我一定要说一下,nodejs-websocket是纯粹的使用了WebSocket协议,因此使用时需要写心跳检测,检测用户是否在线等情况。

我采用的是socket.io,它使用起来较为简单,功能强大,支持集成websocket服务器端和Express3框架与一身。它可以不需要心跳检测,不过这也是个相对说法,因为它结合封装了轮询机制和实时通信,当websocket连接断掉时,它会不停的尝试连接,耗费资源。当然了,还有其他库,比如node-websocket-server(不需要了解,直接放弃)。

技术实现

在实现前,考虑到发送消息时,向指定用户发送WebSocket消息,但对方可能不在线,这种情况,我这么处理:

socket.io的客户端和服务端都有两个函数 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连接组中删除该用户的WebSocket连接信息。

当然,在连接到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实时发送内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文