NodeBB 7: 使用 Socket.io 实时通信

Posted by River Yang on 2016-12-03

Socket.io 使用 websocket 实现浏览器上的实时通信,可能是此方面功能的最佳实践吧。它提供了前端到后端的全套解决方案,兼容各种语言和平台,传输协议还有降级功能,完美兼容各种浏览器。有了它就可以一套代码保证从手机到IE6的长连接的实时通信, 当然这么说可能不恰当,websocket 并不是长连接,它比长连接要高效得多,对服务器的压力也要小得多,推荐要开发实时通信功能的童鞋使用它代替 HTTP 的长连接。

V2MM上可以聊天,可以建群,有消息可以实时接收,有回复和新内容可以实时收到提醒,都得益于 NodeBB 使用了 socket.io
Socket连接在前端的入口在这里: public/src/sockets.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var ioParams = {
reconnectionAttempts: config.maxReconnectionAttempts,
reconnectionDelay: config.reconnectionDelay,
transports: config.socketioTransports,
path: config.relative_path + '/socket.io'
};
socket = io(config.websocketAddress, ioParams);
socket.on('connect', onConnect);
socket.on('reconnecting', onReconnecting);
socket.on('disconnect', onDisconnect);
socket.on('reconnect_failed', function () {
// Wait ten times the reconnection delay and then start over
setTimeout(socket.connect.bind(socket), parseInt(config.reconnectionDelay, 10) * 10);
});

从这段代码展示了 NodeBB 前端的 websocket 建立连接的过程,这种实现很有典型性,socket.io 连接的时候可以控制 reconnectionAttempts, reconnectionDelay 等变量,并且监听 reconnect_failed 事件,连不上的时候定时自动重连,这样在网络出状况,或者服务器重启的时候,websocket 可以自动连接。

transports 指定了传输通道,默认为 ['polling', 'websocket']polling是降级支持,XHR pollingjsonp polling 的统称,有些浏览器可能还不支持 websocket。那这里为什么顺序不是倒过来的呢? 先使用 websocket 连接,不行再降级为 polling?

Socket.IO never assumes that WebSocket will just work, because in practice there’s a good chance that it won’t. Instead, it establishes a connection with XHR or JSONP right away, and then attempts to upgrade the connection to WebSocket. Compared to the fallback method which relies on timeouts, this means that none of your users will have a degraded experience.
—- socket.io blog

即如果首先使用 websocket 尝试连接,可能不稳定,等到连接断开再“降级”的用户体验也不好,所以会在连接的时候立即使用 polling 的方式先建立连接,然后在“升级”为 websocket. 这是一种很聪明的做法,建议一般网站都按照这种方法来实现。

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
function reJoinCurrentRoom() {
var url_parts = window.location.pathname.slice(config.relative_path.length).split('/').slice(1);
var room;
switch(url_parts[0]) {
case 'user':
room = 'user/' + (ajaxify.data ? ajaxify.data.theirid : 0);
break;
case 'topic':
room = 'topic_' + url_parts[1];
break;
case 'category':
room = 'category_' + url_parts[1];
break;
case 'recent':
room = 'recent_topics';
break;
case 'unread':
room = 'unread_topics';
break;
case 'popular':
room = 'popular_topics';
break;
case 'admin':
room = 'admin';
break;
case 'categories':
room = 'categories';
break;
}
app.currentRoom = '';
app.enterRoom(room);
}

Rooms 也是 socket.io 的一大特色,将不同的连接归类于不同的房间,房间里消息可以群发,可以注册和退出房间,是不是很形象呢。房间的作用其实相当于是一个作用域, 可以看到 NodeBB 对于每个帖子和页面都创建了一个房间,打开了这个页面就进入了相应的房间,这样帖子有回复,或者页面有新内容的时候都可以实时通知。

NodeBB 后端的接口除了在 /api/ 下的 Restful 接口以外, 还有很多 socket 接口,位于 src/socket.io 中。比如我在插件 nodebb-plugin-cards2 中 follow/unfollow 一个用户的接口就是通过 socket.io 调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function toggleFollow($card, type, uid, username) {
socket.emit('user.' + type, {
uid: uid
}, function(err) {
if (err) {
return app.alertError(err.message);
}
$('[component="account/follow"]').toggleClass('hide', type === 'follow');
$('[component="account/unfollow"]').toggleClass('hide', type === 'unfollow');
app.alertSuccess('[[global:alert.' + type + ', ' + username + ']]');
});
return false;
}

关于 NodeBB 的更多讨论可以到 V2MM 上来交流,有疑问可以在此留言,也可以来 V2MM 上问我,我一般都可以实时看到消息 :)