消息实时推送,指的是将消息实时地推送到浏览器,用户不需要刷新浏览器就可以实时获取最新的消息,实时聊天室的技术原理也是如此。传统的Web站点为了实现推送技术,所用的技术都是轮询,这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求。在HTML5中定义了Websocket协议,它是一种在单个TCP连接上进行全双工通讯的协议,不足之处是客户端需要HTML5来实现,IE10以下版本不支持。
Socket.IO 是一个完全由JavaScript实现,基于Node.js、支持WebSocket的协议用于实时通信、跨平台的开源框架。Socket.IO除了支持WebSocket通讯协议外,还支持许多种轮询机制以及其它实时通信方式,并封装成了通用的接口,并能够根据浏览器对通讯机制的支持情况自动地选择最佳的方式来实现网络实时应用。
redis 在该项目中主要起到一个消息分发中心的作用。用户通过socket.io namespace 订阅房间号后,socket.io server则往redis订阅(subscribe)该房间号channel。当在该房间中的某一用户发送消息时,则通过redis的publish功能往该房间号channel推送用户发送消息。这样所有订阅该房间号channel的websocket连接则会收到消息回调,然后推送给客户端。
nginx 需要1.3以上版本。并通过配置ip_hash做粘性会话(ip_hash)处理,避免在低版本浏览器socket.io使用兼容方案轮询请求,请求到不同机器,造成异常。
nginx配置(nginx版本须>1.3): 在http{}里配置定义upstream,并设置ip_hash。使同一个ip的请求能够落在同一个机器同一个进程中。如果改节点挂了,则自动重连到另外一个节点。
upstream io_nodes { ip_hash; server 127.0.0.1:6001; server 127.0.0.1:6002; server 127.0.0.1:6003; server 127.0.0.1:6004; server 127.0.0.1:6005; server 127.0.0.1:6006; server 127.0.0.1:6007; server 127.0.0.1:6008; server 10.168.20.214:6001; server 10.168.20.214:6002; server 10.168.20.214:6003; server 10.168.20.214:6004; server 10.168.20.214:6005; server 10.168.20.214:6006; server 10.168.20.214:6007; server 10.168.20.214:6008; }
在server中,配置location:
location / { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_http_version 1.1; proxy_pass http://io_nodes; proxy_redirect off; }
cluster.js
var cupNum = require('os').cpus().length, workerArr = [], roomInfo = []; var connectNum = 0; for (var i = 0; i < cupNum; i++) { workerArr.push(fork('./fork_server.js', [6001 + i])); workerArr[i].on('message', function(msg) { if (msg.cmd && msg.cmd === 'client connect') { connectNum++; console.log('socket server connectnum:' + connectNum); } if (msg.cmd && msg.cmd === 'client disconnect') { connectNum--; console.log('socket server connectnum:' + connectNum); } });
fork_server.js
var process = require('process'); var io = require('socket.io')(); var num = 0; var redis = require('redis'); var redisClient = redis.createClient; var pub = redisClient({port:13800, host: '127.0.0.1', password:'xxxx'}); var sub = redisClient({port: 13800, host:'127.0.0.1', password:'xxxx'}); var roomSet = {}; //获取父进程传递端口 var port = parseInt(process.argv[2]); io.on('connection', function(socket) { //客户端请求ws URL: http://127.0.0.1:6001?roomid=k12_webcourse_room_1 var roomid = socket.handshake.query.roomid; console.log('worker pid: ' + process.pid + ' join roomid: '+ roomid); socket.on('join', function (data) { socket.join(roomid); //加入房间 if(!roomSet[roomid]){ roomSet[roomid] = {}; console.log('sub channel ' + roomid); sub.subscribe(roomid); } roomSet[roomid][socket.id] = {}; reportConnect(); console.log(data.username + ' join, IP: ' + socket.client.conn.remoteAddress); roomSet[roomid][socket.id].username = data.username; pub.publish(roomid, JSON.stringify({"event":'join',"data": data})); }); socket.on('say', function (data) { console.log("Received Message: " + data.text); pub.publish(roomid, JSON.stringify({"event":'broadcast_say',"data": { username: roomSet[roomid][socket.id].username, text: data.text }})); }); socket.on('disconnect', function() { num--; console.log('worker pid: ' + process.pid + ' clien disconnection num:' + num); process.send({ cmd: 'client disconnect' }); if (roomSet[roomid] && roomSet[roomid][socket.id] && roomSet[roomid][socket.id].username) { console.log(roomSet[roomid][socket.id].username + ' quit'); pub.publish(roomid, JSON.stringify({"event":'broadcast_quit',"data": { username: roomSet[roomid][socket.id].username }})); } roomSet[roomid] && roomSet[roomid][socket.id] && (delete roomSet[roomid][socket.id]); }); }); /** * 订阅redis 回调 * @param {[type]} channel [频道] * @param {[type]} count [数量] * @return {[type]} [description] */ sub.on("subscribe", function (channel, count) { console.log('worker pid: ' + process.pid + ' subscribe: ' + channel); }); /** * [description] * @param {[type]} channel [description] * @param {[type]} message * @return {[type]} [description] */ sub.on("message", function (channel, message) { console.log("message channel " + channel + ": " + message); io.to(channel).emit('message', JSON.parse(message)); }); /** * 上报连接到master进程 * @return {[type]} [description] */ var reportConnect = function(){ num++; console.log('worker pid: ' + process.pid + ' client connect connection num:' + num); process.send({ cmd: 'client connect' }); }; io.listen(port); console.log('worker pid: ' + process.pid + ' listen port:' + port);
客户端:
<script src="static/socket.io.js"></script> <script> var roomid = (function () { return prompt('请输入房间号','') })(); var userInfo = { username: (function () { return prompt('请输入rtx昵称', ''); })() }; if(roomid != null && roomid != "") { var socket = io.connect('http://10.244.146.2?roomid='+ roomid); socket.emit('join', { username: userInfo.username }); socket.on('message', function(msg){ switch (msg.event) { case 'join': if (msg.data.username) { console.log(msg.data.username + '加入了聊天室'); var data = { text: msg.data.username + '加入了聊天室' }; showNotice(data); } break; /*收到消息广播后,显示消息*/ case 'broadcast_say': if(msg.data.username!==userInfo.username) { console.log(msg.data.username + '说: ' + msg.data.text); showMessage(msg.data); } break; /*离开聊天室广播后,显示消息*/ case 'broadcast_quit': if (msg.data.username) { console.log(msg.data.username + '离开了聊天室'); var data = { text: msg.data.username + '离开了聊天室' }; showNotice(data); } break; } }) } /*点击发送按钮*/ document.getElementById('send').onclick = function () { var keywords = document.getElementById('keywords'); if (keywords.value === '') { keywords.focus(); return false; } var data = { text: keywords.value, type: 0, username: userInfo.username }; /*向服务器提交一个say事件,发送消息*/ socket.emit('say', data); showMessage(data); keywords.value = ""; keywords.focus(); }; /*展示消息*/ function showMessage(data) { var itemArr = []; itemArr.push('<dd class="'+(data.type === 0 ? "me" : "other")+'">'); itemArr.push('<ul>'); itemArr.push('<li class="nick-name">' + data.username + '</li>'); itemArr.push('<li class="detail">'); itemArr.push('<div class="head-icon"></div>'); itemArr.push('<div class="text">' + data.text + '</div>'); itemArr.push('</li>'); itemArr.push('</ul>'); itemArr.push('</dd>'); document.getElementById('list') += itemArr.join(''); } /*展示通知*/ function showNotice(data) { var item = '<dd class="tc"><span>' + data.text + '</span><dd>'; document.getElementById('list') += item; } /*回车事件*/ document.onkeyup = function (e) { if (!e) e = window.event; if ((e.keyCode || e.which) == 13) { document.getElementById('send').click(); } } </script>
gihub源码地址: https://github.com/493326889/node-multiple-rooms-chat