最近因为利用node
重构某个项目,项目中有一个实时聊天的功能,因而就研究了一下聊天室,在线demo|源码,欢迎你们反馈。这个聊天室的主要利用到了socket.io
和express
。这个聊天室支持群聊,私聊,支持发送图片(PS:你们在体验时最好开启两个浏览器,自问自答)。下面就来和你们分享下实现过程:php
HTML5一种新的协议。它实现了浏览器与服务器全双工通讯。css
为了更好的理解WebSocket
,须要了解一下在没有WebSocket
阶段是如何写聊天室这种实时系统的:
基于http
协议浏览器能够实现单向通讯,只能由浏览器发起请求(Request),服务器进行响应(Response),一个请求对应一个响应。因为服务器不能主动向客户端推送消息,因而广泛采用的方式就是轮询(polling),轮询实现起来很是简单,就是定时的利用ajax
向服务器端进行请求。若是服务器有新的数据就返回新的数据,若是没有数据就返回空响应。用代码来模拟下就是这个样子的:前端
// 前端请求代码 function update (fn) { var xhr = new XMLHttpRequest(); xhr.open("get", "./update.php"); xhr.onreadystatechange = function(){ if(xhr.readyState === 4){ if(xhr.status == 200){ const res = JSON.parse(xhr.response); if (res.flag) { // 进行相应操做 // fn为接到响应后的处理函数 fn && fn(fn); } } } }; xhr.send(); } function polling () { update(); } setInterval(polling, 2000); // 后台响应代码 <?php // 利用随机数的大小来模拟是否有新数据 if (rand(1, 100) < 35) { echo json_encode(array( "flag" => true, "data" => '有新数据来了' )); } else { echo json_encode(array( "flag" => false )); } ?>
这种定时请求的方式的关键在于间隔时间的选取,依据我在上面代码作的模拟,不多几率能拿到下真正的数据,多半的ajax
请求是无效的,因而又有前辈基于轮询提出来了Comet(服务器推),这种技术能够经过长轮询(long polling)实现(还能够利用iframe
),长轮询也是靠ajax
实现客户端的请求,其流程为:客户端发起请求,服务器挂起请求,倘若有新的数据返回,服务器响应客户端刚才的请求,客户端获得响应后继续请求服务器。用伪代码来模拟下长轮询的过程:node
// 前端利用下面函数进行请求 function longPolling () { update(update); } longpolling(); // 后端代码作以下更改 <?php // 利用随机数的大小来模拟是否有新数据 while (true) { if (rand(1, 100) < 5) { echo json_encode(array( "flag" => true, "data" => '有新数据来了' )); break; } } ?>
长轮询的确减小了请求的次数,可是它也有着很大的问题,那就是耗费服务器的资源。
不管是轮询仍是长轮询,还有着一个问题就是http
并非支持长链接不少人会说keep-alive
不就是作到了长链接吗?然而并不是如此,keep-alive
是重用一个TCP
链接,就是说http 1.1作到了一个TCP
链接能够发送多个http
请求,然而每一个http
请求还须要发送Request Header
,每一个请求的响应还会带着Response Header
。对于轮询和长轮询来讲伴随着真实数据的交换,还有进行的就是大量的http header
的交换。
基于这些问题,WebSocket
被提出,WebSocket
能够理解为对http
的一个补丁包,WebSocket
使http
变成了一个真正的长链接,握手阶段利用http
协议,以后就不会再发起http
请求了。下面来看下WebSocket
握手的过程:git
客户端的请求头比通常的http
请求多出来几个字段:github
Upgrade: websocket,Connection: Upgrade
,利用这两个字段来告诉服务器,我要将协议升级为websocket
。web
Sec-WebSocket-Version: 13
,来告诉服务器我想要使用的WebSocket
的版本。ajax
Sec-WebSocket-Key
,其值采用base64编码的随机16字节长的字符序列,这个值会在响应头中回应。express
Sec-WebSocket-Extensions
,提供了一个客户端支持的协议扩展列表来供服务器选择,服务器只能选择一个,而且会将选择的扩展写入响应头的Sec-WebSocket-Extensions
。json
Sec-WebSocket-Protocol
,与Sec-WebSocket-Extensions
原理类似,用于协商应用子协议。
再来看看响应头:
Status Code
,值为101,表示已经升级到WebSocket
协议
Sec-WebSocket-Extensions
告诉客户端服务器选择的协议扩展
Sec-WebSocket-Protocol
告诉客户端服务器选择的子协议
Sec-WebSocket-Accept
经服务器确认而且加密后的Sec-WebSocket-Key
还有一点值得关注的就是协议头由http/https
换成了ws/wss
,也标识真http
完成了其使命,接下来的事情由WebSocket
来负责啦!
因为写原生的WebSocket
在处理低版本浏览器的兼容性上的困难,因此通常在写实时交互的这种项目时通常会利用到socket.io
。socket.io
并不只仅是WebSocket
,还包含着AJAX long polling
,AJAX multipart streaming
,JSONP Polling
等。socket.io
能够看作是基于engine.io
的二次开发。经过emit
和on
能够轻松地实现服务器与客户端之间的双向通讯,emit
来发布事件,on
来订阅事件。
下面开始来写代码,我利用的构建工具是gulp
,模板语言是jade
,css预处理语言是less
,倘若也须要使用到这些,能够关注下我所在团队搭建的一个小的脚手架,先从app.js
开始:
const users = {}, app = express(), server = require("http").createServer(app), io = require("socket.io").listen(server); // 将socket.io绑定到服务器上,使得任何链接到服务器的客户端都具备实时通讯的功能 // 服务器来监听客户端 io.on("connection", (socket) => { // socket是返回的链接对象,两端的交互就是经过这个对象 });
须要建立一个对象(users
)来存储在线用户,键值为用户昵称,为用户登陆来订阅个事件:
socket.on("login", (nickname) => { if (users[nickname] || nickname === "system") { socket.emit("repeat"); } else { socket.nickname = nickname; users[nickname] = { name: nickname, socket: socket, lastSpeakTime: nowSecond() }; socket.emit("loginSuccess"); UsersChange(nickname, true); } }); socket.on("disconnect", () => { if (socket.nickname && users[socket.nickname]) { delete users[socket.nickname]; UsersChange(socket.nickname, false); } }); function UsersChange (nickname, flag) { io.sockets.emit("system", { nickname: nickname, size: Object.keys(users).length, flag: flag }); } function nowSecond () { return Math.floor(new Date() / 1000); }
用户登陆时须要验证其昵称是否含有,倘若函数,则触发在客户端的js
代码中注册的repeat
事件,反之触发loginSuccess
事件而且登陆成功后须要向全部的客户端来广播,因此利用了io.sockets.emit
。repeat
,loginSuccess
,system
,在src/js/index.js中进行注册,主要用于页面的显示,也就是一些dom操做,因此在这里没有什么好讲的。用户退出,直接调用默认事件disconnect
就好,并将该用户从用户对象中移除。
在用户的状态上的坑仍是很多的,由于WebSocket
中间过程比较复杂,常常会出现一些异常的状况,因此须要进行心跳检测,我采用的方式是服务端定时遍历用户列表,倘若用户最后的发言时间与如今相比超过了5分钟,就将其视为掉线,从而避免了"用户undefined退出群聊"的这种状况。
function pong () { const now = nowSecond(); for (let k in users) { if (users[k].lastSpeakTime + MAX_LEAVE_TIME < now) { var socket = users[k].socket; users[k].socket.emit("disconnect"); socket.emit("nouser", "因为长时间未说话,您已经掉线,请从新刷新页面"); socket = null; } } } // 心跳检测 setInterval(pong, PONG_TIME); function UsersChange (nickname, flag) { io.sockets.emit("system", { nickname: nickname, size: Object.keys(users).length, flag: flag }); }
其实socket.io
的使用真的很是简单,很容易就会上手,因此其他功能再也不一一演示,你们能够看代码的实现(写的比较差,还请见谅),客户端代码中大量用到了L
,至关于zepto
的$
,特别须要处理的是在私信和发送图片的处理上,私信须要处理不一样消息框,到底把消息添加到那个消息框中,我利用了一个对象来存储这些信息(cache
),cache
的键名为用户的昵称(由于在注册时判断了其是否惟一,因此能够将其视为惟一的);键值为对象,对象属性以下图所示:
具体实现你们仍是到源码中去看吧!
感谢王哇勇大神的HiChat和小胡子哥的blogChat
因为本人水平有限,若有错误,欢迎你们指出!