前言
寫(xiě)這個(gè)項(xiàng)目主要是有有個(gè)項(xiàng)目需要后端有數(shù)據(jù)實(shí)話返回前端,一開(kāi)始采用前端輪詢(xún)的方式,后面覺(jué)得及時(shí)性上有些不行,然后改為使用websocket ,具體實(shí)現(xiàn)demo以及測(cè)試流程發(fā)出來(lái)提供交流學(xué)習(xí),
一、Web Socket 簡(jiǎn)紹
WebSocket是一種在單個(gè)TCP連接上進(jìn)行全雙工通信的協(xié)議。WebSocket通信協(xié)議于2011年被IETF定為標(biāo)準(zhǔn)RFC 6455,并由RFC7936補(bǔ)充規(guī)范。WebSocket API也被W3C定為標(biāo)準(zhǔn)。
WebSocket使得客戶(hù)端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡(jiǎn)單,允許服務(wù)端主動(dòng)向客戶(hù)端推送數(shù)據(jù)。在WebSocket API中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸。
1 為什么用 websocket?
換句話說(shuō),websocket 解決了什么問(wèn)題?答案是,解決了兩個(gè)主要問(wèn)題:
- 只能客戶(hù)端發(fā)送請(qǐng)求
- 一段時(shí)間內(nèi)的頻繁信息發(fā)送
假設(shè)現(xiàn)在需要設(shè)計(jì)一個(gè)實(shí)時(shí)預(yù)警系統(tǒng)的通知模塊,那么作為工程師我們應(yīng)該怎么設(shè)計(jì)通知的這個(gè)功能呢?因?yàn)檫@些系統(tǒng)的數(shù)據(jù)來(lái)源,一般他通過(guò)硬件設(shè)備采集到后臺(tái)的,如果我們現(xiàn)在只有 http 協(xié)議,那么我們只能讓客戶(hù)端不斷地輪詢(xún)服務(wù)器,輪詢(xún)的時(shí)間間隔越小越能接近實(shí)時(shí)的效果??墒?,輪詢(xún)的效率低,又浪費(fèi)資源。針對(duì)這樣的場(chǎng)景,websocket 應(yīng)運(yùn)而生。
特點(diǎn):
(1)建立在 TCP 協(xié)議之上,服務(wù)器端的實(shí)現(xiàn)比較容易,是一個(gè)可靠的傳輸協(xié)議。
(2)與 HTTP協(xié)議有著良好的兼容性。默認(rèn)端口也是80和443,并且握手階段采用 HTTP 協(xié)議,因此握手時(shí)不容易屏蔽,能通過(guò)各種 HTTP 代理服務(wù)器。
(3)數(shù)據(jù)格式比較輕量,性能開(kāi)銷(xiāo)小,通信高效。
(4)可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù)。
(5)沒(méi)有同源限制,客戶(hù)端可以與任意服務(wù)器通信。
(6)協(xié)議標(biāo)識(shí)符是ws(如果加密,則為wss),服務(wù)器網(wǎng)址就是 URL。
二、代碼實(shí)現(xiàn)
1、前端(html)
1.1、無(wú)前端向后端發(fā)送消息
uid實(shí)際開(kāi)發(fā)中應(yīng)該使用唯一值作為當(dāng)前對(duì)話的key
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
消息展示區(qū):<br/>
<div id="textArea"></div>
</body>
<script>
var textArea = document.getElementById('textArea');
var websocket = null;
//如果瀏覽器支持websocket就建立一個(gè)websocket,否則提示瀏覽器不支持websocket
//uid應(yīng)該要用唯一標(biāo)識(shí),為了測(cè)試方便看
if('websocket' in window){
websocketPage = new WebSocket('ws://localhost:8080/websocket/' + 99);
}else{
alert('瀏覽器不支持websocket!');
}
//建立websocket時(shí)自動(dòng)調(diào)用
websocketPage.onopen = function (event) {
console.log('建立連接');
}
//關(guān)閉webscoket時(shí)自動(dòng)調(diào)用
websocketPage.oncolse = function (event){
console.log('關(guān)閉連接');
}
//websocket接收到消息時(shí)調(diào)用
websocketPage.onmessage = function (event){
//將接收到的消息展示在消息展示區(qū) (心跳響應(yīng)回來(lái)的消息不顯示)
if (event.data !== "conn_success"){
textArea.innerText += event.data;
textArea.innerHTML += "<br/>";
}
}
//websocket出錯(cuò)自動(dòng)調(diào)用
websocketPage.onerror = function () {
alert('websocket出錯(cuò)');
}
//關(guān)閉窗口前關(guān)閉websocket連接
window.onbeforeunload = function (){
websocketPage.close();
}
</script>
</html>
1.2、有前端向后端發(fā)送消息
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Java后端WebSocket的Tomcat實(shí)現(xiàn)</title>
<script type="text/javascript" src="js/jquery.min.js"></script>
</head>
<body>
Welcome<br/><input id="text" type="text" />
<button onclick="send()">發(fā)送消息</button>
<hr/>
<button onclick="closeWebSocket()">關(guān)閉WebSocket連接</button>
<hr/>
<div id="message"></div>
</body>
<script type="text/javascript">
var websocket = null;
//判斷當(dāng)前瀏覽器是否支持WebSocket
if('WebSocket' in window) {
//改成你的地址
websocket = new WebSocket("ws://localhost:8080/websocket/100");
} else {
alert('當(dāng)前瀏覽器 Not support websocket')
}
//連接發(fā)生錯(cuò)誤的回調(diào)方法
websocket.onerror = function() {
setMessageInnerHTML("WebSocket連接發(fā)生錯(cuò)誤");
};
//連接成功建立的回調(diào)方法
websocket.onopen = function() {
setMessageInnerHTML("WebSocket連接成功");
}
var U01data, Uidata, Usdata
//接收到消息的回調(diào)方法
websocket.onmessage = function(event) {
console.log(event);
if (event.data !== "conn_success"){
setMessageInnerHTML("接收消息:"+event.data);
// setMessageInnerHTML(event);
setechart()
}
}
//連接關(guān)閉的回調(diào)方法
websocket.onclose = function() {
setMessageInnerHTML("WebSocket連接關(guān)閉");
}
// //監(jiān)聽(tīng)窗口關(guān)閉事件,當(dāng)窗口關(guān)閉時(shí),主動(dòng)去關(guān)閉websocket連接,防止連接還沒(méi)斷開(kāi)就關(guān)閉窗口,server端會(huì)拋異常。
window.onbeforeunload = function() {
closeWebSocket();
}
//將消息顯示在網(wǎng)頁(yè)上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//關(guān)閉WebSocket連接
function closeWebSocket() {
websocket.close();
}
//發(fā)送消息
function send() {
var message = document.getElementById('text').value;
websocket.send('{"msg":"' + message + '"}');
setMessageInnerHTML("--------------發(fā)送消息:"+message + "");
}
</script>
</html>
2、后端具體代碼(spring boot)
2.1、maven依賴(lài)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
– yml沒(méi)有東西,只有一個(gè)默認(rèn)端口
2.2、配置類(lèi)
需要加一個(gè) WebSocket 端點(diǎn)暴露 的bean 和定時(shí)器注解
@EnableScheduling //定時(shí)器
@SpringBootApplication
public class WebSocketApplication {
public static void main(String[] args) {
SpringApplication.run(WebSocketApplication.class, args);
}
/**
* 服務(wù)器端點(diǎn)導(dǎo)出
* @author zhengfuping
* @date 2023/8/22
* @return ServerEndpointExporter
*/
@Bean
public ServerEndpointExporter getServerEndpointExporter(){
return new ServerEndpointExporter();
}
}
2.3、Web Socket連接工具類(lèi)
@Slf4j
@Service
@ServerEndpoint("/websocket/{uid}")
public class WebSocketServer2 {
//連接建立時(shí)長(zhǎng)
private static final long sessionTimeout = 60000;
// 用來(lái)存放每個(gè)客戶(hù)端對(duì)應(yīng)的WebSocketServer對(duì)象
private static Map<String, WebSocketServer2> webSocketMap = new ConcurrentHashMap<>();
// 與某個(gè)客戶(hù)端的連接會(huì)話,需要通過(guò)它來(lái)給客戶(hù)端發(fā)送數(shù)據(jù)
private Session session;
// 接收id
private String uid;
/**
* 連接建立成功調(diào)用的方法
* @author zhengfuping
* @date 2023/8/22
* @param session
* @param uid
*/
@OnOpen
public void onOpen(Session session , @PathParam("uid") String uid){
session.setMaxIdleTimeout(sessionTimeout);
this.session = session;
this.uid = uid;
if (webSocketMap.containsKey(uid)){
webSocketMap.remove(uid);
}
webSocketMap.put(uid,this);
log.info("websocket連接成功編號(hào)uid: " + uid + ",當(dāng)前在線數(shù): " + getOnlineClients());
try{
// 響應(yīng)客戶(hù)端實(shí)際業(yè)務(wù)數(shù)據(jù)!
sendMessage("conn_success");
}catch (Exception e){
log.error("websocket發(fā)送連接成功錯(cuò)誤編號(hào)uid: " + uid + ",網(wǎng)絡(luò)異常!!!");
}
}
/**
* 連接關(guān)閉調(diào)用的方法
* @author zhengfuping
* @date 2023/8/22
*/
@OnClose
public void onClose(){
try {
if (webSocketMap.containsKey(uid)){
webSocketMap.remove(uid);
}
log.info("websocket退出編號(hào)uid: " + uid + ",當(dāng)前在線數(shù)為: " + getOnlineClients());
} catch (Exception e) {
log.error("websocket編號(hào)uid連接關(guān)閉錯(cuò)誤: " + uid + ",原因: " + e.getMessage());
}
}
/**
* 收到客戶(hù)端消息后調(diào)用的方法
* @param message 客戶(hù)端發(fā)送過(guò)來(lái)的消息
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
try {
WebSocketServer2.sendInfo(message);
log.info("websocket收到客戶(hù)端編號(hào)uid消息: " + uid + ", 報(bào)文: " + message);
} catch (Exception e) {
log.error("websocket發(fā)送消息失敗編號(hào)uid為: " + uid + ",報(bào)文: " + message);
}
}
/**
* 發(fā)生錯(cuò)誤時(shí)調(diào)用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("websocket編號(hào)uid錯(cuò)誤: " + this.uid + "原因: " + error.getMessage());
error.printStackTrace();
}
/**
* 實(shí)現(xiàn)服務(wù)器主動(dòng)推送
* @author yingfeng
* @date 2023/8/22 10:11
* @Param * @param null
* @return
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 獲取客戶(hù)端在線數(shù)
* @author zhengfuping
* @date 2023/8/22 10:11
* @param
*/
public static synchronized int getOnlineClients() {
if (Objects.isNull(webSocketMap)) {
return 0;
} else {
return webSocketMap.size();
}
}
/**
* 單機(jī)使用,外部接口通過(guò)指定的客戶(hù)id向該客戶(hù)推送消息
* @param key
* @param message
* @return boolean
*/
public static boolean sendMessageByWayBillId(@NotNull String key, String message) {
WebSocketServer2 webSocketServer = webSocketMap.get(key);
if (Objects.nonNull(webSocketServer)) {
try {
webSocketServer.sendMessage(message);
log.info("websocket發(fā)送消息編號(hào)uid為: " + key + "發(fā)送消息: " + message);
return true;
} catch (Exception e) {
log.error("websocket發(fā)送消息失敗編號(hào)uid為: " + key + "消息: " + message);
return false;
}
} else {
log.error("websocket未連接編號(hào)uid號(hào)為: " + key + "消息: " + message);
return false;
}
}
/**
* 群發(fā)自定義消息
* @author zhengfuping
* @date 2023/8/22 9:52
* @param message
*/
public static void sendInfo(String message) {
webSocketMap.forEach((k, v) -> {
WebSocketServer2 webSocketServer = webSocketMap.get(k);
try {
webSocketServer.sendMessage(message);
log.info("websocket群發(fā)消息編號(hào)uid為: " + k + ",消息: " + message);
} catch (IOException e) {
log.error("群發(fā)自定義消息失敗: " + k + ",message: " + message);
}
});
}
/**
* 服務(wù)端群發(fā)消息-心跳包
* @author zhengfuping
* @date 2023/8/22 10:09
* @param message 推送數(shù)據(jù)
* @return int 連接數(shù)
*/
public static synchronized int sendPing(String message){
if (webSocketMap.size() == 0)
return 0;
StringBuffer uids = new StringBuffer();
AtomicInteger count = new AtomicInteger();
webSocketMap.forEach((uid,server)->{
count.getAndIncrement();
if (webSocketMap.containsKey(uid)){
WebSocketServer2 webSocketServer = webSocketMap.get(uid);
try {
if (Integer.valueOf(uid) ==101){
Integer i=1/0;
}
webSocketServer.sendMessage(message);
if (count.equals(webSocketMap.size() - 1)){
uids.append("uid");
return;
}
uids.append(uid).append(",");
} catch (Exception e) {
webSocketMap.remove(uid);
log.info("客戶(hù)端心跳檢測(cè)異常移除: " + uid + ",心跳發(fā)送失敗,已移除!");
}
}else {
log.info("客戶(hù)端心跳檢測(cè)異常不存在: " + uid + ",不存在!");
}
});
log.info("客戶(hù)端心跳檢測(cè)結(jié)果: " + uids + "連接正在運(yùn)行");
return webSocketMap.size();
}
/**
* 連接是否存在
* @param uid
* @return boolean
*/
public static boolean isConnected(String uid) {
if (Objects.nonNull(webSocketMap) && webSocketMap.containsKey(uid)) {
return true;
} else {
return false;
}
}
}
2.4、Controller用于測(cè)試主動(dòng)發(fā)送消息
@RestController
@RequestMapping("/test")
public class WebSocketController{
/**
* 檢驗(yàn)連接
* @date 2023/8/22
* @Param * @param webSocketId
* @return * @return String
*/
@GetMapping("/webSocketIsConnect/{webSocketId}")
public String webSocketIsConnect(@PathVariable("webSocketId") String webSocketId){
if (WebSocketServer2.isConnected(webSocketId)) {
return webSocketId+"正在連接";
}
return webSocketId+"連接斷開(kāi)!";
}
/**
* 單發(fā) 消息
* @author zhengfuping
* @date 2023/8/22 10:25
* @param webSocketId 指定 連接
* @param message 數(shù)據(jù)
* @param pwd 驗(yàn)證密碼
* @return String
*/
@GetMapping("/sendMessageByWayBillId")
public String sendMessageByWayBillId(String webSocketId, String message, String pwd) {
boolean flag = false;
flag = WebSocketServer2.sendMessageByWayBillId(webSocketId, message);
if (flag) {
return "發(fā)送成功!";
}
return "發(fā)送失?。?;
}
/**
* 群發(fā)
* @author zhengfuping
* @date 2023/8/22 10:26
* @param message
* @param pwd
*/
@GetMapping("/broadSendInfo")
public void sendInfo(String message, String pwd) {
WebSocketServer2.sendInfo(message);
}
}
2.5、配置定時(shí)任務(wù),用于調(diào)用主動(dòng)向客戶(hù)端發(fā)送心跳
每10秒調(diào)用一次,主動(dòng)檢測(cè),查看客戶(hù)端連接是否異常斷開(kāi),如果異常斷開(kāi),則把該會(huì)話從集合中剔除掉,避免無(wú)限積壓。
@Component
@Slf4j
public class WebSocketTask {
@Scheduled(cron = "0/10 * * * * ?")
public void clearOrders(){
int num = 0;
try {
num = WebSocketServer2.sendPing("conn_success");
} finally {
log.info("websocket心跳檢測(cè)結(jié)果,共【" + num + "】個(gè)連接");
}
}
}
三、測(cè)試
1、 測(cè)試消息發(fā)送
1.1、前端日志
1.2、后端日志
2、測(cè)試客戶(hù)端異常斷開(kāi),服務(wù)器通過(guò)心跳檢測(cè)自動(dòng)剔除掉異常對(duì)話。
因?yàn)闇y(cè)試不方便,只能通過(guò)斷點(diǎn)實(shí)現(xiàn)效果
-
前端需要把主動(dòng)關(guān)閉會(huì)話的注釋掉,不讓主動(dòng)關(guān)閉
-
先在連接關(guān)閉的地方和心跳檢測(cè)地方打上斷點(diǎn),斷點(diǎn)需要設(shè)置成Thread,要不然沒(méi)法異步
-
然后關(guān)閉掉一個(gè)前端頁(yè)面讓他把會(huì)話關(guān)閉,就會(huì)進(jìn)入該斷點(diǎn)位置,通過(guò)斷點(diǎn)讓它停住不讓他去正常關(guān)閉文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-666179.html
4. 然后選擇執(zhí)行該心跳檢測(cè)的斷點(diǎn)代碼
5. 進(jìn)入心跳的循環(huán)給每個(gè)會(huì)話發(fā)送心跳檢測(cè),此時(shí)前端已經(jīng)異常斷開(kāi)了
6. 因?yàn)榍岸艘呀?jīng)關(guān)閉會(huì)話了,則發(fā)送心跳會(huì)失敗,會(huì)直接進(jìn)入catch塊,然后把該會(huì)話從集合中剔除掉
最終日志文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-666179.html
到了這里,關(guān)于前端加springboot實(shí)現(xiàn)Web Socket連接通訊以及測(cè)試流程(包括后端實(shí)現(xiàn)心跳檢測(cè))的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!