最近在用nodejs实现一个消息通知的功能,其中用到了socket.io;由于初次使用socket.io,在网上查找资料大都是聊天室广播的教程,没有找到一对一发送消息的具体实现方法,这里分享一下自己学习使用过程中踩到某些坑和最终实现方法。
要实现的功能:
server端数据更新后,发送通知到对应客户端的用户;server是用nodejs写的,并使用了分布式,同时开启了多个实例。
一、安装依赖包
二、socket.io的使用
1、index.js 文件内容
1 2 3 4 5 6 7 8
| var express = require('express'); var app = express(); var server = require('http').Server(app); var io = require('socket.io')(server,{ "path":"/notice" }); new (require("./notice").init)(io);
|
2、notice.js文件内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| var Notice = null; var socketMap = {}; exports.init = function (io){ io.use(function(socket, next){ var token = socket.request._query.token || ""; if(validate(token)){ socket.request.headers.user = {userId:userId}; return next(); }else{ return next(new Error('Authentication error')); } }); Notice = io.of("/notice").on('connection', function(socket) { var user = socket.handshake.headers.user; var user_id = user && user.userId; if(user_id){ socketMap[user_id] = socket.id; } socket.on('disconnect', function() { delete socketMap[user_id]; }); }); } exports.send = function(data){ var user_id = data.accountID; var socket_id = socketMap[user_id]; Notice.to(socket_id).emit('notice', data); }
|
3、客户端文件
1 2 3 4 5 6 7
| <script src="/socket.io/socket.io.js"></script> <script> var socket = io('http://localhost?token=token'); socket.on('notice', function (data) { console.log(data); }); </script>
|
在跨域请求的情况下socket.request.headers.cookie
是无法获取到有效的cookie的,所以这里直接从客户端传token过来进行验证。验证通过后,将用户信息存到headers中,客户端连接成功后,取出用户信息,并将用户id与此socket连接对应id存储到全局socketMap
变量中,然后外部模块通过调用send方法来发送消息。
这种情况在单实例情况下是没有问题的,但是如果server开启了多个实例就出问题了。
三、使用nginx实现分布式部署
首先需要安装nginx;安装完成后编写配置文件;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| upstream socket_test { ip_hash; server 127.0.0.1:8013; server 127.0.0.1:8014; } server { listen 80; server_name socket.test.com; location / { proxy_pass http://socket_test/; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
|
启动两个项目实例,分别监听8013和8014端口;
然后启动nginx
通过测试会发现,消息有时候能收到,有时候收不到。原因是因为客户端请求的时候,会不定的请求服务端两个实例中的一个,如果连接的时候是请求的8013端口的实例,二发送消息是8014端口的实例 ,就会在8014的实例中找不到建立连接的socket。
四、分布式解决方法
socket.io官网中提供了使用socket.io-redis
来解决此方法;socket.io-redis中使用了redis的消息订阅与发布的功能,当有通知发送的时候,会触发onmessage事件,然后会调用broadcast广播。
接下来将socket.io-redis添加到index.js文件中
1 2 3 4 5 6 7 8 9 10
| var express = require('express'); var app = express(); var server = require('http').Server(app); var io = require('socket.io')(server,{ "path":"/notice" }); new (require("./notice").init)(io); var redis = require('socket.io-redis'); io.adapter(redis({ host: settings.REDIS_HOST, port: settings.REDIS_PORT }));
|
然后重启后会发现还是不行,原因是因为在socketMap变量中有时候就找不到对应的user_id;因为是分布式的,所有socketMap变量在两台实例中并不是共享的,所以这里使用redis,在socket建立的时候将user_id对应的socket_id存储到redis中,发送消息的时候从redis中读取socket_id这样就可以了。
有时候一个用户可能会同时有多个socket连接,所以user_id对应的socket_id就可以是一个数组,发送消息的时候要循环发送,连接断开的时候要只删除对应的socket_id,保留其他建立的连接。
另外服务器重启时socket会重新建立连接,所以在初始化的的时候要清空redis中所有存储的数据。