当前位置: 首页 > 后端技术 > Node.js

使用express+socket.io实现简易版的聊天室

时间:2023-04-03 13:46:28 Node.js

写在前面最近因为使用node重构某项目,项目有实时聊天功能,所以研究了一下聊天室,在线演示|源代码,欢迎反馈。这个聊天室主要用的是socket.io和express。这个聊天室支持群聊,私聊,支持发图片(PS:体验的时候最好打开两个浏览器,自己问答)。给大家分享一下实现过程:WebSocketHTML5是一个新的协议。它实现了浏览器和服务器之间的全双工通信。为了更好的理解WebSocket,需要了解如何在没有WebSocket的情况下编写聊天室等实时系统:基于http协议,浏览器可以实现单向通信,只需浏览器发起一个请求(Request),服务器响应(Response),一个请求对应一个响应。由于服务端不能主动向客户端推送消息,常用的方式是轮询。轮询实现起来很简单,就是每隔一段时间用ajax向服务器请求一次。如果服务器有新数据,则返回新数据;如果没有数据,则返回空响应。这是代码模拟的样子://前端请求代码函数更新(fn){varxhr=newXMLHttpRequest();xhr.open("get","./update.php");xhr.onreadystatechange=function(){if(xhr.readyState===4){if(xhr.status==200){constres=JSON.parse(xhr.response);if(res.flag){//执行相应的操作//fn是处理函数fn&&fn(fn);}}}};xhr.send();}functionpolling(){update();}setInterval(polling,2000);//后台响应代码true,"data"=>'新数据来了'));}else{echojson_encode(array("flag"=>false));}?>这种定时请求方式的关键是间隔时间的选择。根据我上面代码做的模拟,有很小的概率能拿到真实的数据,而且大部分ajax请求都是无效的,所以有前辈提出了基于轮询的Comet(serverpush)。这个技术可以通过长轮询来实现(也可以使用iframe),长轮询也是依赖ajax来实现客户端的请求。流程是:客户端发起请求,服务端暂停请求,如果有新的数据返回,服务端响应刚才客户端的请求,客户端在得到响应后继续请求服务端用伪代码模拟长轮询过程://前端使用如下函数进行请求函数longPolling(){}longpolling();//对后端代码做如下改动true,"data"=>'新数据来了'));休息;}}?>长轮询确实减少了请求的数量,但是它也有一个很大的问题,就是消耗服务器资源。无论是轮询还是长轮询,都存在http不支持长连接的问题。很多人会说keep-alive不就是实现长连接吗?然而事实并非如此,keep-alive是复用了一个TCP连接,也就是说http1.1实现了一个TCP连接可以发送多个http请求,但是每个http请求还需要发送一个RequestHeader,而每个请求的响应也会携带一个ResponseHeader。对于轮询和长轮询,伴随着真实数据的交换,还有大量http头的交换。基于这些问题,提出了WebSocket。WebSocket可以理解为http的补丁包。WebSocket使http成为真正的长连接。握手阶段使用http协议,之后不会再发起http请求。我们来看看WebSocket的握手过程:客户端的请求头比普通的http请求多了几个字段:Upgrade:websocket,Connection:Upgrade,用这两个字段告诉服务器我要升级协议到websocket。Sec-WebSocket-Version:13,告诉服务器我要使用的WebSocket版本。Sec-WebSocket-Key,它的值是一个base64编码的随机16字节长的字符序列,这个值会在响应头中响应。Sec-WebSocket-Extensions提供了一个客户端支持的协议扩展列表,供服务端选择。服务器只能选择一个,并将选择的扩展名写入响应头的Sec-WebSocket-Extensions。Sec-WebSocket-Protocol,与Sec-WebSocket-Extensions原理类似,用于协商应用子协议。我们看一下响应头:StatusCode,值为101,表示已经升级为WebSocket协议。Sec-WebSocket-Extensions告诉客户端服务器选择的协议扩展。Sec-WebSocket-Protocol告诉客户端服务器选择的子协议Sec-WebSocket-Accept。服务器确认并加密Sec-WebSocket-Key后,还有一点值得注意的是,协议头由http/https变成了ws/wss,这也说明真正的http完成了它的使命,接下来的事情将由WebSocket处理。!socket.io由于在编写原生WebSocket时难以处理低版本浏览器的兼容性问题,因此在编写此类具有实时交互的项目时,一般会使用socket.io。socket.io不仅仅是WebSocket,还包括AJAXlongpolling、AJAXmultipartstreaming、JSONPPolling等。Socket.io可以看成是基于engine.io的二次开发。通过emit和on可以轻松实现服务端和客户端的双向通信,emit用于发布事件,on用于订阅事件。用户登录/注销以开始编写代码。我用的构建工具是gulp,模板语言是jade,css预处理语言比较少。如果需要用到这些,可以关注我团队搭建的一个小脚手架,从app.js开始:constusers={},app=express(),server=require("http").createServer(app),io=require("socket.io").listen(server);//将socket.io绑定到服务端,使得任何连接到服务端的客户端都具有实时通信的功能//服务端监听客户端io.on("connection",(socket)=>{//socket为返回的连接对象,两端交互通过该对象});需要创建一个对象(users)存储在线用户,key值为用户昵称,订阅用户登录事件: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]){删除使用rs[socket.nickname];UsersChange(socket.nickname,false);}});functionUsersChange(nickname,flag){io.sockets.emit("system",{nickname:nickname,size:Object.keys(users).length,flag:flag});}functionnowSecond(){returnMath.floor(newDate()/1000);}用户登录时,需要验证自己的昵称是否包含。如果是函数,会触发客户端js代码中注册的repeat事件,否则触发loginSuccess事件,登录成功后需要广播给所有客户端,所以使用io.sockets.emitrepeat,loginSuccess,system,在src/js/index.js中注册,主要用于页面的展示,也就是一些dom操作,这里就不多说了。如果用户退出,只需调用默认事件disconnect并将用户从用户对象中移除。心跳检测对于用户状态还是有很多坑的,因为WebSocket的中间过程比较复杂,经常会出现一些异常情况,所以需要进行心跳检测。我使用的方法是服务器定时遍历用户列表。如果用户结束与当前发言时间5分钟相比,将被视为断开连接,从而避免了“用户未定义退出群聊”的情况。函数pong(){constnow=nowSecond();for(letkinusers){if(users[k].lastSpeakTime+MAX_LEAVE_TIME