📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍
写在前面的话
本系列的上篇文章《知识点扫盲 · 学会 WebService》介绍了企业开发中WebService
技术的实际应用,这边继续介绍一下WebSocket
的基础应用,希望可以帮助到大家。
这里先介绍实战运用,深入的部分后续专题介绍,让我们开始!
Tips:WebSocket,总感觉名字和 WebService 怎么那么像?WS又算谁的缩写呢?
长连接 WebSocket
技术简介
1、WebSocket 是 HTML5 开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。
2、WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
3、WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
4、虽然前后端都可以互相推数据,但主要还是后端推送给前端,常见运用场景:实时聊天、通知公告、视频弹幕。
Tips:总结一句话,保持长连接,让前后端可以互相交互,畅通无阻。
四个事件
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发
Tips:混个眼熟。
SpringBoot 整合 WS
Step1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
Step2、添加配置类
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
Step3、添加具体Socket服务
@Component
@Slf4j
@ServerEndpoint("/webSocket/{userId}")
public class WebSocketHandle {
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 用户ID
*/
private String userId;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
* 虽然@Component默认是单例模式的,但SB还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
*/
private static final CopyOnWriteArraySet<WebSocketHandle> WEB_SOCKETS = new CopyOnWriteArraySet<>();
/**
* 用来存在线连接用户信息
*/
private static final ConcurrentHashMap<String, Session> SESSION_POOL = new ConcurrentHashMap<>();
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
WebSocketHandle that = (WebSocketHandle) o;
return Objects.equals(session, that.session) && Objects.equals(userId, that.userId);
}
@Override
public int hashCode() {
return Objects.hash(session, userId);
}
/**
* 链接成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId) {
//初始化设置当前实例的信息
this.session = session;
this.userId = userId;
//加入全局Socket管理(Set)
WEB_SOCKETS.add(this);
//加入全局session管理(Map)
SESSION_POOL.put(userId, session);
log.info("WebSocket有新的连接,用户为:{}, 总数为:{}", userId, WEB_SOCKETS.size());
}
/**
* 链接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
WEB_SOCKETS.remove(this);
SESSION_POOL.remove(this.userId);
log.info("WebSocket有连接断开,用户为:{}, 总数为:{}", userId, WEB_SOCKETS.size());
}
/**
* 收到客户端消息后调用的方法
*/
@OnMessage
public void onMessage(Session session, String message) {
log.info("WebSocket收到客户端消息,用户为:{}, 消息为:{}:", this.userId, message);
}
/**
* 发送错误时的处理
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误,原因:" + error.getMessage());
error.printStackTrace();
}
/**
* 发消息给全部人
*/
public void sendAllMessage(String message) {
log.info("【websocket消息】广播消息:" + message);
for (WebSocketHandle webSocket : WEB_SOCKETS) {
try {
if (webSocket.session.isOpen()) {
webSocket.session.getAsyncRemote()
.sendText(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 发消息给单个人
*/
public void sendOneMessage(String userId, String message) {
Session session = SESSION_POOL.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:" + message);
session.getAsyncRemote()
.sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 发消息给多个人
*/
public void sendMoreMessage(String[] userIds, String message) {
for (String userId : userIds) {
Session session = SESSION_POOL.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:" + message);
session.getAsyncRemote()
.sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
Step4、测试服务端效果
参考:WebSocket在线测试
按上述步骤改造完,就可以得到WS的服务端,地址形如:ws://127.0.0.1:8180/webSocket/123
如果想不写客户端,可以直接使用测试网站进行测试,如下图。
Step5、测试发送消息
随便写一个接口,就可以触发调用了,看看客户端是否有接收到。
注意,这里的webSocketHandle确实是单例的,但是单个Socket连接也是存在的,而且Bean实例里面的static类型的Map和Set还是存了后续想要操作的内容。
@RequestMapping("/socketTest")
public void socketTest(String id) throws Exception {
webSocketHandle.sendOneMessage(id, "hello~");
}
前端 JS 实现 WS
前端使用WebSocket,可以用原生的方式,参考如下。
当然,也可以引用第三方库,比较出名的是socket.io。
/**
* 备忘
* 页面地址:http://localhost:8180/test/socketDemo.html
* 后端发消息:http://localhost:8180/socketTest?id=123
*/
if ("WebSocket" in window) {
let $scope = {}
let wsUrl = "ws://127.0.0.1:8180/webSocket/123"
let ws;
let tt;
let lockReconnect = false;
// 创建WS链接
$scope.createWebSocket = function (wsUrl) {
try {
ws = new WebSocket(wsUrl);
$scope.webSocketInit();
} catch (e) {
lockReconnect = false
$scope.webSocketReconnect(wsUrl)//重连函数
}
};
// 初始化WS的方法
$scope.webSocketInit = function () {
ws.onclose = function (error) {
//连接关闭的回调函数,进行重连
console.log("连接已关闭...", error);
$scope.webSocketReconnect(wsUrl)
};
ws.onerror = function (error) {
//连接错误的回调函数,进行重连
console.log("连接错误...", error);
$scope.webSocketReconnect(wsUrl)
};
ws.onopen = function () {//连接建立
//发一个初始化连接消息
ws.send('初始化连接');
//启动心跳检测
$scope.heartCheck.start();
};
ws.onmessage = function (event) {
if(event.data !== 'pong'){
let $test = $('#textA')
let temp = $test.text();
$test.text(temp + event.data + "\r\n");
console.log("收到后端的消息:", event.data);
} else {
console.log('收到pong消息,连接还正常~')
}
//接收一次后台推送的消息,即进行一次心跳检测重置
$scope.heartCheck.reset();
};
};
$scope.webSocketReconnect = function (url) {
console.log("socket 连接断开,正在尝试重新建立连接");
//TODO 下面这段代码会导致只重连一次,后续改进了再开放
//TODO 长时间如果没收到pong消息应该也要处理,提示一下报错之类的
/*if (lockReconnect) {
return;
}
lockReconnect = true;*/
//没连接上会一直重连,设置延迟,避免请求过多
tt && clearTimeout(tt);
tt = setTimeout(function () {
$scope.createWebSocket(url);
}, 4000)
};
//心跳检测
//onopen连接上,就开始start及时,如果在定时时间范围内,onmessage获取到了服务端消息,就重置reset倒计时,距离上次从后端获取消息30秒后,执行心跳检测,看是不是断了。
$scope.heartCheck = {
timeout: 5000, //默认30秒
timeoutObj: null,
reset: function () { //接收成功一次推送,就将心跳检测的倒计时重置为30秒
clearTimeout(this.timeoutObj);//重置倒计时
this.start();
},
start: function () {//启动心跳检测机制,设置倒计时30秒一次
this.timeoutObj = setTimeout(function () {
//启动心跳
ws.send("ping");
}, this.timeout)
}
};
//开始创建webSocket连接
$scope.createWebSocket(wsUrl);
// 点击发消息给后端,后端收到后回复信息
function sendMessage() {
let message = document.getElementById("messageInput").value;
if (message) {
ws.send(message);
}
}
} else {
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
总结陈词
此篇文章介绍了WebSocket
的基础应用,仅供学习参考。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。