WebSocket学习文档

一、Websocket说明

1.1、Websocket简介

WebSocket是一种网络通信协议,RFC6455定义了它的通信标准。

WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。

HTTP协议是一种无状态的、无连接的、单向的应用层协议,它采用了请求/响应模型,即通信请求只能由客户端发起,服务端对请求作出应答处理。但是HTTP协议无法实现服务器主要向客户端发起消息。这种单向请求的特点,注定了如果服务有连续的状态变化,客户端要获取就非常麻烦。大多数Web应用程序将通过频繁的异步AJAX请求实现长轮询,但是轮询的效率非常低,非常浪费资源。

Http协议请求过程:

在这里插入图片描述

WebSocket协议:

在这里插入图片描述

1.2、Websocket简介

本协议有两部分:握手和数据传输

握手是基于Http协议的。

来自客户端的握手如下所示:

GET ws:///localhost:chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connetion: Upgrade
Sec-WebSocket-Key: dGh1IHNhbXBsZSBub25jzQ==
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-version: 13

来自客户端的握手说明:

首先是GET请求方式,wswebsocket协议,可不是http协议奥,Connetion: Upgrade表示协议升级,升级的目标协议通过Upgrade: websocket来指定,也就是说最终从http协议升级到了websocket协议

来自服务握手如下所示:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connetion: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbk+xOo==
Sec-WebSocket-Extensions: permessage-deflate

来自服务端的握手说明:

Connetion: Upgrade表示协议升级,升级的目标协议通过Upgrade: websocket来指定,也就是说最终从http协议升级到了websocket协议,其中Sec-WebSocket-KeySec-WebSocket-Accept后面的值是客户端和服务端的识别标识,通过后面的值可以让两者进行互相识别

字段说明:

头名称 说明
Connetion: Upgrade 标志该HTTP请求是一个协议升级请求
Upgrade: websocket 协议升级为WebSocket协议
Sec-WebSocket-version: 13 客户端支持WebSocket协议的版本
Sec-WebSocket-Key: dGh1IHNhbXBsZSBub25jzQ== 客户端采用Base64编码的24位随机字符序列,这是服务器接收客户端HTTP协议升级的证明,并要求服务端响应一个对应加密的Sec-WebSocket-Accept头信息作为应答
Sec-WebSocket-Extensions: permessage-deflate 协议扩展类型

1.3、客户端(浏览器)实现

1.3.1、websocket对象

所有必须的客户端功能(主要指支持Html5的浏览器),都将通过WebSocket对象提供,以下API用于创建WebSocket对象:

var ws = new WebSocket(url);

参数url格式说明:ws://ip:port/资源名称

1.3.2、WebSocket事件

事件 事件处理程序 描述
open websocket对象.open 连接建立时触发
message websocket对象.message 客户端接收到服务端数据时触发
error websocket对象.error 通信发生错误时触发
close websocket对象.close 连接关闭时触发

1.3.3、WebSocket方法

方法 描述
send() 使用连接发送数据

1.4、服务端实现

Tomcat的7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356)

Java WebSocket应用由一系列的WebSocketEndpoint组成,Endpoint是一个java对象,代表WebSocekt链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口,就像Servlet之与Http请求一样。

我们可以通过两种方式定义Endpoint:

  • 第一种是编程式;即继承类javax.websocket.Endponit并实现去方法
  • 第二种是注解式;即定义一个POJO,并添加@ServerEndpoint相关注解。

Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法,规范实现着确保生命周期的各个阶段调用实例的相关方法。生命周期方法如下:

方法 含义描述 注解
onClose 当会话关闭时调用 @OnClose
onOpen 当开启一个新的会话时调用 @OnOpen
onError 当连接过程中异常时调用 @OnError

服务端如何接受客户端发送的数据呢?

通过为Session添加MessageHandler消息处理器来接受消息,当采用注解方式定义EndPoint时,我们还可以通过@OnMessage注解指定接受消息的方法。

服务端如何推送数据给客户端呢?

发送消息则由RemoteEndpoint完成,其实例由Session维护,根据使用情况,我们可以通过Session.getBasicRemote获取同步消息发送的实例,然后调用其sendXXX()方法就可以发送消息,可以通过Session.getAsyncRemote获取异步消息发送实例。

服务端代码:

在这里插入图片描述在这里插入图片描述

二、基于WebSocket的网页聊天室

1、实现流程

在这里插入图片描述

2、消息格式

在这里插入图片描述

三、编码(SpringBoot整合WebSocket)

1、代码

地址:https://gitee.com/mkdxm61/websocket-chatroom-demo.git

