========================前后台分別要做的事情========================
一.前台 使用js【HTML頁面】
var websocket = new WebSocket("ws://localhost:8080/echo");
獲取socket連接。
注意1:
URL地址是"ws://后台服務IP:Port/路由地址"
注意2:
路由地址是任意自定義的。后台會有配置關於一個路由地址的【Handler處理器】和【Interceptor攔截器】
注意3:
一種路由地址之間,是可以相互通信的。
例如:
html1 中配置路由地址:echo html2 中也配置路由地址:echo
在服務器配置了 這一種路由地址echo的【Handler處理器】和【Interceptor攔截器】后。
這就代表,html1和html2都在服務器上注冊了關於 路由地址echo 的webSocket服務。
現在,只要html1使用echo注冊的webSocket服務發送消息(websocket.send(json);),服務器端就會攔截到消息,並對其他注冊了echo這個webSocket服務的頁面發送信息。
可以是對單點發送,也可以是對所有廣播。
二.后台服務器端 spring boot 集成 webSocket
后台服務器端,需要做三件事情:
1.配置webSocket的攔截器,實現HandshakeInterceptor接口
(攔截和webSocket相關的信息)
2.配置webSocket的處理器,實現WebSocketHandler接口
(處理對一個路由地址的相關操作,例如 監聽socket連接成功,socket關閉連接,處理信息等)
3.完善webSocket的配置,實現WebSocketConfigurer接口
(將處理器handler 和 一個路由地址 綁定,並對本地址設置攔截器Interceptor,最后設置訪問域名的限制)
=====================spring boot 集成 webSocket===================
一.新建一個空的spring boot項目,項目依賴2.0.5 spring boot版本
【完整項目結構在最下方可見】
二.完善代碼
1.pom文件完整示例
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.sxd</groupId> <artifactId>socket-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>socket-demo</name> <description>Demo project for Spring Boot And Socket</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--需要添加的jar 依賴--> <!-- json工具 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>LATEST</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>compile</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
2.新建握手攔截器
package com.sxd.config; import lombok.extern.slf4j.Slf4j; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; import javax.servlet.http.HttpServletRequest; import java.util.Map; /** * 實現【握手攔截器】 * * @Slf4j 注解使用解析 https://www.cnblogs.com/sxdcgaq8080/p/11288213.html * @Component 注解是將本攔截器注入為Bean給Spring容器管理 * * @author sxd * @date 2019/8/2 13:47 */ @Slf4j @Component public class MyHandshakeInterceptor implements HandshakeInterceptor{ /** * 重寫方法 在握手之前做的事情 * @param serverHttpRequest * @param serverHttpResponse * @param webSocketHandler * @param map * @return * @throws Exception */ @Override public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception { if (serverHttpRequest instanceof ServletServerHttpRequest) { HttpServletRequest servletRequest = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest(); // 從session中獲取到當前登錄的用戶信息. 作為socket的賬號信息. session的的WEBSOCKET_USERNAME信息,在用戶打開頁面的時候設置. String userName = (String) servletRequest.getSession().getAttribute("WEBSOCKET_USERNAME"); map.put("WEBSOCKET_USERNAME", userName); } return true; } /** * 重寫方法 在握手之后做的事情 * @param serverHttpRequest * @param serverHttpResponse * @param webSocketHandler * @param e */ @Override public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) { } }
3.新建webSocket的handler處理器
package com.sxd.config; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.socket.*; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * 實現WebSocketHandler接口 * * * @author sxd * @date 2019/8/2 14:06 */ @Slf4j @Component public class MySocketHander implements WebSocketHandler { /** * 為了保存在線用戶信息,在方法中新建一個list存儲一下【實際項目依據復雜度,可以存儲到數據庫或者緩存】 */ private final static List<WebSocketSession> SESSIONS = Collections.synchronizedList(new ArrayList<>()); @Override public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception { log.info("鏈接成功......"); SESSIONS.add(webSocketSession); String userName = (String) webSocketSession.getAttributes().get("WEBSOCKET_USERNAME"); if (userName != null) { JSONObject obj = new JSONObject(); // 統計一下當前登錄系統的用戶有多少個 obj.put("count", SESSIONS.size()); users(obj); webSocketSession.sendMessage(new TextMessage(obj.toJSONString())); } } @Override public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception { log.info("處理要發送的消息"); JSONObject msg = JSON.parseObject(webSocketMessage.getPayload().toString()); JSONObject obj = new JSONObject(); if (msg.getInteger("type") == 1) { //給所有人 obj.put("msg", msg.getString("msg")); sendMessageToUsers(new TextMessage(obj.toJSONString())); } else { //給個人 String to = msg.getString("to"); obj.put("msg", msg.getString("msg")); sendMessageToUser(to, new TextMessage(obj.toJSONString())); } } @Override public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception { if (webSocketSession.isOpen()) { webSocketSession.close(); } log.info("鏈接出錯,關閉鏈接......"); SESSIONS.remove(webSocketSession); } @Override public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception { log.info("鏈接關閉......" + closeStatus.toString()); SESSIONS.remove(webSocketSession); } @Override public boolean supportsPartialMessages() { return false; } /** * 給某個用戶發送消息 * * @param userName * @param message */ public void sendMessageToUser(String userName, TextMessage message) { log.info("發消息給某個用戶"); for (WebSocketSession user : SESSIONS) { if (user.getAttributes().get("WEBSOCKET_USERNAME").equals(userName)) { try { if (user.isOpen()) { user.sendMessage(message); } } catch (IOException e) { e.printStackTrace(); } break; } } } /** * 給所有在線用戶發送消息 * * @param message */ public void sendMessageToUsers(TextMessage message) { log.info("發送消息給所用用戶"); for (WebSocketSession user : SESSIONS) { try { if (user.isOpen()) { user.sendMessage(message); } } catch (IOException e) { e.printStackTrace(); } } } /** * 將系統中的用戶傳送到前端 * * @param obj */ private void users(JSONObject obj) { List<String> userNames = new ArrayList<>(); for (WebSocketSession webSocketSession : SESSIONS) { userNames.add((String) webSocketSession.getAttributes().get("WEBSOCKET_USERNAME")); } obj.put("users", userNames); } }
4.新建webSocket的config配置
package com.sxd.config; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; /** * @author sxd * @date 2019/8/2 14:43 */ @Slf4j @Configuration @EnableWebSocket public class MyWebSocketConfig implements WebSocketConfigurer { @Autowired MyHandshakeInterceptor handshakeInterceptor; @Autowired MySocketHander socketHander; /** * 實現 WebSocketConfigurer 接口 * 重寫 registerWebSocketHandlers 方法,這是一個核心實現方法,配置 websocket 入口,允許訪問的域、注冊 Handler、SockJs 支持和攔截器。 * * registry.addHandler()注冊和路由的功能 * 當客戶端發起 websocket 連接,把 /path 交給對應的 handler 處理,而不實現具體的業務邏輯,可以理解為收集和任務分發中心。 * * * addInterceptors() 顧名思義就是為 handler 添加攔截器 * 可以在調用 handler 前后加入我們自己的邏輯代碼。 * * * setAllowedOrigins(String[] domains),允許指定的域名或 IP (含端口號)建立長連接 * 如果只允許自家域名訪問,這里輕松設置。如果不限時使用 * 號,如果指定了域名,則必須要以 http 或 https 開頭。 * * @param registry */ @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { //部分 支持websocket 的訪問鏈接,允許跨域 registry.addHandler(socketHander, "/my_SXD_Socket").addInterceptors(handshakeInterceptor).setAllowedOrigins("*"); //部分 不支持websocket的訪問鏈接,允許跨域 registry.addHandler(socketHander, "/sockjs/my_SXD_Socket").addInterceptors(handshakeInterceptor).setAllowedOrigins("*").withSockJS(); } }
5.新建controller用於模擬用戶登錄,給Session填充值
package com.sxd.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; /** * 本controller中的三個方法,即相當於一個login接口的 三次請求,只不過值寫死在方法中。 * * 只需要分別請求三個接口,即相當於三個用戶分別作了登錄操作,並存儲到了session中。 * * @author sxd * @date 2019/8/2 14:48 */ @Controller @RequestMapping("/socket") public class LoginController { /** * 第一個用戶 * * @param request * @return */ @RequestMapping("/chat1") public String chat1(HttpServletRequest request) { // 假設用戶tom登錄,存儲到session中 request.getSession().setAttribute("WEBSOCKET_USERNAME", "tom"); return "chat1"; } /** * 第二個用戶登錄 * * @param request * @return */ @RequestMapping("/chat2") public String chat2(HttpServletRequest request) { // 假設用戶jerry登錄,存儲到session中 request.getSession().setAttribute("WEBSOCKET_USERNAME", "jerry"); return "chat2"; } /** * 第三個用戶登錄 * * @param request * @return */ @RequestMapping("/chat3") public String chat3(HttpServletRequest request) { // 假設用戶jack登錄,存儲到session中 request.getSession().setAttribute("WEBSOCKET_USERNAME", "jack"); return "chat3"; } }
6.完善application.properties項目配置,全是freemarker的配置
spring.freemarker.template-loader-path=classpath:/templates spring.freemarker.cache=false spring.freemarker.charset=UTF-8 spring.freemarker.check-template-location=true spring.freemarker.content-type=text/html spring.freemarker.expose-request-attributes=false spring.freemarker.expose-session-attributes=false spring.freemarker.request-context-attribute=request spring.freemarker.suffix=.html
7.新建一個templates目錄在resource下,並復制下面三份,分別在templates下創建chat1.html、chat2.html、chat3.html
<!DOCTYPE html> <html lang="en"> <head> <title>測試websocket</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.2/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.css"> </head> <body> <div class="container"> <div class="input-group mb-3"> <div class="input-group-prepend"> <label class="input-group-text" for="inputGroupSelect01">用戶</label> </div> <select class="custom-select" id="inputGroupSelect01"> <option selected>選擇一個...</option> </select> </div> <div class="input-group mb-3"> <input type="text" class="form-control"> <div class="input-group-append"> <span class="input-group-text" id="btn1">發送給所有人</span> </div> </div> <div class="input-group mb-3"> <input type="text" class="form-control"> <div class="input-group-append"> <span class="input-group-text" id="btn2">發送給單人</span> </div> </div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.2/js/bootstrap.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.js"></script> <script language=javascript> $(function () { var websocket; if ('WebSocket' in window) { console.log("WebSocket-->"); //new WebSocket()在IDEA打開的任何項目中都可以 直接調用 websocket = new WebSocket("ws://localhost:8080/my_SXD_Socket"); } else if ('MozWebSocket' in window) { console.log("MozWebSocket-->"); websocket = new MozWebSocket("ws://my_SXD_Socket"); } else { console.log("SockJS-->"); websocket = new SockJS("http://127.0.0.1:8080/sockjs/my_SXD_Socket"); } websocket.onopen = function (evnt) { console.log("鏈接服務器成功!", evnt.data); }; websocket.onmessage = function (evnt) { console.log('收到消息:', evnt.data); var json = JSON.parse(evnt.data); if (json.hasOwnProperty('users')) { var users = json.users; for (var i = 0; i < users.length; i++) { $("#inputGroupSelect01").append('<option value="' + users[i] + '">' + users[i] + '</option>'); } } else { //打印消息 toast(json.msg, 'info') } }; websocket.onerror = function (evnt) { }; websocket.onclose = function (evnt) { console.log("與服務器斷開了鏈接!") } $('#btn2').bind('click', function () { if (websocket != null) { //根據勾選的人數確定是群聊還是單聊 var value = $(this).parent().parent().find('input').val(); //得到選擇的用戶 var name = $("#inputGroupSelect01").find("option:selected").val(); console.log('選中的用戶', name); if (name === '選擇一個...') { toast('請選擇一個用戶', 'warning') } else { var object = { to: name, msg: value, type: 2 }; //將object轉成json字符串發送給服務端 var json = JSON.stringify(object); websocket.send(json); } } else { console.log('未與服務器鏈接.'); } }); $('#btn1').bind('click', function () { if (websocket != null) { //根據勾選的人數確定是群聊還是單聊 var value = $(this).parent().parent().find('input').val(); var object = { msg: value, type: 1 }; //將object轉成json字符串發送給服務端 var json = JSON.stringify(object); websocket.send(json); } else { console.log('未與服務器鏈接.'); } }); }) function toast(text, icon) { $.toast({ text: text, heading: '新消息', icon: icon, showHideTransition: 'slide', allowToastClose: true, hideAfter: 3000, stack: 5, position: 'top-right', bgColor: '#444444', textColor: '#eeeeee', textAlign: 'left', loader: true, loaderBg: '#006eff' }); } </script> </body> </html>
8.最終項目結構如下:

9.啟動項目,並分別訪問下面三個URL,模擬用戶登錄了
http://localhost:8080/socket/chat1
http://localhost:8080/socket/chat2
http://localhost:8080/socket/chat3

10.發送消息給單人或多人
發送給指定的人:

發送給所有人:

