基于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})根据消息类型判断消息类型发送/接收消息
