最近在用nodejs实现一个消息通知的功能,其中用到了socket.io;由于初次使用socket.io,在网上查找资料大都是聊天室广播的教程,没有找到一对一发送消息的具体实现方法,这里分享一下自己学习使用过程中踩到某些坑和最终实现方法。

要实现的功能:
server端数据更新后,发送通知到对应客户端的用户;server是用nodejs写的,并使用了分布式,同时开启了多个实例。

一、安装依赖包

1
npm install socket.io

二、socket.io的使用

1、index.js 文件内容

1
2
3
4
5
6
7
8
//index.js
var express = require('express');
var app = express();
var server = require('http').Server(app);
var io = require('socket.io')(server,{
"path":"/notice" //修改客户端请求的路径,默认为/socket.io
});
new (require("./notice").init)(io); //传入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 = {}; //用户对应socket.id
//初始化socket连接
exports.init = function (io){
//连接验证
io.use(function(socket, next){
//console.log(socket.request.headers.cookie);
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

1
sudo 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
//index.js
var express = require('express');
var app = express();
var server = require('http').Server(app);
var io = require('socket.io')(server,{
"path":"/notice" //修改客户端请求的路径,默认为/socket.io
});
new (require("./notice").init)(io); //传入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中所有存储的数据。

—-publish by CEditor