前幾天寫了一篇《SpringBoot快速入門》一文,而後週末趁着有時間,在這個Springboot框架基礎上整合了WebSocket技術寫了一個網頁版聊天功能。javascript
若是小夥伴找不到那套框架了,能夠看下以前的文章找到Springboot快速入門一文css
往期推薦html
經過該文章能夠了解服務端與客戶端之間的通訊機制,以及瞭解相關的Http協議等技術內容。java
話很少說,先來看看運行的過程:jquery
頁面寫的十分簡單,後續也會陸續將其優化和完善。git
正文github
1、HTTP相關知識web
HTTP協議ajax
http是一個簡單的請求-響應協議,它一般運行在TCP之上。它指定了客戶端可能發送給服務器什麼樣的消息以及獲得什麼樣的響應。請求和響應消息的頭以ASCII碼形式給出;而消息內容則具備一個相似MIME的格式。這個簡單模型是早期Web成功的有功之臣,由於它使開發和部署很是地直截了當
http 爲短鏈接:客戶端發送請求都須要服務器端回送響應。請求結束後,主動釋放連接,所以爲短鏈接。一般的作法是,不須要任何數據,也要保持每隔一段時間向服務器發送"保持鏈接"的請求。這樣能夠保證客戶端在服務器端是"上線"狀態。
HTTP鏈接使用的是"請求-響應"方式,不只在請求時創建鏈接,並且客戶端向服務器端請求後,服務器才返回數據。
2、Socket相關知識
1. 要想明白 Socket,必需要理解 TCP 鏈接。
① TCP 三次握手:握手過程當中並不傳輸數據,在握手後服務器與客戶端纔開始傳輸數據,理想狀態下,TCP 鏈接一旦創建,在通信雙方中的任何一方主動斷開鏈接以前 TCP 鏈接會一直保持下去。
② Socket 是對 TCP/IP 協議的封裝,Socket 只是個接口不是協議,經過 Socket 咱們才能使用 TCP/IP 協議,除了 TCP,也可使用 UDP 協議來傳遞數據。
③ 建立 Socket 鏈接的時候,能夠指定傳輸層協議,能夠是 TCP 或者 UDP,當用 TCP 鏈接,該Socket就是個TCP鏈接,反之。
2. Socket 原理
Socket 鏈接,至少須要一對套接字,分爲 clientSocket,serverSocket 鏈接分爲3個步驟:
(1) 服務器監聽:服務器並不定位具體客戶端的套接字,而是時刻處於監聽狀態;
(2) 客戶端請求:客戶端的套接字要描述它要鏈接的服務器的套接字,提供地址和端口號,而後向服務器套接字提出鏈接請求;
(3) 鏈接確認:當服務器套接字收到客戶端套接字發來的請求後,就響應客戶端套接字的請求,並創建一個新的線程,把服務器端的套接字的描述發給客戶端。一旦客戶端確認了此描述,就正式創建鏈接。而服務器套接字繼續處於監聽狀態,繼續接收其餘客戶端套接字的鏈接請求。
Socket爲長鏈接:一般狀況下Socket 鏈接就是 TCP 鏈接,所以 Socket 鏈接一旦創建,通信雙方開始互發數據內容,直到雙方斷開鏈接。在實際應用中,因爲網絡節點過多,在傳輸過程當中,會被節點斷開鏈接,所以要經過輪詢高速網絡,該節點處於活躍狀態。
不少狀況下,都是須要服務器端向客戶端主動推送數據,保持客戶端與服務端的實時同步。
若雙方是 Socket 鏈接,能夠由服務器直接向客戶端發送數據。
若雙方是 HTTP 鏈接,則服務器須要等客戶端發送請求後,才能將數據回傳給客戶端。
所以,客戶端定時向服務器端發送請求,不只能夠保持在線,同時也詢問服務器是否有新數據,若是有就將數據傳給客戶端。
要弄明白 http 和 socket 首先要熟悉網絡七層:物 數 網 傳 會 表 應,如圖:
如圖
HTTP 協議:超文本傳輸協議,對應於應用層,用於如何封裝數據。
TCP/UDP 協議:傳輸控制協議,對應於傳輸層,主要解決數據在網絡中的傳輸。
IP 協議:對應於網絡層,一樣解決數據在網絡中的傳輸。
傳輸數據的時候只使用 TCP/IP 協議(傳輸層),若是沒有應用層來識別數據內容,傳輸後的協議都是無用的。
應用層協議不少 FTP,HTTP,TELNET等,能夠本身定義應用層協議。
web 使用 HTTP 做傳輸層協議,以封裝 HTTP 文本信息,而後使用 TCP/IP 作傳輸層協議,將數據發送到網絡上。
3、WebSocket相關知識
WebSocket 是 HTML5 開始提供的一種在單個 TCP 鏈接上進行全雙工通信的協議。
WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。
在 WebSocket API 中,瀏覽器和服務器只須要作一個握手的動做,而後,瀏覽器和服務器之間就造成了一條快速通道。二者之間就直接能夠數據互相傳送。
如今,不少網站爲了實現推送技術,所用的技術都是 Ajax 輪詢。輪詢是在特定的的時間間隔(如每1秒),由瀏覽器對服務器發出HTTP請求,而後由服務器返回最新的數據給客戶端的瀏覽器。這種傳統的模式帶來很明顯的缺點,即瀏覽器須要不斷的向服務器發出請求,然而HTTP請求可能包含較長的頭部,其中真正有效的數據可能只是很小的一部分,顯然這樣會浪費不少的帶寬等資源。
HTML5 定義的 WebSocket 協議,能更好的節省服務器資源和帶寬,而且可以更實時地進行通信。
4、實現源碼:
1 聊天頁面chat.html
前端採用bootstrap,引入了: jquery-3.3.1.min.js、bootstrap.min.css。小夥伴可自行選擇:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.springframework.org/schema/mvc"> <head> <meta charset="UTF-8"> <title>chat room websocket</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> <script th:src="@{/js/jquery-3.3.1.min.js}"></script> </head> <body class="container" style="width: 60%"> <div class="form-group" style="width: 100%; margin-top: 10px;"> <div style="width: 100%; background-color: #800080; color: #ffffff;"> <label for="user_name" style="float: left; margin-left: 45%">你好:</label> <h5 id="user_name" th:text="${username}" style="width: 80%;"></h5> </div> </div> <div class="form-group" style="float: left; width: 100%;"> <label for="user_list" style="float: left;">選擇聊天用戶:</label> <select id="user_list" style="width: 15%;"></select> <span id="error_select_msg" style="color: red;"></span> </div> <div class="form-group" style="float: left; width: 100%;"> <div id="message_user" style="width: 25%; height: 450px; overflow-y: auto; position: relative; float: left;" class="form-control" readonly="readonly"> 羣成員:<span id="message_user_count"></span><br/> </div> <div id="message_chat" style="font-size: 13px; width: 75%; height: 300px; overflow-y: auto; position: relative; float: left;" class="form-control" readonly="readonly"> </div> <div style="width: 75%; float: right;"> <div style="width: 100%; height: 110px;"> <textarea style="height: 100%; border-bottom: #ffffff solid 0px;" id="chat_msg" value="" class="form-control"></textarea> </div> <div style="width: 100%; float: right; border-bottom: #808080 solid 1px;"> <button style="float: right;" id="send" class="btn btn-info">發送消息</button> <button style="float: right;" id="send_all" class="btn btn-info">羣發消息</button> <button style="float: right;" id="user_exit" class="btn btn-warning">退出</button> </div> </div> </div> </body> <script type="text/javascript"> $(document).ready(function() { initUserList(); let urlPrefix = 'ws://localhost:8080/net/websocket/'; let ws = null; let username = $('#user_name').text(); ws = initMsg(urlPrefix, username); // 客戶端發送對某一個客戶的消息到服務器 $('#send').click(function() { let userList = $("#user_list option:selected").val(); if (!userList) { $("#error_select_msg").html("請選擇一個用戶!"); return; } let msg = $('#chat_msg').val(); if (!msg) { alert("請輸入聊天內容!"); return; } msg = msg + "[" + userList + "]" + "----------" + username; if (ws) { ws.send(msg); //服務端發送的消息 $('#message_chat').append('<div ><span >' + username + ' </span><br/>'); $('#message_chat').append('<span >' + msg.substring(0, msg.indexOf('[')) + '</span></div>'); $("#chat_msg").val(''); $("#error_select_msg").empty(); } }); // 客戶端羣發消息到服務器 $('#send_all').click(function() { let msg = $('#chat_msg').val(); if (!msg) { alert("請輸入聊天內容!"); return; } msg = msg + "[allUsers]" + "----------" + username; if (ws) { ws.send(msg); //服務端發送的消息 $('#message_chat').append('<div ><span >' + username + ' 的羣發消息 </span><br/>'); $('#message_chat').append('<span >' + msg.replace('[allUsers]----------' + username, '') + '</span></div>'); $("#chat_msg").val(''); $("#error_select_msg").empty(); } }); // 退出聊天室 $('#user_exit').click(function() { if (ws) { ws.close(); } window.location.href = "/chat/login"; }); // 用戶下拉列表點擊事件 $("#user_list").on("change", function() { $("#error_select_msg").empty(); }); }); /** * 初始化用戶列表 */ function initUserList() { let username = $('#user_name').text(); $.ajax({ url: "/getUserList", type: "POST", data: {username: username}, success: function(data) { let result = JSON.parse(data); let html = "<option value=''>---請選擇---</option>"; for (let i = 0; i < result.length; i++) { html += "<option value='" + result[i].username + "'>" + result[i].username + "</option>"; } let userList = ""; for (let i = 0; i < result.length; i++) { userList += "<div class='select_user'>" + result[i].username + "</div>"; } $("#user_list").html(html); $("#message_user_count").text(result.length + "人"); $("#message_user").append(userList); } }); } /** * 初始化消息 * * @param urlPrefix * @param username * @returns {WebSocket} */ function initMsg(urlPrefix, username) { let url = urlPrefix + username; ws = new WebSocket(url); ws.onopen = function () { console.log("創建 websocket 鏈接..."); }; ws.onmessage = function(event) { //服務端發送的消息 $('#message_chat').append(event.data + 'n'); }; ws.onclose = function() { $('#message_chat').append('<div >用戶[' + username + '] 已經離開聊天室!' + '</div>'); console.log("用戶:[" + username + "]已關閉 websocket 鏈接..."); } return ws; } </script> </html>
2 pom.xml加入WebSocket依賴
<!-- 集成webSocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- 集成json --> <dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.2.3</version> </dependency>
3 實現WebSocket服務端
① 建立SocketEndPoint.java核心聊天頁面實現類
該類爲WebSocket的核心實現類,主要實現聊天鏈接、消息發送、退出聊天、異常處理等頁面聊天的核心功能。其中:
@PathParam這個註解是將請求路徑中綁定的佔位符的值給取出來,做爲參數條件使用。是javax.websocket.server下的一個註解。
在項目中,經過name對socket鏈接進行訪問控制,後臺後續會將name做爲惟一主鍵,小夥伴也能夠經過在url裏面增長ket + name的方式進行訪問控制,key做爲登錄以後,服務器給用戶的令牌,經過令牌和name進行權限校驗(這裏目前沒有實現,只保證name是惟一)。
SocketEndPoint.java類實現:
package cn.cansluck.utils.net; import cn.cansluck.service.IUserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.text.DateFormat; import java.util.Date; import java.util.Map; import static cn.cansluck.utils.net.SocketPool.*; import static cn.cansluck.utils.net.SocketHandler.createKey; // 注入容器 @Component // 代表這是一個websocket服務的端點 @ServerEndpoint("/net/websocket/{name}") public class SocketEndPoint { private static final Logger log = LoggerFactory.getLogger(SocketEndPoint.class); private static IUserService userService; @Autowired public void setUserService(IUserService userService){ SocketEndPoint.userService = userService; } @OnOpen public void onOpen(@PathParam("name") String name, Session session) { log.info("有新的鏈接:{}", session); add(createKey(name), session); for (Map.Entry<String, Session> item : sessionMap().entrySet()) { if (item.getKey().equals(name)) { SocketHandler.sendMessageAll("<div style='width: 100%; float: left;'>用戶【" + name + "】已上線</div>", name); } } log.info("在線人數:{}",count()); sessionMap().keySet().forEach(item -> log.info("在線用戶:" + item)); for (Map.Entry<String, Session> item : sessionMap().entrySet()) { log.info("12: {}", item.getKey()); } } @OnMessage public void onMessage(String message) { if (message.contains("[allUsers]")) { String userInfo = message.substring(message.indexOf("[allUsers]")).replace("[allUsers]----------", ""); SocketHandler.sendMessageAll( "<div style='width: 100%; float: left;'> " + userInfo + "羣發消息</div><div style='width: 100%; font-size: 18px; font-weight: bolder; float: right;'>" + message.substring(0, message.indexOf("[")) + "</div>", userInfo); } else { String acceptUser = message.substring(message.indexOf("[") + 1, message.lastIndexOf("]")); String sendUser = message.substring(message.lastIndexOf("-") + 1, message.length()); Session userSession; for (Map.Entry<String, Session> item : sessionMap().entrySet()) { if (item.getKey().equals(acceptUser)) { userSession = item.getValue(); String userInfo = message.substring(0, message.indexOf("[")); SocketHandler.sendMessage(userSession, "<div style='width: 100%; float: left;'> " + sendUser + "</div><div style='width: 100%; font-size: 18px; font-weight: bolder; float: right;'>" + userInfo + "</div>"); } } } log.info("有新消息: {}", message); } @OnClose public void onClose(@PathParam("name") String name,Session session) { log.info("鏈接關閉: {}", session); remove(createKey(name)); log.info("在線人數:{}", count()); sessionMap().keySet().forEach(item -> log.info("在線用戶:" + item)); for (Map.Entry<String, Session> item : sessionMap().entrySet()){ log.info("12: {}", item.getKey()); } Date date = new Date(); DateFormat df = DateFormat.getDateTimeInstance();//能夠精確到時分秒 SocketHandler.sendMessageAll("<div style='width: 100%; float: left;'>[" + df.format(date) + "] " + name + "已離開聊天室</div>", name); } @OnError public void onError(Session session, Throwable throwable) { try { session.close(); } catch (IOException e) { log.error("退出發生異常: {}", e.getMessage()); } log.info("鏈接出現異常: {}", throwable.getMessage()); } }
② 建立SocketPool.java在線鏈接池類
package cn.cansluck.utils.net; import javax.websocket.Session; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * WebSocket鏈接池類 * * @author Cansluck */ public class SocketPool { // 在線用戶websocket鏈接池 private static final Map<String, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>(); /** * 新增一則鏈接 * @param key 設置主鍵 * @param session 設置session */ public static void add(String key, Session session) { if (!key.isEmpty() && session != null){ ONLINE_USER_SESSIONS.put(key, session); } } /** * 根據Key刪除鏈接 * @param key 主鍵 */ public static void remove(String key) { if (!key.isEmpty()){ ONLINE_USER_SESSIONS.remove(key); } } /** * 獲取在線人數 * @return 返回在線人數 */ public static int count(){ return ONLINE_USER_SESSIONS.size(); } /** * 獲取在線session池 * @return 獲取session池 */ public static Map<String, Session> sessionMap(){ return ONLINE_USER_SESSIONS; } }
③ 建立SocketHandler.java動做處理工具類
package cn.cansluck.utils.net; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.websocket.RemoteEndpoint; import javax.websocket.Session; import java.io.IOException; import static cn.cansluck.utils.net.SocketPool.sessionMap; /** * WebSocket動做類 * * @author Cansluck */ public class SocketHandler { private static final Logger log = LoggerFactory.getLogger(SocketHandler.class); /** * 根據key和用戶名生成一個key值,簡單實現下 * @param name 發送人 * @return 返回值 */ public static String createKey(String name){ return name; } /** * 給指定用戶發送信息 * @param session session * @param msg 發送的消息 */ public static void sendMessage(Session session, String msg) { if (session == null) return; final RemoteEndpoint.Basic basic = session.getBasicRemote(); if (basic == null) return; try { basic.sendText(msg); } catch (IOException e) { log.error("消息發送異常,異常狀況: {}", e.getMessage()); } } /** * 給全部的在線用戶發送消息 * @param message 發送的消息 * @param username 發送人 */ public static void sendMessageAll(String message, String username) { log.info("廣播:羣發消息"); // 遍歷map,只輸出給其餘客戶端,不給本身重複輸出 sessionMap().forEach((key, session) -> { if (!username.equals(key)) { sendMessage(session, message); } }); } }
④ 建立ChatController.java頁面訪問控制器類
package cn.cansluck.controller; import cn.cansluck.service.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; /** * 登陸頁 * * @author Cansluck */ @RequestMapping("/chat") @Controller public class ChatController { @Autowired private IUserService userService; /** * 登錄 * * @author Cansluck * @return 返回頁面 */ @RequestMapping("/login") public String login(String username, String password, ModelMap map) { if (null == username || "".equals(username)) return "login"; boolean isLogin = userService.login(username, password); if (isLogin) { map.addAttribute("username", username); return "chat"; } return "login"; } }
⑤ 建立SocketConfig.java的websocket配置類
package cn.cansluck.utils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * WebSocket配置類 * * @author Cansluck */ @Configuration @EnableWebSocket public class SocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }
以上就是一個WebSocket的簡單實現,更多的場景小夥伴能夠自行在這個基礎上實現更多功能。後續會繼續完善該聊天的功能,代碼將會上傳到GitHub上供下載。有興趣的小夥伴能夠一塊兒來創做玩一下呀~後續還會將項目打包部署到我我的的騰訊雲服務器上,有興趣的能夠一塊兒來聊天呀~
GitHub項目下載地址
https://github.com/125207780/springboot-project.git
小夥伴們能夠自行下載並操做,能夠一塊兒修改一塊兒玩呀~
更多精彩敬請關注公衆號
Java極客思惟
微信掃一掃,關注公衆號