一、WebSocket概述
1、WebSocket簡介
WebSocket協(xié)議是基于TCP的一種新的網(wǎng)絡(luò)協(xié)議。它實現(xiàn)了瀏覽器與服務(wù)器全雙工(full-duplex)通信——允許服務(wù)器主動發(fā)送信息給客戶端。
2、為什么需要WebSocket
HTTP 是基于請求響應(yīng)式的,即通信只能由客戶端發(fā)起,服務(wù)端做出響應(yīng),無狀態(tài),無連接。
無狀態(tài):每次連接只處理一個請求,請求結(jié)束后斷開連接。
無連接:對于事務(wù)處理沒有記憶能力,服務(wù)器不知道客戶端是什么狀態(tài)。
通過HTTP實現(xiàn)即時通訊,只能是頁面輪詢向服務(wù)器發(fā)出請求,服務(wù)器返回查詢結(jié)果。輪詢的效率低,非常浪費(fèi)資源,因為必須不停連接,或者 HTTP 連接始終打開。
WebSocket的最大特點就是,服務(wù)器可以主動向客戶端推送信息,客戶端也可以主動向服務(wù)器發(fā)送信息,是真正的雙向平等對話。
WebSocket特點:
(1)建立在 TCP 協(xié)議之上,服務(wù)器端的實現(xiàn)比較容易。
(2)與 HTTP 協(xié)議有著良好的兼容性。默認(rèn)端口也是80和443,并且握手階段采用 HTTP 協(xié)議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務(wù)器。
(3)數(shù)據(jù)格式比較輕量,性能開銷小,通信高效。
(4)可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù)。
(5)沒有同源限制,客戶端可以與任意服務(wù)器通信。
(6)協(xié)議標(biāo)識符是ws(如果加密,則為wss),服務(wù)器網(wǎng)址就是 URL。
二、SpringBoot整合WebSocket
創(chuàng)建 SpringBoot項目,引入 WebSocket依賴,前端這里比較簡陋。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.7.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.7.12</version>
</dependency>
application.yml:
server:
port: 8081
spring:
thymeleaf:
mode: HTML
cache: true
prefix: classpath:/templates/
encoding: UTF-8
suffix: .html
check-template-location: true
template-resolver-order: 1
1、WebSocketConfig
啟用 WebSocket的支持也是很簡單。
/**
* WebSocket配置類。開啟WebSocket的支持
*/
@Configuration
public class WebSocketConfig {
/**
* bean注冊:會自動掃描帶有@ServerEndpoint注解聲明的Websocket Endpoint(端點),注冊成為Websocket bean。
* 要注意,如果項目使用外置的servlet容器,而不是直接使用springboot內(nèi)置容器的話,就不要注入ServerEndpointExporter,因為它將由容器自己提供和管理。
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2、WebSocketServer
這里就是重點了,核心都在這里。
因為WebSocket是類似客戶端服務(wù)端的形式(采用ws協(xié)議),那么這里的WebSocketServer其實就相當(dāng)于一個ws協(xié)議的Controller
直接@ServerEndpoint("/imserver/{userId}") 、@Component啟用即可,然后在里面實現(xiàn)@OnOpen開啟連接,@onClose關(guān)閉連接,@onMessage接收消息等方法。
新建一個ConcurrentHashMap用于接收當(dāng)前userId的WebSocket或者Session信息,方便IM之間對userId進(jìn)行推送消息。單機(jī)版實現(xiàn)到這里就可以。
集群版(多個ws節(jié)點)還需要借助 MySQL或者 Redis等進(jìn)行訂閱廣播方式處理,改造對應(yīng)的 sendMessage方法即可。
/**
* WebSocket的操作類
*/
@Component
@Slf4j
/**
* html頁面與之關(guān)聯(lián)的接口
* var reqUrl = "http://localhost:8081/websocket/" + cid;
* socket = new WebSocket(reqUrl.replace("http", "ws"));
*/
@ServerEndpoint("/websocket/{sid}")
public class WebSocketServer {
/**
* 靜態(tài)變量,用來記錄當(dāng)前在線連接數(shù),線程安全的類。
*/
private static AtomicInteger onlineSessionClientCount = new AtomicInteger(0);
/**
* 存放所有在線的客戶端
*/
private static Map<String, Session> onlineSessionClientMap = new ConcurrentHashMap<>();
/**
* 連接sid和連接會話
*/
private String sid;
private Session session;
/**
* 連接建立成功調(diào)用的方法。由前端<code>new WebSocket</code>觸發(fā)
*
* @param sid 每次頁面建立連接時傳入到服務(wù)端的id,比如用戶id等??梢宰远x。
* @param session 與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送消息
*/
@OnOpen
public void onOpen(@PathParam("sid") String sid, Session session) {
/**
* session.getId():當(dāng)前session會話會自動生成一個id,從0開始累加的。
*/
log.info("連接建立中 ==> session_id = {}, sid = {}", session.getId(), sid);
//加入 Map中。將頁面的sid和session綁定或者session.getId()與session
//onlineSessionIdClientMap.put(session.getId(), session);
onlineSessionClientMap.put(sid, session);
//在線數(shù)加1
onlineSessionClientCount.incrementAndGet();
this.sid = sid;
this.session = session;
sendToOne(sid, "連接成功");
log.info("連接建立成功,當(dāng)前在線數(shù)為:{} ==> 開始監(jiān)聽新連接:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), sid);
}
/**
* 連接關(guān)閉調(diào)用的方法。由前端<code>socket.close()</code>觸發(fā)
*
* @param sid
* @param session
*/
@OnClose
public void onClose(@PathParam("sid") String sid, Session session) {
//onlineSessionIdClientMap.remove(session.getId());
// 從 Map中移除
onlineSessionClientMap.remove(sid);
//在線數(shù)減1
onlineSessionClientCount.decrementAndGet();
log.info("連接關(guān)閉成功,當(dāng)前在線數(shù)為:{} ==> 關(guān)閉該連接信息:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), sid);
}
/**
* 收到客戶端消息后調(diào)用的方法。由前端<code>socket.send</code>觸發(fā)
* * 當(dāng)服務(wù)端執(zhí)行toSession.getAsyncRemote().sendText(xxx)后,前端的socket.onmessage得到監(jiān)聽。
*
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
/**
* html界面?zhèn)鬟f來得數(shù)據(jù)格式,可以自定義.
* {"sid":"user-1","message":"hello websocket"}
*/
JSONObject jsonObject = JSON.parseObject(message);
String toSid = jsonObject.getString("sid");
String msg = jsonObject.getString("message");
log.info("服務(wù)端收到客戶端消息 ==> fromSid = {}, toSid = {}, message = {}", sid, toSid, message);
/**
* 模擬約定:如果未指定sid信息,則群發(fā),否則就單獨(dú)發(fā)送
*/
if (toSid == null || toSid == "" || "".equalsIgnoreCase(toSid)) {
sendToAll(msg);
} else {
sendToOne(toSid, msg);
}
}
/**
* 發(fā)生錯誤調(diào)用的方法
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("WebSocket發(fā)生錯誤,錯誤信息為:" + error.getMessage());
error.printStackTrace();
}
/**
* 群發(fā)消息
*
* @param message 消息
*/
private void sendToAll(String message) {
// 遍歷在線map集合
onlineSessionClientMap.forEach((onlineSid, toSession) -> {
// 排除掉自己
if (!sid.equalsIgnoreCase(onlineSid)) {
log.info("服務(wù)端給客戶端群發(fā)消息 ==> sid = {}, toSid = {}, message = {}", sid, onlineSid, message);
toSession.getAsyncRemote().sendText(message);
}
});
}
/**
* 指定發(fā)送消息
*
* @param toSid
* @param message
*/
private void sendToOne(String toSid, String message) {
// 通過sid查詢map中是否存在
Session toSession = onlineSessionClientMap.get(toSid);
if (toSession == null) {
log.error("服務(wù)端給客戶端發(fā)送消息 ==> toSid = {} 不存在, message = {}", toSid, message);
return;
}
// 異步發(fā)送
log.info("服務(wù)端給客戶端發(fā)送消息 ==> toSid = {}, message = {}", toSid, message);
toSession.getAsyncRemote().sendText(message);
/*
// 同步發(fā)送
try {
toSession.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("發(fā)送消息失敗,WebSocket IO異常");
e.printStackTrace();
}*/
}
}
3、controller
controller中只有一個簡單的界面跳轉(zhuǎn)操作,其他的不需要。
@Controller
@RequestMapping("/demo")
public class DemoController {
/**
* 跳轉(zhuǎn)到websocketDemo.html頁面,攜帶自定義的cid信息。
* http://localhost:8081/demo/toWebSocketDemo/user-1
*
* @param cid
* @param model
* @return
*/
@GetMapping("/toWebSocketDemo/{cid}")
public String toWebSocketDemo(@PathVariable String cid, Model model) {
model.addAttribute("cid", cid);
return "websocketDemo";
}
}
4、websocketDemo.html
新建一個文件,放到 templates目錄下面。頁面簡單使用js代碼調(diào)用WebSocket。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>666666</title>
</head>
<body>
傳遞來的數(shù)據(jù)值cid:
<input type="text" th:value="${cid}" id="cid"/>
<p>【toUserId】:
<div><input id="toUserId" name="toUserId" type="text" value="user-1"></div>
<p>【toUserId】:
<div><input id="contentText" name="contentText" type="text" value="hello websocket"></div>
<p>【操作】:
<div>
<button type="button" onclick="sendMessage()">發(fā)送消息</button>
</div>
</body>
<script type="text/javascript">
var socket;
if (typeof (WebSocket) == "undefined") {
console.log("您的瀏覽器不支持WebSocket");
} else {
console.log("您的瀏覽器支持WebSocket");
//實現(xiàn)化WebSocket對象,指定要連接的服務(wù)器地址與端口 建立連接
var cid = document.getElementById("cid").value;
console.log("cid-->" + cid);
var reqUrl = "http://localhost:8081/websocket/" + cid;
socket = new WebSocket(reqUrl.replace("http", "ws"));
//打開事件
socket.onopen = function () {
console.log("Socket 已打開");
//socket.send("這是來自客戶端的消息" + location.href + new Date());
};
//獲得消息事件
socket.onmessage = function (msg) {
console.log("onmessage--" + msg.data);
//發(fā)現(xiàn)消息進(jìn)入 開始處理前端觸發(fā)邏輯
};
//關(guān)閉事件
socket.onclose = function () {
console.log("Socket已關(guān)閉");
};
//發(fā)生了錯誤事件
socket.onerror = function () {
alert("Socket發(fā)生了錯誤");
//此時可以嘗試刷新頁面
}
//離開頁面時,關(guān)閉socket
//jquery1.8中已經(jīng)被廢棄,3.0中已經(jīng)移除
// $(window).unload(function(){
// socket.close();
//});
}
function sendMessage() {
if (typeof (WebSocket) == "undefined") {
console.log("您的瀏覽器不支持WebSocket");
} else {
// console.log("您的瀏覽器支持WebSocket");
var toUserId = document.getElementById('toUserId').value;
var contentText = document.getElementById('contentText').value;
var msg = '{"sid":"' + toUserId + '","message":"' + contentText + '"}';
console.log(msg);
socket.send(msg);
}
}
</script>
</html>
5、測試運(yùn)行效果
(1)訪問頁面,建立連接
啟動項目,訪問 http://localhost:8081/demo/toWebSocketDemo/{cid} 跳轉(zhuǎn)到頁面,然后就可以和WebSocket交互了。
這里開啟三個瀏覽器的窗口:
http://localhost:8081/demo/toWebSocketDemo/user-1
此時瀏覽器的console顯示如下:
此時瀏覽器的network 顯示如下:
服務(wù)端打印如下圖所示內(nèi)容:
http://localhost:8081/demo/toWebSocketDemo/user-2
此時瀏覽器的network 顯示如下:
此時瀏覽器的network 顯示如下:
服務(wù)端打印如下圖所示內(nèi)容:
http://localhost:8081/demo/toWebSocketDemo/user-3
此時瀏覽器的network 顯示如下:
此時瀏覽器的network 顯示如下:
服務(wù)端打印如下圖所示內(nèi)容:
(2)、在user-2給user-1發(fā)消息
此時user-2的瀏覽器network里面并沒有再請求接口
?
此時查看服務(wù)端的console,截圖如下:
?查看user-1的console,如下所示:
user-1的network并沒有再請求接口。
(3)、給全員發(fā)消息
在user-3頁面,給所有用戶發(fā)消息
user-2頁面console如下
user-1頁面的console如下
?此時服務(wù)端的console打印如下:文章來源:http://www.zghlxwxcb.cn/news/detail-812133.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-812133.html
到了這里,關(guān)于spring boot學(xué)習(xí)第六篇:SpringBoot 集成WebSocket詳解的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!