微信小程序游戏的websocket实现
WebSocket概述”>1. WebSocket概述
1.1. 背景
常规的web网络请求是单向的,即客户端向服务端发送请求->服务端响应请求
。
由于HTTP请求是无状态的,为了记住客户端身份,需要借助Cookie、Session等机制,即便如此,服务端也无法主动向客户端推送消息。如果客户端需要即时获取服务端的某个数据状态,常规的处理方式就是轮询,即定时向服务端发送请求,在过去的项目中接触到的轮询场景有
- 二维码扫描登录,查询用户是否已扫描二维码
- 订单支付,查询用户是否已完成支付并自动跳转
由于轮询的机制是通过定时器实现的,效率比较底下,对于服务器的性能并不友好,且时效性不好(需要等待下一次请求才能获取结果)。在某些场景下(聊天、游戏对战等),由服务器主动向客户端推送消息显得更为合理。因此我们需要WebSocket
WebSockets 是一个可以创建和服务器间进行双向会话的高级技术。通过这个API你可以向服务器发送消息并接受基于事件驱动的响应
1.2. 协议
WebSocket是一种基于ws协议的技术。使用它可以在客户端与服务器之间建立一段连续的、全双工的连接。 建立WebSocket链接赖于HTTP协议升级机制,简单来讲,HTTP协议提供了一种特殊的机制,ws连接可以以常用的协议启动(如HTTP/1.1),随后再升级到WebSockets。当然,需要升级的请求得配置下面的header
Connection: Upgrade
,表示这是一个升级请求Upgrade: websocket
,表示升级后的协议为websocket
上面这两个请求头是建立ws连接必须的,除此之外,还可以对请求头进行扩展,更精细地控制websocket请求
Sec-WebSocket-Extensions: extensions
,从规范中选择,已逗号分隔的扩展名Sec-WebSocket-Key: key
,这个头部实际上是用来阻止那些不是故意建立ws客户端的用户,无法被xht.setRequestHeader()
主动添加Sec-WebSocket-Protocol: subprotocols
,建立websocket时可以指定子协议,Sec-WebSocket-Version: supportedVersions
,请求websocket的版本号
响应头中,会携带一个Sec-WebSocket-Accept: hash
的头部,这是根据请求头中的Sec-WebSocket-Key
加密生成的hash值,表示服务端能够与该客户端建立websocket请求了。
1.3. nginx配置
如果需要配置nginx转发,根据上面的协议,配置Connection
和Upgrade
就可以了
location / { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
2. 封装客户端和服务端
2.1. 封装小程序中的webSocket
小程序中的webSocket由微信进行了封装,但是其用法基本与浏览器中的webSocket保持一致。
根据webSocket的“向服务器发送消息并接受基于事件驱动的响应”的特性,我们可以将其封装成事件系统形式,然后前后端约定对应的事件名称进行调用即可.
事件系统在jQuery和Vue等库中都有实现,其核心思想就是维护一个事件处理函数的字典,在on
时收集对应事件的事件处理函数,然后在emit
时触发对应事件
与后台进行消息体约定,由于webSocket是一个双向的过程,因此封装对于客户端和服务端均适用。只需要在传输的消息中指定对应的事件名和数据即可
{ "event": eventName, // 事件名称 "content": data // 消息数据内容 }
下面是一个小程序中webSocket的简单实现,
class WxWebSocket { constructor(connectOpt) { this.connectOpt = connectOpt this.isConnect = false this.eventList = [] this.reTry = 0 this.maxReTry = 3 } connect(cb) { let connectOpt = this.connectOpt wx.connectSocket(connectOpt) wx.onSocketOpen((res) => { this.isConnect = true this.listen() cb(res) }) wx.onSocketClose((res) => { this.isConnect = false }) wx.onSocketError((res) => { this.isConnect = false }) } close(opt) { if (this.isConnect) { this.isConnect = false wx.closeSocket() } } /** *监听事件 */ listen() { wx.onSocketMessage((res) => { let data = JSON.parse(res.data) let {event, content} = data let subs = this.eventList[event] if (!subs) { console.log(`no ${event} listener`) return } subs.forEach(sub => { if (typeof sub === 'function') { sub(content) } }) }) } /** * 注册对应响应事件回调 * @param eventName * @param cb */ on(eventName, cb) { if (!this.eventList[eventName]) { this.eventList[eventName] = [] } if (typeof cb === 'function') { this.eventList[eventName].push(cb) } } /** * 发送消息 * @param eventName * @param data */ emit(eventName, data) { if (!this.isConnect) { console.log('no connect for webSocket') return } let params = { event: eventName, content: data } wx.sendSocketMessage({ data: JSON.stringify(params), // todo success fail }) } }
在web中只需要将对应的api替换成open
、send
和onmessage
即可,有空应该去看一下socket.io
的客户端实现。
2.2. 搭建webSocket服务端
使用nodejs可以很方便地搭建一个本地的socket服务器,比较常用的有socket.io和websocket。
这里使用websocket
进行演示
const http = require("http"); const WebSocketServer = require("websocket").server; const fs = require("fs"); const httpServer = http.createServer((request, res) => { fs.readFile(__dirname + "/index.html", function(err, data) { if (err) { res.writeHead(500); return res.end("Error loading index.html"); } res.writeHead(200); res.end(data); }); }); const wsServer = new WebSocketServer({ httpServer, autoAcceptConnections: true, fragmentOutgoingMessages: true }); let connections = []; // 这里可以监听客户端主动触发的一些事件名 let logic = {}; wsServer.on("connect", connection => { connections.push(connection); connection .on("message", message => { if (message.type === "utf8") { let data = message.utf8Data; data = JSON.parse(data); let { event, content } = data; let response // 获取logic的响应数据 if (typeof logic[event] === 'function'){ response = logic[event](content); }else { response = { event: 'default', content: { 'msg': 'hello' } } } connections.forEach(function(destination) { destination.sendUTF(JSON.stringify(response)); }); } }) .on("close", (reasonCode, description) => { var index = connections.indexOf(connection); if (index !== -1) { connections.splice(index, 1); } console.log(connection.remoteAddres + ' disconnected'); }); }); httpServer.listen(3001, () => { console.log("[" + new Date() + "] Serveris listening on port 3001"); });
3. 遇见的一些问题
3.1. Android手机wifi代理不转发wss请求
在测试环境下遇见的一个bug,本地是通过charles进行代理和抓包的,发现Android无法正常建立webSocket连接,也无法进行抓包。
在调试过程中发现,通过局域网IP连接本地服务器是没有问题的,通过域名连接测试环境的服务就会连接失败。
经过查询,发现部分Android手机的wss协议不走wifi代理,将websocket的请求直接发送到线上环境,然而本地代理连接的是测试环境,线上还没有进行部署,导致请求失败,无法正常建立连接。(这个bug排查了一两天….简直怀疑人生了)
知道了原因,解决问题的核心思想就是让Android手机的wss连接都测试环境的代理即可,有下面几个思路
- 直接修改手机的hosts文件,需要root,且需要对每部手机进行修改,比较麻烦
- 在手机上安装drony,强制所有请求走drony代理
我们采用的是第二种方案,drony的安装和使用可以参考这里。(PS:drony的操作界面是左右滑动的,不是点击上面tab栏的标签进行切换,有点蛋疼…)
3.2. 心跳连接
使用webSocket时,长时间(60s,可设置)不进行通信,会出现下面错误
failed: Error during WebSocket handshake: Unexpected response code: 400
解决办法有两种,
- 配置nginx,修改长时间不通信断开连接的时间阈值
- 建立心跳连接,开启定时器,隔一段时间触发ping事件,服务端响应pong事件即可。
在项目中采用了心跳连接的方式进行处理。
setHeartBeat() { let socket = this.data.socket setInterval(() => { // ping事件 socket.emit(SOCKET_EVENT.HEART_BEAT) // 默认不处理服务器的pong响应,只需要维持连接即可 }, 9 * 1000) },
3.3. 真机上测试小程序中的ws请求
在真机上测试的时候,不管指定的协议是ws还是wss,貌似发送的都是wss请求,如果是连接本地的测试服务器,记得配置证书。
3.4. iOS手机息屏一段时间后倒计时不正常
就是在屏幕休眠或者该程序切换到后台 的时候,ios系统倒计时会暂停,但是在使用中的时候这个绝对算是一个bug。
这个bug貌似跟webSocket没啥关系,不过在项目中遇见了,顺道记一下。
解决办法有两种
- 启动倒计时的时候,将过期时间戳保存在本地,从休眠恢复后判断本地保存的过期时间戳与当前时间戳进行比较,然后进行判断定时器是否已经失效
- 在服务端保存过期时间戳,恢复休眠时从服务端获取是否过期的状态
上面两种办法无非是在客户端或者服务端维持一个过期时间戳,基本思路是一致的。至于BUG产生的原因,这个应该跟iOS系统有关系~暂时没有深究。
3.5. 小程序的生命周期
此前对于小程序的生命周期函数并没有做过多的了解,由于需要注意建立websocket连接及关闭连接的时机,因此重新翻阅了一下小程序的声明周期文档,发现之前的理解有一点问题。
onLoad
,页面第一次加载完成onReady
,页面第一次渲染完成onShow
,显示页面时触发,这里指的是下面几种情形- 从其他页面进入到当前页面
- 小程序从后台进入前台时
- 从微信界面进入小程序
- 按Home键返回桌面后,又重新进入微信小程序
onHide
,跟onShow
基本是对应的- 当navigateTo或底部tab切换时调用
- 点击右上角的按钮,小程序进入后台时
onUnload
- 当redirectTo或navigateBack的时候调用
- 整个小程序被销毁时
更多可以参考
4. 总结
这是第一次在生产项目中使用webSocket,且是在小程序环境中运行,遇见了不少问题(十几个内测bug/捂脸),索性最后都得到了解决。
其中,Android手机不转发wss走代理的问题,花了一两天的时间进行调试,最终发现问题的原因简直羞愧难当~
此外这个项目的规模虽然不大,但是逻辑比较多,前期没有规划好,后台的调整和修复导致代码比较繁复,有不少优化的余地,开发时间较紧,留下了一些技术债务,后面都是要还的~
需要更多帮助,可参考Netty WebSocket 分类下的其它文章。