目录
- 什么是websocket
- websocket 原理
- websocket与http的关系
- 实际开发
- 后端代码
- 总结:
什么是websocket
WebSocket 是一种网络通信协议。RFC6455定义了它的通信标准。
WebSocket是HTML5下一种新的协议(websocket协议本质上是一个基于tcp的协议)
它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的
http是一种无状态,无连接,单向的应用层协议,它采用了请求/响应模型,通信请求只能由客户端发起,服务端对请求做出应答处理。这样的弊端显然是很大的,只要服务端状态连续变化,客户端就必须实时响应,都是通过javascript与ajax进行轮询,这样显然是非常麻烦的,同时轮询的效率低,非常的浪费资源(http一直打开,一直重复的连接)。
于是就有了websocket,Websocket是一个持久化的协议,它是一种全面双工通讯的网络技术,任意一方都可以建立连接将数据推向另一方,websocket只需要建立一次连接,就可以一直保持
websocket 原理
websocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信
在websocket出现之前,web交互一般是基于http协议的短连接或者长连接
websocket是一种全新的协议,不属于http无状态协议,协议名为"ws"
说它是TCP传输,主要体现在建立长连接后,浏览器是可以给服务器发送数据,服务器也可以给浏览器发送请求的。当然它的数据格式并不是自己定义的,是在要传输的数据外层有ws协议规定的外层包的。
websocket与http的关系
相同点:
都是基于tcp的,都是可靠性传输协议,都是应用层协议
不同点:
WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息
HTTP是单向的
WebSocket是需要浏览器和服务器握手进行建立连接的
而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接
联系
WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的
关系图
实际开发
我们有一个需求就是报警数据来了之后在前端报警处理,不使用websocket就只能通过轮询每3秒调用一次接口,在吧查回来的数据进行判断,如果多了就开始调接口继续操作,这样很浪费资源。
使用websocket之后,建立连接之后。后端察觉数据变化之后,通过他的send方法通知前端,前端通过onmessage提示调用,可以在里面直接调用查询数据接口继续操作。
我们这里是建立连接通过他数据变化后端通知前端,前端有个方法会执行,我们在这个方法里面调用我们查询接口,也可以是后端把这条数据发回来我们处理,根据实际情况而定。
后端代码
我们后端是做了容器化的分布式的,主要代码如下
public class WebSocketConfig { | |
/** | |
* 如果使用Springboot默认内置的tomcat容器,则必须注入ServerEndpoint的bean; | |
* 如果使用外置的web容器,则不需要提供ServerEndpointExporter,下面的注入可以注解掉 | |
*/ | |
public ServerEndpointExporter serverEndpointExporter(){ | |
return new ServerEndpointExporter(); | |
} | |
} | |
public class WebSocketServer { | |
| |
//与某个客户端的连接会话,需要通过它来给客户端发送数据 | |
private Session session; | |
| |
private static CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>(); | |
//用来存放每个客户端对应的WebSocket对象。 | |
private static Map<String,Session> sessionPool = new HashMap<>(); | |
| |
/** | |
* 连接成功后调用的方法 | |
* @param session | |
* @param key | |
*/ | |
public void onOpen(Session session, ("key") String key) throws IOException { | |
//key为前端传给后端的token | |
this.session = session; | |
//从token中获取到userid当做区分websocket客户端的key | |
Long userId = JwtHelper.getUserId(key); | |
sessionPool.put(String.valueOf(userId),session); | |
if (sessionPool.get(String.valueOf(userId))==null){ | |
webSockets.add(this); | |
} | |
webSockets.add(this); | |
System.out.println("webSocket连接成功"); | |
System.out.println("WebSocket有新的连接,连接总数为:"+webSockets.size()); | |
} | |
| |
/** | |
* 连接关闭调用的方法 | |
*/ | |
public void onClose() { | |
webSockets.remove(this); | |
System.out.println("webSocket连接关闭"); | |
} | |
| |
/** | |
* 收到客户端消息后调用的方法,根据业务要求进行处理,这里就简单地将收到的消息直接群发推送出去 | |
* @param message 客户端发送过来的消息 | |
*/ | |
public void onMessage(String message) { | |
//心跳检测 | |
int beginIndex = 10; | |
if ("HeartBeat".equals(message.substring(0,9))){ | |
String token = message.substring(beginIndex); | |
System.out.println("token1"+token); | |
if (!"".equals(token)){ | |
System.out.println("token2"+token); | |
Long userId = JwtHelper.getUserId(token); | |
sendTextMessage(String.valueOf(userId),"ok"); | |
} | |
} | |
System.out.println("WebSocket收到客户端消息:"+message); | |
} | |
/** | |
* 发生错误时的回调函数 | |
* @param session | |
* @param error | |
*/ | |
public void onError(Session session, Throwable error) { | |
log.error("发生错误"); | |
error.printStackTrace(); | |
} | |
| |
/** | |
* 实现服务器主动推送消息 | |
*/ | |
//单点消息发送 | |
public void sendTextMessage(String key,String message){ | |
Session session = sessionPool.get(key); | |
if (session!=null){ | |
try { | |
session.getBasicRemote().sendText(message); | |
}catch (Exception e){ | |
e.printStackTrace(); | |
} | |
} | |
} | |
| |
} |
前端代码
1.在src/utils 建立websocket.js 引入文件,这个websocket是全局的,通过登录,和退出控制,在哪儿页面都可以使用。
// 提示信息 | |
import { Message } from 'element-ui' | |
// 引入token 解析用户id在后端处理 | |
import { getToken } from '@/utils/auth' | |
| |
var url = 'ws://后端地址/equipment/websocket/' | |
| |
var ws | |
var tt | |
var lockReconnect = false //避免重复连接 | |
var clientId = getToken() //cookies中获取token值 | |
| |
var websocket = { | |
// 建立连接 | |
Init: function (clientId) { | |
if ('WebSocket' in window) { | |
ws = new WebSocket(url + clientId) | |
} else if ('MozWebSocket' in window) { | |
ws = new MozWebSocket(url + clientId) | |
} else { | |
// console.log('您的浏览器不支持 WebSocket!') | |
return | |
} | |
// websocket 生命周期根据websocket状态自己会执行 | |
// websocket 成功 失败 错误 断开 这里会自动执行 | |
// 这个方法后端通过send调用 这个方法会执行和接收参数 | |
ws.onmessage = function (e) { | |
// console.log('接收消息:' + e.data) | |
heartCheck.start() | |
if (e.data == 'ok') { | |
//心跳消息不做处理 | |
return | |
} | |
Message({ | |
message: e.data, | |
type: 'success' | |
}) | |
//messageHandle(e.data) | |
} | |
ws.onclose = function () { | |
console.log('连接已关闭') | |
Message({ | |
message: '报警功能连接已关闭', | |
type: 'error' | |
}) | |
reconnect(clientId) | |
} | |
ws.onopen = function () { | |
// console.log('连接成功') | |
Message({ | |
message: '报警功能连接成功', | |
type: 'success' | |
}) | |
heartCheck.start() | |
} | |
| |
ws.onerror = function (e) { | |
// console.log('数据传输发生错误') | |
Message({ | |
message: '数据传输发生错误', | |
type: 'error' | |
}) | |
reconnect(clientId) | |
} | |
}, | |
// 我们单独写了一个方法 调用ws的关闭方法,这样就可以在退出登录的时候主动关闭连接 | |
//关闭连接 | |
onClose: function () { | |
console.log('主动关闭连接!') | |
//关闭websocket连接和关闭断开重连机制 | |
lockReconnect = true | |
// 调用 上面的websocket关闭方法 | |
ws.close() | |
}, | |
// 前端的send给后端发信息 | |
Send: function (sender, reception, body, flag) { | |
let data = { | |
sender: sender, | |
reception: reception, | |
body: body, | |
flag: flag | |
} | |
let msg = JSON.stringify(data) | |
// console.log('发送消息:' + msg) | |
ws.send(msg) | |
}, | |
// 返回ws对象 | |
getWebSocket () { | |
return ws | |
}, | |
// websocket 自带的状态码意思提示 | |
getStatus () { | |
if (ws.readyState == 0) { | |
return '未连接' | |
} else if (ws.readyState == 1) { | |
return '已连接' | |
} else if (ws.readyState == 2) { | |
return '连接正在关闭' | |
} else if (ws.readyState == 3) { | |
return '连接已关闭' | |
} | |
} | |
} | |
| |
// 刷新页面后需要重连 | |
if (window.performance.navigation.type == 1 && getToken() != null) { | |
//刷新后重连 | |
// reconnect(clientId); | |
websocket.Init(clientId) | |
//如果websocket没连接成功,则开始延迟连接 | |
if (ws == null) { | |
reconnect(clientId) | |
} | |
} | |
| |
export default websocket | |
| |
//根据消息标识做不同的处理 | |
function messageHandle (message) { | |
let msg = JSON.parse(message) | |
switch (msg.flag) { | |
case 'command': | |
// console.log('指令消息类型') | |
break | |
case 'inform': | |
// console.log('通知') | |
break | |
default: | |
// console.log('未知消息类型') | |
} | |
} | |
// 重连方法 刷新页面 连接错误 连接关闭时调用 | |
function reconnect (sname) { | |
if (lockReconnect) { | |
return | |
} | |
lockReconnect = true | |
//没连接上会一直重连,设置延迟避免请求过多 | |
tt && clearTimeout(tt) | |
tt = setTimeout(function () { | |
// console.log('执行断线重连...') | |
websocket.Init(sname) | |
lockReconnect = false | |
}, 4000) | |
} | |
| |
//心跳检测 跟后端是对应的 会进行处理 | |
// 连接成功 和后端推消息时调用 | |
var heartCheck = { | |
timeout: 1000 * 60 * 3, | |
timeoutObj: null, | |
serverTimeoutObj: null, | |
start: function () { | |
// console.log('开始心跳检测') | |
var self = this | |
this.timeoutObj && clearTimeout(this.timeoutObj) | |
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj) | |
this.timeoutObj = setTimeout(function () { | |
//这里发送一个心跳,后端收到后,返回一个心跳消息, | |
//onmessage拿到返回的心跳就说明连接正常 | |
// console.log('心跳检测...') | |
ws.send('HeartBeat:' + clientId) | |
self.serverTimeoutObj = setTimeout(function () { | |
if (ws.readyState != 1) { | |
ws.close() | |
} | |
// createWebSocket(); | |
}, self.timeout) | |
}, this.timeout) | |
} | |
} | |
|
2.在登录和退出的时候进行websocket进行建立连接和关闭连接,就是在vuex(src/store/modules/user.js)调用这里方法(详细的Vuex做登录主页文章会有)。
// 引入外部文件 | |
import websocket from '@/utils/websocket' | |
// 请求 | |
const actions = { | |
// 登录 | |
async login (ctx, data) { | |
// 调用mutations里的方法存到state | |
// 登录成功后创建websocket连接 | |
// res.data 是token | |
websocket.Init(res.data) | |
ctx.commit('SET_TOKEN', res.data) | |
}, | |
// 退出登录 | |
logout (ctx) { | |
// 主动关闭连接 | |
websocket.onClose() | |
Message.success('退出成功,请重新登录') | |
} | |
} |
3.在需要用到websocket使用,我们这里是在首页使用。
// 引入websocket文件 | |
import websocket from '@/utils/websocket' | |
| |
// 登录成功一进到页面的时候调用 | |
created() { | |
this.getWebSocket() | |
}, | |
| |
// getWebSocket()方法 | |
method: { | |
// websocket 接受消息 | |
getWebSocket() { | |
// websocket.getWebSocket()这个是websocket里面方法返回ws对象(websocket.js) | |
let ws = websocket.getWebSocket() | |
// 通过ws这个对象获取这个websocket里面后端推消息前端执行的方法onmessage。 | |
// 给他赋给我们自己方法 this.websocketonmessage | |
websocketonmessage(e)就会执行 | |
ws.onmessage = this.websocketonmessage | |
}, | |
//接收到消息的回调函数 | |
websocketonmessage(e) { | |
// e后端传回来参数 | |
// console.log(e.data); | |
// 防止心跳监测,返回来的ok 对方法执行的影响(心跳监测方法也会执行一次) | |
if (e.data == 'ok') { | |
//心跳消息不做处理 | |
return | |
} | |
// 需要监测的接口 我们查询数据的接口 在进行处理 | |
this.alarmerlist() | |
}, | |
} |
细节:这样登录创建连接之后,后端察觉到数据变化,就通过他的send方法给我们推消息我们前端websocket.js文件的onmessage这个方法会自己调用执行并会接受参数。我们在需要的页面引入websocket使用。把它赋值我们自己写的方法,这样数据一变化我们就会调用一次查询接口,进行处理,大大节约性能(http要用轮询一直调用查询接口)。
总结:
经过这一趟流程下来相信你也对 Vue 中前后端使用WebSocket 有了初步的深刻印象,但在实际开发中我 们遇到的情况肯定是不一样的,所以我们要理解它的原理,万变不离其宗。加油,打工人!