WebSocket#
传统的浏览器和服务器之间的交互模式是基于请求/响应的模式,虽然可以使用js发送定时任务让浏览器在服务器中拉取但是弊端很明显,首先就是不能避免的延迟,其次就是频繁的请求,让服务器的压力骤然提升
WebSocket是H5新增的协议,用于构建浏览器和服务器之间的不受限的长连接的通信模式,不再局限于请求/响应式的模型,服务端可以主动推送消息给客户端,(游戏有某个玩家得奖了的弹幕)基于这个特性我们可以构建我们的实时的通信程序
协议详情:#
websocket建立连接时,是通过浏览器发送的HTTP请求,报文如下:
GET ws://localhost:3000/ws/chat HTTP/1.1 Host: localhost Upgrade: websocket Connection: Upgrade Origin: http://localhost:3000 Sec-WebSocket-Key: client-random-string Sec-WebSocket-Version: 13
- 首先GET请求是以
ws
开头的 - 其中请求头中的
Upgrade: websocket Connection: Upgrade
表示尝试建立WebSocket连接
对于服务端的相应数据
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: server-random-string
其中的101
,表示服务端支持WebSocket协议, 双方基于Http请求,成功建立起WebSocket连接,双方之间的通信也不再通过HTTP
JS对WebSocket的封装对象#
对于JS的WebSocket对象来说,它常用 4个回调方法,以及两个主动方法
方法名 | 作用 |
onopen() | 和服务端成功建立连接后回调 |
onmessage(e) | 收到服务端的的消息后回调,e为消息对象 |
onerror() | 链接出现异常回调,如服务端关闭 |
onclose() | 客户端单方面断开连接时回调 |
send(e) | 主动向服务端推送消息 |
close() | 主动关闭通道 |
再次对WebSocket进行封装#
知道了回调函数回调时机,我们接下来要做的就是在他的整个生命周期的不同回调函数中,添加我们指定的动作就ok了,下面是通过Window定义一个全局的聊天对象CHAT
window.CHAT={ var socket = null; // 初始化socket init:function(){ // 判断当前的浏览器是否支持WebSocket if(window.WebSocket){ // 检验当前的webSocket是否存在,以及连接的状态,如已经连接,直接返回 if(CHAT.socket!=null&&CHAT.socket!=undefined&&CHAT.socket.readyState==WebSocket.OPEN){ return false; }else{// 实例化 , 第二个ws是我们可以自定义的, 根据后端的路由来 CHAT.socket=new WebSocket("ws://192.168.43.10:9999/ws"); // 初始化WebSocket原生的方法 CHAT.socket.onopen=CHAT.myopen(); CHAT.socket.onmessage=CHAT.mymessage(); CHAT.socket.onerror=CHAT.myerror(); CHAT.socket.onclose=CHAT.myclose(); } }else{ alert("当前设备不支持WebSocket"); } } // 发送聊天消息 chat:function(msg){ // 如果的当前的WebSocket是连接的状态,直接发送 否则从新连接 if(CHAT.socket.readyState==WebSocket.OPEN&&CHAT.socket!=null&&CHAT.socket!=undefined){ socket.send(msg); }else{ // 重新连接 CHAT.init(); // 延迟一会,从新发送 setTimeout(1000); CHAT.send(msg); } } // 当连接建立完成后对调 myopen:function(){ // 拉取连接建立之前的未签收的消息记录 // 发送心跳包 } mymessage:function(msg){ // 因为服务端可以主动的推送消息,我们提前定义和后端统一msg的类型, 如,拉取好友信息的消息,或 聊天的消息 if(msg==聊天内容){ // 发送请求签收消息,改变请求的状态 // 将消息缓存到本地 // 将msg 转换成消息对象, 植入html进行渲染 }else if(msg==拉取好友列表){ // 发送请求更新好友列表 } } myerror:function(){ console.log("连接出现异常..."); } myclose:function(){ console.log("连接关闭..."); } keepalive: function() { // 构建对象 var dataContent = new app.DataContent(app.KEEPALIVE, null, null); // 发送心跳 CHAT.chat(JSON.stringify(dataContent)); // 定时执行函数, 其他操作 // 拉取未读消息 // 拉取好友信息 } }
对消息类型的约定#
WebSocket对象通过send(msg)
;方法向后端提交数据,常见的数据如下:
- 客户端发送聊天消息
- 客户端签收消息
- 客户端发送心跳包
- 客户端请求建立连接
为了使后端接收到不同的类型的数据做出不同的动作, 于是我们约定发送的msg的类型;
// 消息action的枚举,这个枚举和后端约定好,统一值 CONNECT: 1, // 第一次(或重连)初始化连接 CHAT: 2, // 聊天消息 SIGNED: 3, // 消息签收 KEEPALIVE: 4, // 客户端保持心跳 PULL_FRIEND:5, // 重新拉取好友 // 消息模型的构造函数 ChatMsg: function(senderId, receiverId, msg, msgId){ this.senderId = senderId; this.receiverId = receiverId; this.msg = msg; this.msgId = msgId; } // 进一步封装两个得到最终版消息模型的构造函数 DataContent: function(action, chatMsg, extand){ this.action = action; this.chatMsg = chatMsg; this.extand = extand; }
如何发送数据?#
我们使用js,给发送按钮绑定点击事件,一经触发,从缓存中获取出我们需要的参数,调用
CHAT.chat(Json.stringify(dataContent));
后端netty会解析dataContent的类型,进一步处理
如何签收未与服务器连接时好友发送的消息?#
- 消息的签收时机:
之所以会有未签收的信息,是因为客户端未与服务端建立WebSocket连接, 当服务端判断他维护的channel组中没有接受者的channel时,不会发送数据,而是把数据持久化到数据库,并且标记flag=未读, 所以我们签收信息自然放在客户端和服务端建立起连接时的回调函数中执行 - 步骤:
- 客户端通过js请求,拉取全部的和自己相关的flag=未读的消息实体列表
- 从回调函数数中,把列表中的数据取出,缓存在本地
- 将列表中的数据回显在html页面中
- 和后端约定,将该列表中所有的实例的id取出,用逗号分隔拼接成字符串, 以
action=SIGNED
的方式发送给后端,让其进行签收