更新時(shí)間:2022-11-17 來(lái)源:黑馬程序員 瀏覽量:
一、文章導(dǎo)讀
服務(wù)器推送你還在使用輪詢嗎?本文將帶你領(lǐng)略WebSocket的魅力,輕松實(shí)現(xiàn)服務(wù)器推送功能。本文將以下面兩方面讓你理解WebSocket并應(yīng)用到具體的開發(fā)中。
WebSocket概述
使用WebSocket實(shí)現(xiàn)網(wǎng)頁(yè)聊天室
二、WebSocket
2.WebSocket介紹
WebSocket 是一種網(wǎng)絡(luò)通信協(xié)議。RFC6455 定義了它的通信標(biāo)準(zhǔn)。
WebSocket 是 HTML5 開始提供的一種在單個(gè) TCP 連接上進(jìn)行全雙工通訊的協(xié)議。
HTTP 協(xié)議是一種無(wú)狀態(tài)的、無(wú)連接的、單向的應(yīng)用層協(xié)議。它采用了請(qǐng)求/響應(yīng)模型。通信請(qǐng)求只能由客戶端發(fā)起,服務(wù)端對(duì)請(qǐng)求做出應(yīng)答處理。
這種通信模型有一個(gè)弊端:HTTP 協(xié)議無(wú)法實(shí)現(xiàn)服務(wù)器主動(dòng)向客戶端發(fā)起消息。
這種單向請(qǐng)求的特點(diǎn),注定了如果服務(wù)器有連續(xù)的狀態(tài)變化,客戶端要獲知就非常麻煩。大多數(shù) Web 應(yīng)用程序?qū)⑼ㄟ^頻繁的異步 AJAX 請(qǐng)求實(shí)現(xiàn)長(zhǎng)輪詢。輪詢的效率低,非常浪費(fèi)資源(因?yàn)楸仨毑煌_B接,或者 HTTP 連接始終打開)。
http協(xié)議:
websocket協(xié)議:
2. websocket協(xié)議
本協(xié)議有兩部分:握手和數(shù)據(jù)傳輸。
握手是基于http協(xié)議的。
來(lái)自客戶端的握手看起來(lái)像如下形式:
GET ws://localhost/chat HTTP/1.1 Host: localhost Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Extensions: permessage-deflate Sec-WebSocket-Version: 13
來(lái)自服務(wù)器的握手看起來(lái)像如下形式:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Extensions: permessage-deflate
字段說明:
| 頭名稱 | 說明 | | :------------------------ | ------------------------------------------------------------ | | Connection:Upgrade | 標(biāo)識(shí)該HTTP請(qǐng)求是一個(gè)協(xié)議升級(jí)請(qǐng)求 | | Upgrade: WebSocket | 協(xié)議升級(jí)為WebSocket協(xié)議 | | Sec-WebSocket-Version: 13 | 客戶端支持WebSocket的版本 | | Sec-WebSocket-Key: | 客戶端采用base64編碼的24位隨機(jī)字符序列,服務(wù)器接受客戶端HTTP協(xié)議升級(jí)的證明。要求服務(wù)端響應(yīng)一個(gè)對(duì)應(yīng)加密的Sec-WebSocket-Accept頭信息作為應(yīng)答 | | Sec-WebSocket-Extensions | 協(xié)議擴(kuò)展類型 |
3. 客戶端(瀏覽器)實(shí)現(xiàn)
3.1 websocket對(duì)象
實(shí)現(xiàn) WebSockets 的 Web 瀏覽器將通過 WebSocket 對(duì)象公開所有必需的客戶端功能(主要指支持 Html5 的瀏覽器)。
以下 API 用于創(chuàng)建 WebSocket 對(duì)象:
var ws = new WebSocket(url);
> 參數(shù)url格式說明: ws://ip地址:端口號(hào)/資源名稱
3.2 websocket事件
WebSocket 對(duì)象的相關(guān)事件
| 事件 | 事件處理程序 | 描述 |
| ------- | ----------------------- | -------------------------- |
| open | websocket對(duì)象.onopen | 連接建立時(shí)觸發(fā) |
| message | websocket對(duì)象.onmessage | 客戶端接收服務(wù)端數(shù)據(jù)時(shí)觸發(fā) |
| error | websocket對(duì)象.onerror | 通信發(fā)生錯(cuò)誤時(shí)觸發(fā) |
| close | websocket對(duì)象.onclose | 連接關(guān)閉時(shí)觸發(fā) |
3.3 WebSocket方法
WebSocket 對(duì)象的相關(guān)方法:
| 方法 | 描述 |
| ------ | ---------------- |
| send() | 使用連接發(fā)送數(shù)據(jù) |
4. 服務(wù)端實(shí)現(xiàn)
Tomcat的7.0.5 版本開始支持WebSocket,并且實(shí)現(xiàn)了Java WebSocket規(guī)范(JSR356)。
Java WebSocket應(yīng)用由一系列的WebSocketEndpoint組成。Endpoint 是一個(gè)java對(duì)象,代表WebSocket鏈接的一端,對(duì)于服務(wù)端,我們可以視為處理具體WebSocket消息的接口, 就像Servlet之與http請(qǐng)求一樣。
我們可以通過兩種方式定義Endpoint:
第一種是編程式, 即繼承類 javax.websocket.Endpoint并實(shí)現(xiàn)其方法。
第二種是注解式, 即定義一個(gè)POJO, 并添加 @ServerEndpoint相關(guān)注解。
Endpoint實(shí)例在WebSocket握手時(shí)創(chuàng)建,并在客戶端與服務(wù)端鏈接過程中有效,最后在鏈接關(guān)閉時(shí)結(jié)束。在Endpoint接口中明確定義了與其生命周期相關(guān)的方法, 規(guī)范實(shí)現(xiàn)者確保生命周期的各個(gè)階段調(diào)用實(shí)例的相關(guān)方法。生命周期方法如下:
| 方法 | 含義描述 | 注解 |
| ------- | ------------------------------------------------------------ | -------- |
| onClose | 當(dāng)會(huì)話關(guān)閉時(shí)調(diào)用。 | @OnClose |
| onOpen | 當(dāng)開啟一個(gè)新的會(huì)話時(shí)調(diào)用, 該方法是客戶端與服務(wù)端握手成功后調(diào)用的方法。 | @OnOpen |
| onError | 當(dāng)連接過程中異常時(shí)調(diào)用。 | @OnError |
服務(wù)端如何接收客戶端發(fā)送的數(shù)據(jù)呢?
通過為 Session 添加 MessageHandler 消息處理器來(lái)接收消息,當(dāng)采用注解方式定義Endpoint時(shí),我們還可以通過 @OnMessage 注解指定接收消息的方法。
服務(wù)端如何推送數(shù)據(jù)給客戶端呢?
發(fā)送消息則由 RemoteEndpoint 完成, 其實(shí)例由 Session 維護(hù), 根據(jù)使用情況, 我們可以通過Session.getBasicRemote 獲取同步消息發(fā)送的實(shí)例 , 然后調(diào)用其 sendXxx()方法就可以發(fā)送消息, 可以通過Session.getAsyncRemote 獲取異步消息發(fā)送實(shí)例。
服務(wù)端代碼:
@ServerEndpoint("/robin") public class ChatEndPoint { private static Set<ChatEndPoint> webSocketSet = new HashSet<>(); private Session session; @OnMessage public void onMessage(String message, Session session) throws IOException { System.out.println("接收的消息是:" + message); System.out.println(session); //將消息發(fā)送給其他的用戶 for (Chat chat : webSocketSet) { if(chat != this) { chat.session.getBasicRemote().sendText(message); } } } @OnOpen public void onOpen(Session session) { this.session = session; webSocketSet.add(this); } @OnClose public void onClose(Session seesion) { System.out.println("連接關(guān)閉了。。。"); } @OnError public void onError(Session session,Throwable error) { System.out.println("出錯(cuò)了。。。。" + error.getMessage()); } }
三、基于WebSocket的網(wǎng)頁(yè)聊天室
1.需求
通過 websocket 實(shí)現(xiàn)一個(gè)簡(jiǎn)易的聊天室功能 。
1). 登陸聊天室
2). 登陸之后,進(jìn)入聊天界面進(jìn)行聊天
登陸成功后,呈現(xiàn)出以后的效果:
當(dāng)我們想和李四聊天時(shí)就可以點(diǎn)擊 `好友列表` 中的 `李四`,效果如下:
接下來(lái)就可以進(jìn)行聊天了,“張三”的界面如下:
“李四” 的界面如下:
2. 實(shí)現(xiàn)流程
3. 消息格式
客戶端 --> 服務(wù)端
{"toName":"張三","message":"你好"}
服務(wù)端 --> 客戶端
系統(tǒng)消息格式:{"isSystem":true,"fromName":null,"message":["李四","王五"]}
推送給某一個(gè)的消息格式:{"isSystem":false,"fromName":"張三","message":"你好"}
4. 功能實(shí)現(xiàn)
4.1 創(chuàng)建項(xiàng)目,導(dǎo)入相關(guān)jar包的坐標(biāo)
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.5.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--devtools熱部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <scope>true</scope> </dependency> </dependencies> <build> <plugins> <!-- 打jar包時(shí)如果不配置該插件,打出來(lái)的jar包沒有清單文件 --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
4.2 引入靜態(tài)資源文件
4.3 引入公共資源
pojo類
/** * @version v1.0 * @ClassName: Message * @Description: 瀏覽器發(fā)送給服務(wù)器的websocket數(shù)據(jù) * @Author: 黑馬程序員 */ public class Message { private String toName; private String message; public String getToName() { return toName; } public void setToName(String toName) { this.toName = toName; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
/** * @version v1.0 * @ClassName: ResultMessage * @Description: 服務(wù)器發(fā)送給瀏覽器的websocket數(shù)據(jù) * @Author: 黑馬程序員 */ public class ResultMessage { private boolean isSystem; private String fromName; private Object message;//如果是系統(tǒng)消息是數(shù)組 public boolean getIsSystem() { return isSystem; } public void setIsSystem(boolean isSystem) { this.isSystem = isSystem; } public String getFromName() { return fromName; } public void setFromName(String fromName) { this.fromName = fromName; } public Object getMessage() { return message; } public void setMessage(Object message) { this.message = message; } }
/** * @version v1.0 * @ClassName: Result * @Description: 用于登陸響應(yīng)回給瀏覽器的數(shù)據(jù) * @Author: 黑馬程序員 */ public class Result { private boolean flag; private String message; public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
MessageUtils工具類
/** * @version v1.0 * @ClassName: MessageUtils * @Description: 用來(lái)封裝消息的工具類 * @Author: 黑馬程序員 */ public class MessageUtils { public static String getMessage(boolean isSystemMessage,String fromName, Object message) { try { ResultMessage result = new ResultMessage(); result.setIsSystem(isSystemMessage); result.setMessage(message); if(fromName != null) { result.setFromName(fromName); } ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(result); } catch (JsonProcessingException e) { e.printStackTrace(); } return null; } }
4.4 登陸功能實(shí)現(xiàn)
login.html:使用異步進(jìn)行請(qǐng)求發(fā)送
$(function() { $("#btn").click(function() { $.get("login",$("#loginForm").serialize(),function(res) { if(res.flag) { //跳轉(zhuǎn)到 main.html頁(yè)面 location.href = "main.html"; } else { $("#err_msg").html(res.message); } },"json"); }); })
UserController:進(jìn)行登陸邏輯處理
@RestController public class UserController { @RequestMapping("/login") public Result login(User user, HttpSession session) { Result result = new Result(); if(user != null && "123".equals(user.getPassword())) { result.setFlag(true); //將用戶名存儲(chǔ)到session對(duì)象中 session.setAttribute("user",user.getUsername()); } else { result.setFlag(false); result.setMessage("登陸失敗"); } return result; } }
4.5 獲取當(dāng)前登錄的用戶名
main.html:頁(yè)面加載完畢后,發(fā)送請(qǐng)求獲取當(dāng)前登錄的用戶名
var username; $(function() { $.ajax({ url:"getUsername", success:function(res) { username = res; $("#userName").html("用戶:" + res + "<span style='float: right;color: green'>在線</span>"); }, async:false }); }
UserController
在UserController中添加一個(gè)getUsername方法,用來(lái)從session中獲取當(dāng)前登錄的用戶名并響應(yīng)回給瀏覽器
@RequestMapping("/getUsername") public String getUsername(HttpSession session) { String username = (String) session.getAttribute("user"); return username; }
4.6 聊天室功能
客戶端實(shí)現(xiàn)
在main.html頁(yè)面實(shí)現(xiàn)前端代碼:
var toName; var username; function showChat(name) { toName = name; //清除聊天區(qū)的數(shù)據(jù) $("#msgs").html(""); //現(xiàn)在聊天對(duì)話框 $("#chatArea").css("display","inline"); //顯示“正在和誰(shuí)聊天” $("#chatMes").html("正在和 <font face=\"楷體\">"+toName+"</font> 聊天"); //切換用戶,需要將聊天記錄渲染到聊天區(qū) var storeData = sessionStorage.getItem(toName); if(storeData != null) { $("#msgs").html(storeData); } } $(function() { $.ajax({ url:"getUsername", success:function(res) { username = res; //顯示在線信息 $("#userName").html(" 用戶:"+res+"<span style='float: right;color: green'>在線</span>"); }, async: false }) //創(chuàng)建websocket var ws; if(window.WebSocket) { ws = new WebSocket("ws://localhost/chat"); } //綁定事件 ws.onopen = function(evt) { //顯示在線信息 $("#userName").html(" 用戶:"+username+"<span style='float: right;color: green'>在線</span>"); } ws.onmessage = function(evt) { //接收服務(wù)器推送的消息 var data = evt.data; //將該字符串?dāng)?shù)據(jù)轉(zhuǎn)換為json var res = JSON.parse(data); //判斷是系統(tǒng)消息還是推送給個(gè)人的消息 if(res.isSystem) { //系統(tǒng)消息 var names = res.message; var userListStr = ""; var broadcastStr = ""; for(var name of names) { if(name != username) { userListStr += "<li class=\"rel-item\"><a onclick='showChat(\""+name+"\")'>"+name+"</a></li>"; broadcastStr += "<li class=\"rel-item\" style=\"color: #9d9d9d;font-family: 宋體\">您的好友 "+name+" 已上線</li>"; } } //將數(shù)據(jù)渲染到頁(yè)面 $("#userlist").html(userListStr); $("#broadcastList").html(broadcastStr); } else { //非系統(tǒng)消息 var content = res.message; //拼接聊天區(qū)展示的數(shù)據(jù) var str = "<div class=\"msg robot\"><div class=\"msg-left\" worker=\"\"><div class=\"msg-host photo\" style=\"background-image: url(img/avatar/Member002.jpg)\"></div><div class=\"msg-ball\">"+content+"</div></div></div>"; //有可能現(xiàn)在不是和指定用戶的聊天框,所以需要進(jìn)行判斷 var storeData = sessionStorage.getItem(res.fromName); if(storeData != null) { storeData += str; } else { storeData = str; } sessionStorage.setItem(res.fromName,storeData); if(toName == res.fromName) { //將數(shù)據(jù)追加到聊天區(qū) $("#msgs").append(str); } } } ws.onclose = function() { //顯示在線信息 $("#userName").html(" 用戶:"+username+"<span style='float: right;color: red'>離線</span>"); } //給發(fā)送按鈕綁定點(diǎn)擊事件 $("#submit").click(function() { //獲取輸入的內(nèi)容 var data = $("#context_text").val(); //將該文本框清空 $("#context_text").val(""); //拼接消息 var str = "<div class=\"msg guest\"><div class=\"msg-right\"><div class=\"msg-host headDefault\"></div><div class=\"msg-ball\">"+data+"</div></div></div>"; $("#msgs").append(str); //將聊天記錄進(jìn)行存儲(chǔ)sessionStorage var storeData = sessionStorage.getItem(toName); if(storeData != null) { //將此次的內(nèi)容拼接到storeData中 str = storeData + str; } //將消息存儲(chǔ)到sessionStorage中 sessionStorage.setItem(toName,str); //定義服務(wù)端需要的數(shù)據(jù)格式 var message = {toName:toName,message:data}; //將輸入的數(shù)據(jù)發(fā)送給服務(wù)器 ws.send(JSON.stringify(message)); }); })
服務(wù)端代碼實(shí)現(xiàn)
`WebSocketConfig` 類實(shí)現(xiàn)
開啟 springboot 對(duì)websocket的支持
@Configuration public class WebSocketConfig { @Bean //注入ServerEndpointExporter,自動(dòng)注冊(cè)使用@ServerEndpoint注解的 public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
`ChatEndPoint` 類實(shí)現(xiàn)
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfigurator.class) @Component public class ChatEndpoint { //用來(lái)存儲(chǔ)每一個(gè)客戶端對(duì)象對(duì)應(yīng)的ChatEndpoint對(duì)象 private static Map<String,ChatEndpoint> onlineUsers = new ConcurrentHashMap<>(); //和某個(gè)客戶端連接對(duì)象,需要通過他來(lái)給客戶端發(fā)送數(shù)據(jù) private Session session; //httpSession中存儲(chǔ)著當(dāng)前登錄的用戶名 private HttpSession httpSession; @OnOpen //連接建立成功調(diào)用 public void onOpen(Session session, EndpointConfig config) { //需要通知其他的客戶端,將所有的用戶的用戶名發(fā)送給客戶端 this.session = session; //獲取HttpSession對(duì)象 HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName()); //將該httpSession賦值給成員httpSession this.httpSession = httpSession; //獲取用戶名 String username = (String) httpSession.getAttribute("user"); //存儲(chǔ)該鏈接對(duì)象 onlineUsers.put(username,this); //獲取需要推送的消息 String message = MessageUtils.getMessage(true, null, getNames()); //廣播給所有的用戶 broadcastAllUsers(message); } private void broadcastAllUsers(String message) { try { //遍歷 onlineUsers 集合 Set<String> names = onlineUsers.keySet(); for (String name : names) { //獲取該用戶對(duì)應(yīng)的ChatEndpoint對(duì)象 ChatEndpoint chatEndpoint = onlineUsers.get(name); //發(fā)送消息 chatEndpoint.session.getBasicRemote().sendText(message); } } catch (Exception e) { e.printStackTrace(); } } private Set<String> getNames() { return onlineUsers.keySet(); } @OnMessage //接收到消息時(shí)調(diào)用 public void onMessage(String message,Session session) { try { //獲取客戶端發(fā)送來(lái)的數(shù)據(jù) {"toName":"張三","message":"你好"} ObjectMapper mapper = new ObjectMapper(); Message mess = mapper.readValue(message, Message.class); //獲取當(dāng)前登錄的用戶名 String username = (String) httpSession.getAttribute("user"); //拼接推送的消息 String data = MessageUtils.getMessage(false, username, mess.getMessage()); //將數(shù)據(jù)推送給指定的客戶端 ChatEndpoint chatEndpoint = onlineUsers.get(mess.getToName()); chatEndpoint.session.getBasicRemote().sendText(data); } catch (Exception e) { e.printStackTrace(); } } @OnClose //連接關(guān)閉時(shí)調(diào)用 public void onClose(Session session) { //獲取用戶名 String username = (String) httpSession.getAttribute("user"); //移除連接對(duì)象 onlineUsers.remove(username); //獲取需要推送的消息 String message = MessageUtils.getMessage(true, null, getNames()); //廣播給所有的用戶 broadcastAllUsers(message); } }
`GetHttpSessionConfigurator` 配置類實(shí)現(xiàn)
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator { @Override public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) { HttpSession httpSession = (HttpSession) request.getHttpSession(); config.getUserProperties().put(HttpSession.class.getName(),httpSession); } }