说明: 该代码主体来自于https://gitee.com/youngyajun/websocket-chatroom-demo,然后我在此基础上添加了注释和bug修复

2、解释

2.1、用户登录

用户名可以随意输入,密码是123,然后就可以登录了,前端代码在resources/static/html/index.htmlresources/static/js/index.js中,后端代码在com.yyj.controller.LoginController.login()

2.2、服务端WebSocket中@OnOpen、@OnMessage、@OnClose下面的方法如何生效

com.yyj.ws.ChatEndpoint: 该类上面添加的有@Component注解,说明该对象将会交给spring管理,虽然在该类上面添加了@ServerEndpoint注解,但是WebSocket的几个事件依然无法使用

com.yyj.config.WebSocketConfig: 该配置类中创建了一个ServerEndpointExporter对象,该对象可以扫描添加@ServerEndpoint注解的类,所以上面的ChatEndpoint中的几个事件就可以起到作用了

com.yyj.ws.GetHttpSessionConfigurator: 该类的作用就是将当前会话中的HttpSession对象存储在ServerEndpointConfig对象中

com.yyj.ws.ChatEndpoint中添加@OnOpen、@OnMessage、@OnClose的方法: 这些注解下面的方法中的参数我们可以参考EndPoint,如下:

在这里插入图片描述

2.3、onOpen事件触发

前端登录成功之后,会进入resources/static/html/chat.html,该html网页加载了chat.js脚本,在resources/static/js/chat.js中有这样一行代码var ws = new WebSocket("ws://localhost:8088/chat"),作用是让客户端(浏览器)和服务端建立连接,建立连接的时候就会调用服务端的com.yyj.ws.ChatEndpoint#onOpen方法,之后客户端中的onOpen事件将被触发,也就是chat.js中的ws.onopen事件,open事件中的方法将会把用户信息和在线状态展示出来,即下列代码:

$('#chatMeu').html('<p>用户:' + username + "<span style='color: greenyellow; height: 20px'>(在线)</span></p>")

我们着重来说明一下后端的@onOpen注解下面的方法所做的事情,首先我们可以看到com.yyj.ws.ChatEndpoint#onlineUsers对象,该对象属于ChatEndpoint类,用于存储在线用户信息,其中key是用户名称,值是ChatEndpoint对象,该ChatEndpoint对象中可以获取Session对象,该对象中可以从服务端向客户端发送请求,这实现了全双工通信,com.yyj.ws.ChatEndpoint#session对象是ChatEndpoint对象的内部属性,这也就是我们上面所说的“从服务端向客户端发送请求的对象”,我们回到com.yyj.ws.ChatEndpoint#onOpen方法,一行一行代码看:

// session可以从服务端向客户端发送请求,这个操作的作用是将session对象存储在.ChatEndpoint#onlineUsers对象中
this.session = session;
// 获取HttpSession对象,该对象的获取过程在com.yyj.ws.GetHttpSessionConfigurator#modifyHandshake方法中
HttpSession httpSession = (HttpSession) endpointConfig.getUserProperties().get(HttpSession.class.getName());
// 可以让其他方法使用,比如onMessage方法
this.httpSession = httpSession;
// 获取用户名称,存储用户名称的代码在com.yyj.controller.LoginController#login方法中
String username = (String) httpSession.getAttribute("username");
// 将ChatEndpoint对象存储在onlineUsers集合中,用来存储当前在线用户信息,方便全局广播使用
onlineUsers.put(username, this);
// 获取消息,由于我们告诉所有用户,目前谁登陆了,所以systemMsgFlag是true;由于是全局消息,所以fromName是null;message是一个Set集合,存放在线用户名称,方便在页面上的在线用户列表和用户登录消息中列出
String message = MessageUtils.getMessage(true, null, getAllOnlineUsername());
// 全局广播消息
broadcastMsgToAllOnlineUsers(message);

既然提到了broadcastMsgToAllOnlineUsers方法,我们顺便了看一下该方法中写了什么:

// 获取在线用户名称
Set<String> names = onlineUsers.keySet();
// 获取ChatEndpoint对象,该对象中有个Session属性,可以用来从服务端向客户端发送消息
ChatEndpoint chatEndpoint = onlineUsers.get(name);
// 获取可以发送消息到客户端的对象
RemoteEndpoint.Basic basicRemote = chatEndpoint.session.getBasicRemote();
// 从服务端向客户端发送消息
basicRemote.sendText(message);

2.4、OnClose事件触发

比如当客户端(浏览器)关闭的时候,com.yyj.ws.ChatEndpoint#onClose方法就会被调用,我们一行一行来看代码:

