一、WebSocket概述
1.1 什么是WebSocket
WebSocket是一種在單個(gè)TCP連接上進(jìn)行全雙工通信的網(wǎng)絡(luò)協(xié)議。它是為了在Web瀏覽器和Web服務(wù)器之間提供實(shí)時(shí)、雙向的通信而設(shè)計(jì)的。傳統(tǒng)的HTTP協(xié)議是一種單向通信協(xié)議,客戶端發(fā)送請(qǐng)求,服務(wù)器響應(yīng),然后連接就關(guān)閉了。而WebSocket允許在客戶端和服務(wù)器之間建立持久連接,使得雙方可以通過該連接隨時(shí)發(fā)送數(shù)據(jù)。
WebSocket協(xié)議通過在HTTP握手階段使用Upgrade頭來升級(jí)連接,使其成為全雙工通信通道。一旦升級(jí)完成,WebSocket連接就保持打開狀態(tài),允許雙方在任何時(shí)候發(fā)送數(shù)據(jù)。
WebSocket協(xié)議的特點(diǎn)包括:
- 全雙工通信: 客戶端和服務(wù)器之間可以同時(shí)發(fā)送和接收數(shù)據(jù),而不需要等待響應(yīng)。
- 低延遲: 由于連接保持打開狀態(tài),可以更快地傳輸數(shù)據(jù),適用于實(shí)時(shí)性要求較高的應(yīng)用,如在線游戲、聊天應(yīng)用等。
- 跨域支持: 與AJAX請(qǐng)求不同,WebSocket允許跨域通信,提供更大的靈活性。
WebSocket 端點(diǎn)通常會(huì)觸發(fā)一些生命周期事件,這些事件可以用于處理數(shù)據(jù)、管理連接的狀態(tài)等。
1.2 WebSocket的生命周期事件
- onOpen 事件: 在端點(diǎn)建立新WebSocket連接時(shí)并且在任何其他事件發(fā)生之前,將觸發(fā)onOpen 事件。在這個(gè)事件中,可以執(zhí)行一些初始化操作,例如記錄連接信息、添加到連接池等。
- onMessage 事件: 當(dāng)端點(diǎn)接收到客戶端發(fā)送的消息時(shí),將觸發(fā)onMessage 事件。在這個(gè)事件中,可以處理接收到的消息并根據(jù)需要做出相應(yīng)的反應(yīng)。
- onError 事件: 當(dāng)在 WebSocket 連接期間發(fā)生錯(cuò)誤時(shí),將觸發(fā)onError事件。在這個(gè)事件中可以處理錯(cuò)誤情況。
- onClose 事件: 當(dāng)連接關(guān)閉時(shí),將觸發(fā)onClose事件。在這個(gè)事件中可以執(zhí)行一些清理工作,例如從連接池中移除連接、記錄連接關(guān)閉信息等。
二、WebSocket實(shí)現(xiàn)群聊功能
2.1 服務(wù)端:注解式端點(diǎn)事件處理
在服務(wù)端使用@ServerEndpoint注解將 Java 類聲明成 WebSocket 服務(wù)端端點(diǎn)。
@ServerEndpoint(value = "/chat", configurator= GetHttpSessionConfigurator.class)
@Component // SpringBoot 的組件注解
public class ChatServer
對(duì)于注解式服務(wù)端端點(diǎn),WebSocket API中的生命周期事件要求使用以下方法級(jí)注解:@OnOpen @OnMessage @OnError @OnClose。
@ServerEndpoint(value = "/chat", configurator= GetHttpSessionConfigurator.class)
@Component
public class ChatServer {
// WebSocker 生命周期函數(shù): 連接建立時(shí)調(diào)用
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
// 從 EndpointConfig 中獲取之前從握手時(shí)獲取的 httpSession
this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
String userName = (String) this.httpSession.getAttribute("username");
// 保存已登錄用戶 session
clients.put(userName, session);
log.info(userName + "已與服務(wù)器建立連接");
}
// WebSocker 生命周期函數(shù): 收到消息時(shí)調(diào)用
@OnMessage
public void onMessage(String message) {
log.info("服務(wù)器收到消息: " + message);
// 服務(wù)器群發(fā)消息
groupSend(message);
}
// WebSocker 生命周期函數(shù): 連接斷開時(shí)調(diào)用
@OnClose
public void onClose() {
String userName = (String) this.httpSession.getAttribute("username");
clients.remove(userName);
log.info(userName + "已與服務(wù)器斷開連接");
}
......
}
其中,服務(wù)端在收到消息時(shí),在OnMessage事件中向客戶端群發(fā)消息
// WebSocker 生命周期函數(shù): 收到消息時(shí)調(diào)用
@OnMessage
public void onMessage(String message) {
log.info("服務(wù)器收到消息: " + message);
// 服務(wù)器群發(fā)消息
groupSend(message);
}
private void groupSend(String message) {
// 遍歷所有連接,向客戶端群發(fā)消息
message = this.httpSession.getAttribute("username") + ": " + message;
Set<Map.Entry<String, Session>> entries = clients.entrySet();
for (Map.Entry<String, Session> client : entries) {
Session session = client.getValue();
try {
session.getBasicRemote().sendText(message); // 發(fā)送消息
} catch (IOException e) {
}
}
}
2.2 客戶端:JavaScript中的WebSocket對(duì)象
在客戶端使用JavaScript的WebSocket對(duì)象作為客戶端端點(diǎn)
// new WebSocket("ws://url")
ws = new WebSocket("ws://localhost:8848/chat");
對(duì)于JavaScript的WebSocket對(duì)象,其生命周期事件為onopen,onmessage,onerror和onclose。
ws.onmessage = function (msg) {
let message = msg.data; // 獲取服務(wù)端發(fā)送的信息
// 將消息顯示到頁(yè)面中
let li = document.createElement('li');
li.textContent = message;
let messages = document.getElementById('messages');
messages.appendChild(li);
window.scrollTo(0, document.body.scrollHeight);
}
在客戶端使用WebSocket對(duì)象的send(message)方法向服務(wù)端發(fā)送消息
三、Session、Cookie實(shí)現(xiàn)24小時(shí)內(nèi)自動(dòng)識(shí)別用戶
在服務(wù)端登錄驗(yàn)證的handler中,創(chuàng)建攜帶驗(yàn)證信息的Cookie(這里圖方便直接攜帶了用戶名,實(shí)際應(yīng)該使用根據(jù)用戶UID加密過的token)并返回給客戶端瀏覽器,設(shè)定有效期為24h。
同時(shí),在會(huì)話域Session 中保存用戶名以便 WebSocket 獲?。⊿ervlet在新建Session時(shí)也會(huì)返回給客戶端帶有JSESSIONID的Cookie):
@RestController
public class UserController {
@PostMapping("/login")
public String login(@RequestBody UserEntity user, HttpServletRequest request, HttpServletResponse response) {
if(loginCheck(user)) {
Cookie cookie = new Cookie("username", user.getUserName());
cookie.setMaxAge(24 * 60 * 60); // 設(shè)置 Cookie 的有效時(shí)間為 24h
response.addCookie(cookie);
// 在 session 中設(shè)置 userName 以便 WebSocket 獲取
request.getSession().setAttribute("username", user.getUserName());
return "success";
} else {
return "failed";
}
}
......
}
此后24h內(nèi),客戶端瀏覽器訪問服務(wù)端時(shí)會(huì)攜帶以上Cookie,即使會(huì)話連接斷開,服務(wù)端也可以根據(jù)該Cookie直接驗(yàn)證用戶信息并重新在Session中保存,實(shí)現(xiàn)24h內(nèi)用戶免登錄。若會(huì)話未斷開,直接從Session中即可獲取用戶信息。
// 攔截器:登錄校驗(yàn), 不通過則跳轉(zhuǎn)到登錄界面
@Component
public class LoginProtectInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 先使用 session 進(jìn)行登錄校驗(yàn)
String username = (String) request.getSession().getAttribute("username");
if(username != null){
return true; // 放行
}
// session 校驗(yàn)失敗則用 cookie校驗(yàn)
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies){
if("username".equals(cookie.getName())){
// 根據(jù) cookie獲取 userName
String userName = cookie.getValue();
// 在 session 中設(shè)置 userName
request.getSession().setAttribute("username", userName);
return true; // 放行
}
}
// 校驗(yàn)失敗 跳轉(zhuǎn)到登錄界面
response.sendRedirect("/login.html");
return false;
}
}
上面是在攔截器中進(jìn)行登陸驗(yàn)證,需要對(duì)攔截器進(jìn)行配置。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginProtectInterceptor loginProtectInterceptor;
// 登錄驗(yàn)證攔截器配置
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 訪問聊天室前需要登錄驗(yàn)證
registry.addInterceptor(loginProtectInterceptor).addPathPatterns("/chat.html");
}
}
四、實(shí)驗(yàn)中遇到的一些問題及其解決
4.1 WebSocket獲取httpSession的方法
實(shí)驗(yàn)中需獲取Session中的用戶信息,由于WebSocket與Http協(xié)議的不同,故需要在WebSocket中故在獲取HttpSession,這里參考了以下鏈接中的方法。
https://blog.csdn.net/Zany540817349/article/details/90210075
在@ServerEndpoint注解的源代碼中,可以看到要求一個(gè)ServerEndpointConfig接口下的Configurator子類,該類中有個(gè)modifyHandshake方法,這個(gè)方法可以修改在握手時(shí)的操作,將httpSession加進(jìn)webSocket的配置中。
因此繼承這個(gè)ServerEndpointConfig.Configurator子類,重寫其modifyHandshake方法:
/** 繼承 ServerEndpointConfig.Configurator 類
* 重寫其中的 modifyHandshake 方法
* 在建立連接時(shí)將當(dāng)前會(huì)話的 httpSession 加入到 webSocket 的 Server端的配置中
*/
@Configuration
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec,
HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession=(HttpSession) request.getHttpSession();
sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}
將繼承類加入到@ServerEndpoint注解的configurator屬性中,這樣即可在EndpointConfig中獲取HttpSession:
@Component
@ServerEndpoint(value = "/chat", configurator= GetHttpSessionConfigurator.class)
public class ChatServer {
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
// 從 EndpointConfig 中獲取之前從握手時(shí)獲取的 httpSession
this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
String userName = (String) this.httpSession.getAttribute("username");
......
4.2 WebSocket獲取httpSession為空(Session不一致)的問題
4.1中的獲取httpSession的方法是正確的,但是在modifyHandshake中g(shù)etHttpSession()時(shí)會(huì)報(bào)空指針異常,這里參考了以下鏈接,發(fā)現(xiàn)是服務(wù)端url寫錯(cuò)。
https://blog.csdn.net/csu_passer/article/details/78536060
在前端連接WebSocket的時(shí)候,我的代碼是這樣的:
new WebSocket("ws://127.0.0.1:8848/chat");
但是瀏覽器的地址欄是
http://localhost:8848/chat.html
鏈接中解釋說如果不使用同一個(gè)host,則會(huì)創(chuàng)建不同的連接請(qǐng)求,將WebSocket中服務(wù)端地址修改為與瀏覽器地址欄一致,則可以正確獲取到httpSession。
new WebSocket("ws://localhost:8848/chat");
實(shí)驗(yàn)源代碼
https://gitee.com/amadeuswyk/ustc-courses-net-web-socket/tree/master/文章來源:http://www.zghlxwxcb.cn/news/detail-774095.html
參考資料
https://blog.csdn.net/Zany540817349/article/details/90210075
https://blog.csdn.net/csu_passer/article/details/78536060文章來源地址http://www.zghlxwxcb.cn/news/detail-774095.html
到了這里,關(guān)于使用WebSocket方式能將群聊信息實(shí)時(shí)群發(fā)給所有在線用戶的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!