一、相干手艺引见:
音讯实时推送,指的是将音讯实时地推送到浏览器,用户不须要革新浏览器就可以实时猎取最新的音讯,实时聊天室的手艺道理也是云云。传统的Web站点为了完成推送手艺,所用的手艺都是轮询,这类传统的形式带来很明显的瑕玷,即浏览器须要不停的向效劳器发出请求。
短轮询(Polling)
客户端须要定时往浏览器轮询发送请求,且只有当效劳有数据更新后,客户端的下一次轮询请求才拿到更新后的数据,在数据更新前的屡次请求相当于无效。这对带宽资本形成了极大的糟蹋,若进步轮询定时器时刻,又会有数据更新不实时的懊恼。
commet
为了处置惩罚短轮询的弊病,一种基于http长衔接的”效劳器推”体式格局被hack出来。其与短轮询的辨别主假如,采纳commet时,客户端与效劳端坚持一个长衔接,当数据发作转变时,效劳端主动将数据推送到客户端。Comet 又可以被细分为两种完成体式格局,一种是长轮询机制,一种是流手艺。
长轮询
长轮询跟短轮询差别的处所是,客户端往效劳端发送请求后,效劳端推断是不是有数据更新,若没有,则将请求hold住,守候数据更新时,才返回相应。如许则防止了大批无效的http请求,但纵然采纳长轮询体式格局,接收数据更新的最小时刻距离照样为2*RTT(往复时刻)。
流手艺
流手艺(http stream)基于iframe完成。经由历程HTML标签iframe src指向效劳端,竖立一个长衔接。当有数据推送,则往客户端返回,无须再请求。但流手艺有个瑕玷就是,在浏览器顶部会一向涌现页面未加载完成的loading标示。
websocket
为了处置惩罚效劳端怎样更快地实时推送数据到客户端以及以上推送体式格局手艺的不足,HTML5中定义了Websocket协定,它是一种在单个TCP衔接上举行全双工通讯的协定。与http协定差别的请求/相应形式差别,Websocket在竖立衔接之前有一个Handshake(Opening Handshake)历程,竖立衔接以后,两边即可双向通讯。固然,因为websocket是html5新特征,在部份浏览器(IE10以下)是不支撑的。
我们来看下websocket的握手报文:
请求报文:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13
Origin: http://example.com
“Upgrade “、”Connection”: 关照效劳器这个请求是一个websocket协定,须要辨别处置惩罚
“Upgrade: websocket”: 表明这是一个 WebSocket 范例请求,意在关照 server 须要将通讯协定切换到 WebSocket
“Sec-WebSocket-Key”: 是 client 发送的一个 base64 编码的密文,请求 server 必需返回一个对应加密的 “Sec-WebSocket-Accept” 应对,不然 client 会抛出 “Error during WebSocket handshake” 毛病,并封闭衔接
“Sec-WebSocket-Protocol”:一个用户定义的字符串,用来辨别同URL下,差别的效劳所须要的协定
“Sec-WebSocket-Version”:Websocket Draft (协定版本)
相应报文:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
“Sec-WebSocket-Accept”: 这个则是经由效劳器确认,而且加密事后的 Sec-WebSocket-Key。加密体式格局为将Sec-WebSocket-Key与一段牢固的 GUID 字符串举行衔接,然后举行SHA-1 hash,接着base64编码获得。
socket.io(http://socket.io)
是一个完全由JavaScript完成,基于Node.js、支撑WebSocket的协定用于实时通讯、跨平台的开源框架。Socket.IO除了支撑WebSocket通讯协定外,还支撑许多种轮询机制以及别的实时通讯体式格局,并封装成了通用的接口,并可以依据浏览器对通讯机制的支撑状况自动地挑选最好的体式格局来完成收集实时运用。
起首,我们建立一个socket.io server对象,指定监听80端口。而且指定收到message音讯,以及socket端口的监听要领。接着,当socket竖立衔接后,经由历程socket.emit要领,可以往客户端发送音讯。
var io = require('socket.io')();
io.on('connection', function(socket) {
//接收音讯
socket.on('message', function (msg) {
console.log('receive messge : ' + msg );
});
//发送音讯
socket.emit('message', 'hello');
//断开衔接回调
socket.on('disconnect', function () {
console.log('socket disconnect');
});
});
io.listen(80);
客户端的代码也非常简朴,只需引入socket.io对应的客户端库(https://github.com/socketio/s…。
在socket竖立衔接的回调中,运用socket.emit以及socket.on就可以离别做音讯的发送以及监听了。
<script>
var socket = io('http://localhost/');
socket.on('connect', function () {
socket.emit('message', 'hi, i am client!');
socket.on('message', function (msg) {
console.log('msg received from server');
});
});
</script>
二、多节点集群架构设想
若只是单机布置运用,纯真运用socket.io的音讯事宜监听处置惩罚即可满足我们的需求。但随着营业的扩展,我们须要斟酌多机集群布置,客户端可以衔接就任一节点,并发送音讯。怎样做到多节点的同时推送,我们须要竖立一套多节点之间的音讯分发/定阅架构。这时刻我们引入redis的pub/sub功用。
redis
redis是一个key-value存储系统,在该项目中重要起到一个音讯分发中间(publish/subscribe)的作用。用户经由历程socket.io namespace 定阅房间号后,socket.io server则往redis定阅(subscribe)该房间号channel。当在该房间中的某一用户发送音讯时,则经由历程redis的publish功用往redis该房间号channel publish音讯。如许一切定阅该房间号channel的websocket衔接则会收到音讯回调,然后推送给客户端。
nginx
因为采纳了集群架构,则须要nginx来做反向代办。须要注重的是,websocket的支撑须要nginx1.3以上版本。而且我们须要经由历程设置ip_hash做粘性会话(ip_hash)处置惩罚,防止在低版本浏览器socket.io运用兼容计划轮询请求,请求到差别机械,形成session非常。
####三、架构设想图
客户端经由历程socket.io namespace 指定对应roomid,请求到nginx。nginx依据ip_hash反向代办到对应机械的某一端口的socket.io server 历程。竖立websocket衔接,并往redis定阅对应到房间(roomid)channel。到这个时刻,一个定阅了某一房间的websocket通道竖立完成。
当用户发送音讯时,socket.io server捕获到该房间到音讯后,即往redis对应房间id的channel publish音讯。这时刻一切定阅了该房间id channel的socket.io server就会收到定阅相应,接着找到对应房间id的webscoket通道,并将音讯推送到客户端。
四、代码示例(多房间实时聊天室):
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.x.x.x:6001;
server 10.x.x.x:6002;
server 10.x.x.x:6003;
server 10.x.x.x:6004;
server 10.x.x.x:6005;
server 10.x.x.x:6006;
server 10.x.x.x:6007;
server 10.x.x.x: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
我们采纳了多历程的设想,充分利用cpu多核上风。经由历程主历程统一管理保护子历程,每一个历程监听一个端口。
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;
//竖立redis pub、sub衔接
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]);
//当websocket衔接时
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); //到场房间
// 往redis定阅房间id
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;
// 往该房间id的reids channel publish用户进入房间音讯
pub.publish(roomid, JSON.stringify({"event":'join',"data": data}));
});
//用户谈话 推送音讯到redis
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);
});
/**
* 收到redis publish 对应channel的音讯
* @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').innerHTML += itemArr.join('');
}
/*展现关照*/
function showNotice(data) {
var item = '<dd class="tc"><span>' + data.text + '</span><dd>';
document.getElementById('list').innerHTML += 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/…