// 获取当前退出用户名称
String username = (String) httpSession.getAttribute("username");
// 将离线用户从在线用户列表中移除
onlineUsers.remove(username);
// 全局广播目前在线的用户
String message = MessageUtils.getMessage(true, null, getAllOnlineUsername());
broadcastMsgToAllOnlineUsers(message);

因为当浏览器都关闭了,那么chat.js中的onClose事件就没啥好说了,除非当后端服务关闭了,那么前端的onClose事件将会触发

2.5、onMessage事件触发

当发送按钮被点击的时候,chat.js中的$("#submit").click()方法将被调用,首先检查对方在线状态,然后检查输入框内容是否为空,然后清空发送框,并准备发送给服务端的消息,然后将当前发送的消息拼接到当前聊天框中,然后获取当前用户和被发送消息的用户的以往发送过的消息内容,并拼接最新的内容到以往消息最后面,最终当然是通过ws.send(JSON.stringify(sendJson))把消息发送给服务端了

然后服务端中的com.yyj.ws.ChatEndpoint#onMessage方法就接收到了请求,然后我们一步一步来看代码吧,如下:

// 获取客户端发送给服务端的消息
Message msg = objectMapper.readValue(message, Message.class);
// 获取消息接收者
String toName = msg.getToName();
// 获取消息内容
String msgData = msg.getMessage();
// 获取消息发送者
String username = (String) httpSession.getAttribute("username");
// 获取发送给个人的消息
String sendMsg = MessageUtils.getMessage(false, username, msgData);
// 根据消息接收者名称,找到消息接收者对应的ChatEndpoint对象,进而找到该对象中的Session对象,进入完成消息发送操作,其中不仅仅可以使用sendText方法,还有sendBinary、sendObject方法
onlineUsers.get(toName).session.getBasicRemote().sendText(sendMsg);

当我们把消息从服务端发送到客户端的时候,客户端的onMessage事件将会被触发,我们来看chat.js中的onmessage事件,一行一行来看代码吧,如下:

// 获取服务端发送过来的对象
var dataStr = evt.data;
// 将String字符串转换成Json对象
var jsonData = JSON.parse(dataStr);
// 判断是否是系统消息
if (jsonData.systemMsgFlag)

如果是系统消息,将会重新根据在线用户列表渲染右侧内容,如下:
在这里插入图片描述

如果是个人消息,既然找到此处了,那肯定是找该客户端的是没错了,如果正好是聊天框的人,那么就线拼接消息到聊天框底部,然后把消息记录到sessionStorage中;当然用户可能在和别人聊天,那就需要把消息存储到sessionStorage中,当想和发送消息的用户聊天的时候,也能看到最新消息

在这里插入图片描述

三、作用

  1. 即时通讯产品:上面的Demo比较简单,无法实现在线用户向不在线用户发送消息,另外也无法实现消息持久化,在下面的拓展中,我将提出改进意见
  2. 在页面上操作了一些事情,但是无法得知该事情啥时候结束,不过页面上需要看到结果:比如在索引管理后台中创建索引,后台使用异步方法创建,前端不知道啥时候能创建完成,但是索引创建状态需要发生变化,这就需要用到WebSocket;比如考核系统在进行组卷的时候,当我们选择了一道试题,但是后台需要做一系列操作,所以后台需要进行异步处理,所以前台依然需要得知目前选中的题目数量,这就可以使用WebSocket来做。

四、拓展

Demo改进:上面我们提到demo无法向不在线用户发送消息,原因就是消息接收者不在线,然后从服务端向客户端发送消息的session不存在,我们无法向其发送消息,目前的处理办法是告诉消息发送者“消息接受者不在线,无法发送消息”,如果想解决这个问题,我们可以把消息存储在mq之中,当消息接收者上线之后,从服务端向客户端发送消息的对象就存在了,只需要把mq中的消息消费掉,我们就可以通过session向消息接收者发送消息了,我们通过下面的例子做详细说明:

假设目前张三需要给李四发送消息,但是李四不在线,我们把消息从客户端发送到服务端之后,服务端知道消息发送者、消息接收者、消息类型、消息内容,然后服务端在onMessage方法中把消息发送到mq的队列之中,队列的名称可以使用消息发送者和消息接收者的id等来生成,当李四上线之后,然后服务端在onOpen方法之中将会消费mq队列中的消息,然后通过session对象将消息从服务端发送到客户端,这就是完成了消息的发送,即使对方不在线,也可以发送消息

后续有时间的话我会把它实现的

猜你喜欢

转载自blog.csdn.net/qq_42449963/article/details/124206756
今日推荐