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

基于ThinkJS的WebSocket通信详解

时间:2023-04-03 17:54:17 Node.js

基于ThinkJS的WebSocket通信详解文章介绍如何在ThinkJS项目中使用WebSocket实现多端实时通信。ThinkJS是基于Koa2开发的企业级Node.js服务端框架,本文将从零开始实现一个简单的聊天室。希望读者有所收获。WebSocketWebSocket是HTML5提出的一种协议。它的出现是为了解决客户端和服务器之间的实时通信问题。在WebSocket出现之前,实现实时消息传递一般有两种方式:客户端通过轮询不断向服务器端发送请求,客户端有新消息更新。这种方法的缺点是显而易见的。客户端需要不断地向服务器发送请求。但是HTTP请求可能包含很长的header,真正有效的数据可能只是一小部分。显然,这会浪费大量的带宽资源。HTTPlongConnection,客户端通过HTTP请求连接到服务器后,底层的TCP连接不会立即断开,后续信息仍然可以通过同一个连接传输。这种方式的一个问题是每次连接都会占用服务器资源,收到消息后会断开连接,需要重新发送请求。等等等等。可以看出,这两种实现方式的本质都是从客户端“Pull”到服务端的过程,而服务端是没有办法主动“Push”到客户端的。所有方法都依赖客户端先发起请求。为了满足双方的实时通信,WebSocket应运而生。WebSocket协议首先,WebSocket是基于HTTP协议,或者说是借用了HTTP协议来完成连接的握手部分。其次,WebSocket是一个持久化协议。与HTTP等非持久化协议相比,HTTP请求在收到服务器回复后会直接断开连接。下次收到消息时,需要重新发送HTTP请求。连接成功后,可以保持连接状态。下图应该体现了两者的关系:在发起WebSocket请求时,需要先通过HTTP请求告诉服务器将协议升级为WebSocket。浏览器首先发送一个请求:GET/HTTP/1.1Host:localhost:8080Origin:[url=http://127.0.0.1:3000]http://127.0.0.1:3000[/url]Connection:UpgradeUpgrade:WebSocketSec-WebSocket-Version:13Sec-WebSocket-Key:w4v7O6xFTi36lq3RNcgctw==服务器响应请求:HTTP/1.1101SwitchingProtocolsConnection:UpgradeUpgrade:WebSocketSec-WebSocket-Accept:Oy4NRAQ13jhfONC7bP8dTKb4PTU=请求头中的核心部分是Connection和Upgrade现场服务器将HTTP升级为WebSocket协议。服务器返回相应信息后,连接成功,客户端和服务器可以正常通信。随着新标准的推进,WebSocket已经相对成熟,各种主流浏览器对WebSocket的支持都比较好(不兼容IE的低版本,IE10以下)。.js,一个支持WebSocket协议的跨平台实时通信开源框架。它包括客户端的JavaScript和服务器端的Node.js,具有良好的兼容性。它会根据浏览器的支持情况选择不同的通信方式,比如上面介绍的轮询和HTTP长连接。WebSocket简易聊天室目前ThinkJS支持Socket.io,并对其做了一些简单的封装。你只需要做一些简单的配置就可以使用WebSocket。服务端配置stickyClusterThinkJS默认采用多进程模型,每次请求都会根据策略发送到不同的进程执行。其多进程模型请参考《细谈 ThinkJS 多进程模型》。但是HTTP请求需要在WebSocket连接之前完成握手升级,并且必须保证多次请求命中同一个进程才能保证握手成功。这时候就需要开启StickyCluster功能,让客户端的所有请求都命中同一个进程。修改配置文件src/config/config.js即可。module.exports={stickyCluster:true,//...}添加WebSocket配置在src/config/extend.js中导入WebSocket:constwebsocket=require('think-websocket');module.exports=[//...websocket(think.app),];在src/config/adapter.js文件中配置WebSockethandle:socketio,messages:{open:'/websocket/open',//处理建立连接时websocketController对应的openActionclose:'/websocket/close',//连接建立时要处理的Actionclosedroom:'/websocket/room'//房间事件处理的Action}}}配置中的消息对应事件的映射关系。比如上面的例子,客户端触发房间事件,服务端需要在websocket控制器下的roomAction中处理消息。添加WebSocket实现会创建一个处理消息的控制器文件。上面的配置是/websocket/xxx,所以直接在项目根目录src/controller下创建websocket.js文件。module.exports=classextendsthink.Controller{//this.socket是客户端发送消息对应的socket实例,this.io是Socket.io的实例constructor(...arg){super(...参数);this.io=this.ctx.req.io;this.socket=this.ctx.req.websocket;}asyncopenAction(){this.socket.emit('open','websocketsuccess')}closeAction(){this.socket.disconnect(true);}};这个时候服务器代码已经配置好了。客户端配置客户端代码使用比较简单,导入socket.io.js即可直接使用。引入后初始化代码中创建WebSocket连接:this.socket=io();this.socket.on('open',data=>{console.log('open',data)})这是最简单的WebSocketdemo。当页面打开时,会自动创建一个WebSocket连接。创建成功后,服务端会触发open事件,客户端会在监听的open事件中收到服务端返回的websocket成功字符串。接下来我们开始实现一个简单的聊天室。一个简单聊天室的实现从刚才的内容我们知道,每次创建WebSocket连接都会创建一个Socket句柄,对应代码中的this.socket变量。所以本质上聊天室中人与人之间的交流可以转换为每个人对应的Socket句柄的交流。我只需要找到这个人对应的Socket句柄,就可以给对方发送消息了。为了简单的实现,我们可以设置一个全局变量来存储一些连接到服务器的WebSocket的信息。在src/bootstrap/global.js中设置全局变量:global.$socketChat={};然后在src/bootstrap/worker.js中引入global.js,使全局变量生效。要求('./global');然后将roomAction和messageAction添加到服务器端控制器。messageAction用于接收客户端用户的聊天信息,并将信息发送给所有客户端成员。roomAction用于接收客户端进入/离开聊天室的信息。两者的区别是聊天消息需要同步到所有成员所以使用this.io.emit,聊天室消息同步到除当前客户端以外的所有成员所以使用this.socket.broadcast.emitmodule.exports=classextendsthink.Controller{constructor(...arg){super(...arg);this.io=this.ctx.req.io;this.socket=this.ctx.req.websocket;global.$socketChat.io=this.io;}asyncmessageAction(){this.io.emit('message',{nickname:this.wsData.nickname,type:'message',message:this.wsData.message,id:this.socket.id})}asyncroomAction(){global.$socketChat[this.socket.id]={nickname:this.wsData.nickname,socket:this.socket}this.socket.broadcast.emit('room',{nickname:this.wsData.昵称,类型:'in',id:this.socket.id})}asynccloseAction(){constcloseSocket=global.$socketChat[this.socket.id];const昵称=closeSocket&&closeSocket.nickname;this.socket.disconnect(true);this.socket.removeAllListeners();this.socket.broadcast.emit('room',{nickname,type:'out',id:this.socket.id})deleteglobal.$socketChat[this.socket.id]}}客户端通过监听处理信息服务器发出的事件。this.socket.on('message',data=>{//通过socket的id进行比较,判断消息的发送者data.isMe=(data.id===this.socket.id);this.chatData.push(data);})this.socket.on('room',(data)=>{this.chatData.push(data);})通过emitserverthis对应的action发送消息。socket.emit('room',{nickname:this.nickname})this.socket.emit('message',{message:this.chatMsg,nickname:this.nickname})根据消息类型判断消息类型发送/接收消息{{item.nickname}}进入聊天室

{{item.nickname}}离开聊天室

{{item.nickname}}:{{item.message}}

至此,一个简单的聊天室就完成了多节点通信问题。刚才我们说了通信的本质其实就是一个Socket句柄在查询的过程中,本质上我们是用全局变量来存储所有的WebSocket句柄来解决WebSocket连接查找的问题。但是当我们的服务器扩容的时候,多个服务器就会有WebSocket连接。这时候使用全局变量搜索跨节点WebSocket连接的方法就会失效。此时,我们就需要另一种方式来实现跨服务器通信同步。一般有几种方式:消息队列在发送消息时不直接执行emit事件,而是将消息发送到消息队列,然后所有节点对这条消息进行消费。获取到数据后,检查接收方的WebSocket连接是否在当前节点上。如果不是,则忽略此数据,如果是,则执行发送动作。节点通信使用Redis等外部存储服务作为之前的“全局变量”。所有节点创建WebSocket连接后,向Redis注册并告诉所有人在“192.168.1.1”有一个名为“A”的连接。.当B要给A发消息时,会去Redis找到A的连接所在的节点,通知192.168.1.1节点B要给A发消息,然后由节点执行发送动作。基于Redis的节点通讯实现Redis的pub/sub是一种消息通讯方式:发送者(pub)发送消息,订阅者(sub)接收消息。WebSocket的一个节点收到消息后,通过Redis发布(pub),其他节点作为订阅者(sub)接收消息,然后进行后续处理。本次我们将在聊天室的demo上实现节点通信功能。首先在websocket控制器文件中添加一个接口调用constip=require('ip');consthost=ip.address();module.exports=classextendsthink.Controller{asyncopenAction(){//记录当前WebSocket连接到服务器ipawaitglobal.rediser.hset('-socket-chat',host,1);}emit(action,data){if(action==='message'){this.io.emit(action,data)}else{this.socket.broadcast.emit(action,data);}this.crossSync(action,data)}asyncmessageAction(){constdata={nickname:this.wsData.nickname,type:'message',message:this.wsData.message,id:this.socket.id};this.emit('消息',数据);}asynccloseAction(){constconnectSocketCount=Object.keys(this.io.sockets.connected).length;this.crossSync(动作,数据);如果(connectSocketCount<=0){awaitglobal.rediser.hdel('-socket-chat',host);}}asynccrossSync(action,params){constips=awaitglobal.rediser.hkeys('-socket-chat').filter(ip=>ip!==host);ips.forEach(ip=>request({method:'POST',uri:`http://${ip}/api/websocket/sync`,form:{action,data:JSON.stringify(params)},JSON:真}););}}然后在src/controller/api/websocket中实现通信接口邮政();constblackApi=['房间','消息','关闭','打开'];如果(!blackApi.includes(action))返回this.fail();//由于是跨服务器接口,直接使用io.emit发送给当前所有客户端constio=global.$socketChat.io;io&&io.emit(action,JSON.parse(data));}};这样就实现了跨服务通信功能。当然,这只是一个简单的demo,但是基本原理是和socket.io-redis一样的两种Redis(sub/pub)方法,socket.io官方提供了一个库socket.io-redis来实现。封装了Redis的pub/sub功能,让开发者可以忽略Redis的相关部分,方便开发者使用。使用时只需要传入Redis的配置即可。//Thinkjssocket.io-redis配置constredis=require('socket.io-redis');exports.websocket={...socketio:{adapter:redis({host:'localhost',port:6379}),message:{...}}}//然后控制器websocket.jsthis.io.emit('嗨','所有插座');HTTP与WebSocket通信如果你想通过非socket.io进程与socket.io服务通信,例如:HTTP,你可以使用官方的socket.io-emitter库。使用如下:vario=require('socket.io-emitter')({host:'127.0.0.1',port:6379});setInterval(function(){io.emit('time',newDate);},5000);后记整个聊天室的代码已经上传到github,大家可以直接下载体验聊天室示例