WebSocket
1.什么是WebSocket
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
2. WebSocket技术出现之前,Web端实现即时通讯的方法有哪些
2.1 定期轮询的方式
客户端按照某个时间间隔不断地向服务端发送请求,请求服务端的最新数据然后更新客户端显示。这种方式实际上浪费了大量流量并且对服务端造成了很大压力。
2.2 SSE(Server-Sent Event,服务端推送事件)
SSE(Server-Sent Event,服务端推送事件)是一种允许服务端向客户端推送新数据的HTML5技术。与由客户端每隔几秒从服务端轮询拉取新数据相比,这是一种更优的解决方案。
相较于WebSocket,它也能从服务端向客户端推送数据。WebSocket能做的,SSE也能做,反之亦然,但在完成某些任务方面,它们各有千秋。
2.3 Comet技术
基于 HTTP 长连接的“服务器推”技术,Comet并不是一种新的通信技术,它是在客户端请求服务端这个模式上的一种hack技术,通常来讲,它主要分为以下两种做法:
2.3.1 基于长轮询 的服务端推送技术
具体来讲,就是客户端首先给服务端发送一个请求,服务端收到该请求之后如果数据没有更新则并不立即返回,服务端阻塞请求的返回,直到数据发生了更新或者发生了连接超时,服务端返回数据之后客户端再次发送同样的请 求
**2.3.2 ** 基于流式数据传输的长连接
通常的做法是在页面中嵌入一个隐藏的iframe,然后让这个iframe的src属性指向我们请求的一个服务端地址,并且为了数据更新,我们将页面上数据更新操作封装为一个js函数,将函数名当做参数传递到这个地址当中。
服务端收到请求后解析地址取出参数(客户端js函数调用名),每当有数据更新的时候,返回对客户端函数的调用,并且将要跟新的数据以js函数的参数填入到返回内容当中,例如返回“”这样一个字符串,意味着以data为参数调用客户端update函数进行客户端view更新。
comet技术是针对客户端请求服务器响应模型而模拟出的一个服务端推送数据实时更新技术。而且由于浏览器兼容性不能够广泛应用。
3. WebSocket 和 Socket 的区别
Socket 不是协议,是应用层与 TCP/IP 通信的中间软件抽象层,是一组接口。而 WebSocket 是应用层协议。
4. WebSocket 基本使用
4.1 协议
WebSocket 的协议标识符是ws,如果在 TLS 协议上,标识符是wss,类似于 https
4.2 触发事件
@OnOpen 连接成功触发事件
@OnClose 连接关闭触发事件
@OnMessage 收到消息触发事件
@OnError 连接异常触发事件
4.3 注册WebSocket
/**
* @author: 桂秋拾貳
* @date: 2022/10/24 18:25
* @description: 开启WebSocket支持
*/
@Configuration
public class WebSocketConfig {
/**
* 功能描述: 注入ServerEndpointExporter bean对象,自动注册使用了@ServerEndpoint注解的bean
*
* @Author 桂秋拾貳
* @Date 18:26 2022/10/24
* @param
* @Return org.springframework.web.socket.server.standard.ServerEndpointExporter
**/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
4.4 消息类
/**
* @author: 桂秋拾貳
* @date: 2022/10/24 16:04
* @description: 服务器发送给浏览器的数据
*/
@Data
public class ResultMessage {
/**
* 是否为系统消息
*/
private boolean isSystem;
/**
* 发送人
*/
private String fromName;
/**
* 消息
*/
private Object message;
/**
* 功能描述: 系统消息
*
* @Author 桂秋拾貳
* @Date 14:33 2022/11/1
* @param message
* @Return com.buba.config.websocket.ResultMessage
**/
public static ResultMessage systemMessage (Object message) {
ResultMessage resultMessage = new ResultMessage();
resultMessage.setSystem(true);
resultMessage.setMessage(message);
return resultMessage;
}
/**
* 功能描述: 用户消息
*
* @Author 桂秋拾貳
* @Date 14:33 2022/11/1
* @param fromName
* @param message
* @Return com.buba.config.websocket.ResultMessage
**/
public static ResultMessage userMessage (String fromName, Object message) {
ResultMessage resultMessage = new ResultMessage();
resultMessage.setSystem(false);
resultMessage.setFromName(fromName);
resultMessage.setMessage(message);
return resultMessage;
}
}
4.5 配置类
@ServerEndpoint("/connect/{userName}")
@Component
public class WebSocketServer {
/**
* 用来存储每一个客户端对象对应的ChatEndpoint对象
*/
private static Map<String, WebSocketServer> onlineUsers = new HashMap<>();
/**
* 声明session对象,通过该对象可以发送消息给指定的用户
*/
private Session session;
/**
* 接收userId
*/
private String userName="";
/**
* 功能描述: 连接建立时候调用
*
* @Author 桂秋拾貳
* @Date 18:52 2022/10/24
* @param session
* @param userName
* @Return void
**/
@OnOpen
public void onOpen (Session session, @PathParam(value="userName") String userName) {
this.session = session;
this.userName = userName;
if (onlineUsers.containsKey(userName)) {
onlineUsers.remove(userName);
}
onlineUsers.put(userName, this);
System.out.println("用户:" + userName + "加入服务器,当期在线人数:" + onlineUsers.size());
}
/**
* 功能描述: 接收到客户端发送的数据时被调用
*
* @Author 桂秋拾貳
* @Date 18:23 2022/10/24
* @param message
* @param session
* @Return void
**/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
if (message != null) {
// 解析发送的消息
JSONObject jsonObject = JSONObject.parseObject(message);
String from = jsonObject.get("fromName").toString();
String to = jsonObject.get("to").toString();
String mes = jsonObject.get("message").toString();
if (mes.equals("heartCheck")) {
sendMessage(mes);
} else {
// 组装发送人和消息
ResultMessage resultMessage = ResultMessage.userMessage(from, mes);
// 根据接收人获取对应的服务,并发送
onlineUsers.get(to).sendMessage(JSONObject.toJSONString(resultMessage));
}
}
}
/**
* 功能描述: 连接关闭时被调用
*
* @Author 桂秋拾貳
* @Date 18:24 2022/10/24
* @param session
* @Return void
**/
@OnClose
public void onClose (Session session) {
if (onlineUsers.containsKey(userName)) {
onlineUsers.remove(userName);
}
System.out.println("用户:" + userName + "退出服务器,当前在线人数:" + onlineUsers.size());
}
/**
* 功能描述: 连接异常时被调用
*
* @Author 桂秋拾貳
* @Date 18:24 2022/10/24
* @param session
* @param error
* @Return void
**/
@OnError
public void onError (Session session, Throwable error) {
}
/**
* 功能描述: 发送消息
*
* @Author 桂秋拾貳
* @Date 11:39 2022/10/25
* @param message
* @Return void
**/
public void sendMessage (String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
}
5. 前端配置webSocket文件
5.1 普通使用
mounted () {
if ('WebSocket' in window) {
this.websocket = new WebSocket('ws://localhost:8081/connect/' + this.userName)
this.initWebSocket()
} else {
alert('当前浏览器不支持WebSocket!!!')
}
},
methods: {
// 初始化
initWebSocket () {
// 连接错误
this.websocket.onerror = this.setErrorMessage
// 连接成功
this.websocket.onopen = this.setOnopenMessage
// 收到消息的回调
this.websocket.onmessage = this.setOnmessageMessage
// 连接关闭的回调
this.websocket.onclose = this.setOncloseMessage
// 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = this.onbeforeunload
},
setErrorMessage () {
window.console.log('WebSocket连接发生错误,状态码:' + this.websocket.readyState)
},
setOnopenMessage () {
window.console.log('WebSocket连接成功,状态码:' + this.websocket.readyState)
},
// 根据服务器推送的消息做自己的业务处理
setOnmessageMessage (event) {
},
setOncloseMessage () {
window.console.log('WebSocket连接关闭,状态码:' + this.websocket.readyState)
},
onbeforeunload () {
this.closeWebSocket()
},
closeWebSocket () {
this.websocket.close()
},
}
5.2 使用Vuex配置全局WebSocket
5.2.1 在main中全局引入
//引入
import socketPublic from '@/utils/WebSocketVuex'
Vue.prototype.$socketPublic = socketPublic
5.2.2 配置WebSocketVuex.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex)
export default new Vuex.Store({
// 数据,相当于data
state: {
ws: null, // 建立的连接
lockReconnect: false, // 是否真正建立连接
timeout: 15000, // 15秒一次心跳
timeoutObj: null, // 心跳心跳倒计时
serverTimeoutObj: null, // 心跳倒计时
timeoutnum: null, // 断开 重连倒计时
msg: null, // 接收到的信息
url: 'ws://ip:端口号/connect/'
},
// 相当于计算属性
getters: {
// 获取接收的信息
socketMsgs: (state) => state.msg
},
// 里面定义方法,操作state方法
mutations: {
// 初始化ws 用户登录后调用
webSocketInit(state) {
const userName = "拼接用户姓名"
let that = this
if (判断是否有用户名) {
state.ws = new WebSocket(state.url + userName);
} else {
state.ws = new WebSocket(state.url)
}
// this 创建一个state.ws对象【发送、接收、关闭socket都由这个对象操作】
state.ws.onopen = function(res){
console.log("Connection success...");
// 启动心跳检测
that.commit("start");
}
state.ws.onmessage = function(res){
if (res.data === "heartCheck") {
// 收到服务器信息,心跳重置
that.commit("reset");
console.log("socket-heartCheck");
}else{
state.msg = res;
}
}
state.ws.onclose = function(res){
console.log("Connection closed...");
// 重连
that.commit('reconnect');
}
state.ws.onerror = function(res){
console.log("Connection error...");
// 重连
that.commit('reconnect');
}
},
reconnect(state) {
// 重新连接
let that = this;
if (state.lockReconnect) {
return;
}
state.lockReconnect = true;
// 没连接上会一直重连,30秒重试请求重连,设置延迟避免请求过多
state.timeoutnum && clearTimeout(state.timeoutnum);
state.timeoutnum = setTimeout(() => {
// 新连接
that.commit('webSocketInit')
state.lockReconnect = false;
}, 2000);
},
reset(state) {
// 重置心跳
let that = this;
// 清除时间
clearTimeout(state.timeoutObj);
clearTimeout(state.serverTimeoutObj);
// 重启心跳
that.commit('start')
},
start(state) {
console.log('开始心跳检测');
// 开启心跳
var self = this;
state.timeoutObj && clearTimeout(state.timeoutObj);
state.serverTimeoutObj && clearTimeout(state.serverTimeoutObj);
state.timeoutObj = setTimeout(() => {
console.log('心跳检测...');
// 这里发送一个心跳,后端收到后,返回一个心跳消息,
if (state.ws.readyState === 1) {
// 如果连接正常
state.ws.send("heartCheck");
} else {
// 否则重连
self.commit('reconnect');
}
state.serverTimeoutObj = setTimeout(function () {
// 超时关闭
state.ws.close();
}, state.timeout);
}, state.timeout);
},
},
// 操作异步操作mutation
actions: {
webSocketInit({commit}, url) {
commit('webSocketInit', url)
},
webSocketSend({commit}, p) {
commit('webSocketSend', p)
}
},
modules: {
}
})
5.2.3 监听state/getters中属性变化
<script>
export default {
data () {
return {
}
},
watch: {
// 监听vuex中的属性值变化
'$socketPublic.getters.socketMsgs'() {
this.toDoSocket(this.$socketPublic.getters.socketMsgs.data)
}
},
created () {
this.initWebSocket();
},
methods: {
// 初始化ws
initWebSocket() {
this.$socketPublic.dispatch('webSocketInit');
},
// 对接收的消息进行业务处理
toDoSocket (data) {
// JSON.parse(data)
}
}
}
</script>
6. 注意
websocket一段时间无响应就会自动断开连接,需要配合心跳机制来检测使用
7. 前端发送消息
ws.send(消息体)