http協議是無狀態協議,每次請求都不知道前面發生了什麼,並且只能夠由瀏覽器端請求服務器端,而不能由服務器去主動通知瀏覽器端,是單向的,在不少場景就不適合,好比實時的推送,消息通知或者股票等信息的推送;在沒有 websocket 以前,要解決這種問題,只能依靠 ajax輪詢 或者 長輪詢,這兩種方式極大的消耗資源;而websocket,只須要藉助http協議進行握手,而後保持着一個websocket鏈接,直到客戶端主動斷開;相對另外的兩種方式,websocket只進行一次鏈接,當有數據的時候再推送給瀏覽器,減小帶寬的浪費和cpu的使用。
WebSocket是html5新增長的一種通訊協議,目前流行的瀏覽器都支持這個協議,例如Chrome,Safari,Firefox,Opera,IE等等,對該協議支持最先的應該是chrome,從chrome12就已經開始支持,隨着協議草案的不斷變化,各個瀏覽器對協議的實現也在不停的更新。該協議仍是草案,沒有成爲標準,不過成爲標準應該只是時間問題了,從WebSocket草案的提出到如今已經有十幾個版本了,目前對該協議支持最完善的瀏覽器應該是chrome,畢竟WebSocket協議草案也是Google發佈的,下面咱們教程咱們使用springboot 集成 websocket 實現消息的一對一以及所有通知功能。javascript
本文使用spring boot 2.1.1+ jdk1.8 + idea。html
一:引入依賴html5
如何建立springboot項目本文再也不贅述,首先在建立好的項目pom.xml中引入以下依賴:java
<!--引入websocket依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>web<!--引入thymeleaf模板引擎依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>ajax
二:建立websocket配置類spring
ServerEndpointExporter 會自動註冊使用了@ServerEndpoint註解聲明的Websocket endpoint。要注意,若是使用獨立的servlet容器,而不是直接使用springboot的內置容器,就不要注入ServerEndpointExporter,由於它將由容器本身提供和管理。chrome
package com.sailing.websocket.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * @author baibing * @project: springboot-socket * @package: com.sailing.websocket.config * @Description: socket配置類,往 spring 容器中注入ServerEndpointExporter實例 * @date 2018/12/20 09:46 */ @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }
三:編寫websocket服務端代碼瀏覽器
package com.sailing.websocket.common; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; /** * @author baibing * @project: springboot-socket * @package: com.sailing.websocket.common * @Description: WebSocket服務端代碼,包含接收消息,推送消息等接口 * @date 2018/12/200948 */ @Component @ServerEndpoint(value = "/socket/{name}") public class WebSocketServer { //靜態變量,用來記錄當前在線鏈接數。應該把它設計成線程安全的。 private static AtomicInteger online = new AtomicInteger(); //concurrent包的線程安全Set,用來存放每一個客戶端對應的WebSocketServer對象。 private static Map<String,Session> sessionPools = new HashMap<>(); /** * 發送消息方法 * @param session 客戶端與socket創建的會話 * @param message 消息 * @throws IOException */ public void sendMessage(Session session, String message) throws IOException{ if(session != null){ session.getBasicRemote().sendText(message); } } /** * 鏈接創建成功調用 * @param session 客戶端與socket創建的會話 * @param userName 客戶端的userName */ @OnOpen public void onOpen(Session session, @PathParam(value = "name") String userName){ sessionPools.put(userName, session); addOnlineCount(); System.out.println(userName + "加入webSocket!當前人數爲" + online); try { sendMessage(session, "歡迎" + userName + "加入鏈接!"); } catch (IOException e) { e.printStackTrace(); } } /** * 關閉鏈接時調用 * @param userName 關閉鏈接的客戶端的姓名 */ @OnClose public void onClose(@PathParam(value = "name") String userName){ sessionPools.remove(userName); subOnlineCount(); System.out.println(userName + "斷開webSocket鏈接!當前人數爲" + online); } /** * 收到客戶端消息時觸發(羣發) * @param message * @throws IOException */ @OnMessage public void onMessage(String message) throws IOException{ for (Session session: sessionPools.values()) { try { sendMessage(session, message); } catch(Exception e){ e.printStackTrace(); continue; } } } /** * 發生錯誤時候 * @param session * @param throwable */ @OnError public void onError(Session session, Throwable throwable){ System.out.println("發生錯誤"); throwable.printStackTrace(); } /** * 給指定用戶發送消息 * @param userName 用戶名 * @param message 消息 * @throws IOException */ public void sendInfo(String userName, String message){ Session session = sessionPools.get(userName); try { sendMessage(session, message); }catch (Exception e){ e.printStackTrace(); } } public static void addOnlineCount(){ online.incrementAndGet(); } public static void subOnlineCount() { online.decrementAndGet(); } }
四:增長測試頁面路由配置類安全
若是爲每個頁面寫一個action太麻煩,spring boot 提供了頁面路由的統一配置,在 spring boot 2.0 之前的版本中咱們只須要繼承 WebMvcConfigurerAdapter ,並重寫它的 addViewControllers 方法便可,可是 2.0版本後 WebMvcConfigurerAdapter已經被廢棄,使用 WebMvcConfigurer 接口代替(其實WebMvcConfigurerAdapter也是實現了WebMvcConfigurer),因此咱們只須要實現它便可:
package com.sailing.websocket.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; /** * 在SpringBoot2.0及Spring 5.0 WebMvcConfigurerAdapter已被廢棄,目前找到解決方案就有 * 1 直接實現WebMvcConfigurer (官方推薦) * 2 直接繼承WebMvcConfigurationSupport * @ https://blog.csdn.net/lenkvin/article/details/79482205 */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { /** * 爲各個頁面提供路徑映射 * @param registry */ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/client").setViewName("client"); registry.addViewController("/index").setViewName("index"); } }
五:建立測試頁面
在 resources下面建立 templates 文件夾,編寫兩個測試頁面 index.html 和 client.html 和上面配置類中的viewName相對應,兩個頁面內容如出一轍,只是在鏈接websocket部分模擬的用戶名不同,一個叫 lucy 一個叫 lily :
<!DOCTYPE HTML> <html> <head> <title>WebSocket</title> </head> <body> Welcome<br/> <input id="text" type="text" /><button onclick="send()">Send</button> <button onclick="closeWebSocket()">Close</button> <div id="message"> </div> </body> <script type="text/javascript"> var websocket = null; //判斷當前瀏覽器是否支持WebSocket if('WebSocket' in window){ websocket = new WebSocket("ws://localhost:8080/socket/lucy"); }else{ alert('Not support websocket') } //鏈接發生錯誤的回調方法 websocket.onerror = function(){ setMessageInnerHTML("error"); }; //鏈接成功創建的回調方法 websocket.onopen = function(event){ setMessageInnerHTML("open"); } //接收到消息的回調方法 websocket.onmessage = function(event){ setMessageInnerHTML(event.data); } //鏈接關閉的回調方法 websocket.onclose = function(){ setMessageInnerHTML("close"); } //監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket鏈接,防止鏈接還沒斷開就關閉窗口,server端會拋異常。 window.onbeforeunload = function(){ websocket.close(); } //將消息顯示在網頁上 function setMessageInnerHTML(innerHTML){ document.getElementById('message').innerHTML += innerHTML + '<br/>'; } //關閉鏈接 function closeWebSocket(){ websocket.close(); } //發送消息 function send(){ var message = document.getElementById('text').value; websocket.send(message); } </script> </html>
<!DOCTYPE HTML> <html> <head> <title>WebSocket</title> </head> <body> Welcome<br/> <input id="text" type="text" /><button onclick="send()">Send</button> <button onclick="closeWebSocket()">Close</button> <div id="message"> </div> </body> <script type="text/javascript"> var websocket = null; //判斷當前瀏覽器是否支持WebSocket if('WebSocket' in window){ websocket = new WebSocket("ws://localhost:8080/socket/lily"); }else{ alert('Not support websocket') } //鏈接發生錯誤的回調方法 websocket.onerror = function(){ setMessageInnerHTML("error"); }; //鏈接成功創建的回調方法 websocket.onopen = function(event){ setMessageInnerHTML("open"); } //接收到消息的回調方法 websocket.onmessage = function(event){ setMessageInnerHTML(event.data); } //鏈接關閉的回調方法 websocket.onclose = function(){ setMessageInnerHTML("close"); } //監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket鏈接,防止鏈接還沒斷開就關閉窗口,server端會拋異常。 window.onbeforeunload = function(){ websocket.close(); } //將消息顯示在網頁上 function setMessageInnerHTML(innerHTML){ document.getElementById('message').innerHTML += innerHTML + '<br/>'; } //關閉鏈接 function closeWebSocket(){ websocket.close(); } //發送消息 function send(){ var message = document.getElementById('text').value; websocket.send(message); } </script> </html>
六:測試webscoket controller
package com.sailing.websocket.controller; import com.sailing.websocket.common.WebSocketServer; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.io.IOException; /** * @author baibing * @project: springboot-socket * @package: com.sailing.websocket.controller * @Description: websocket測試controller * @date 2018/12/20 10:11 */ @RestController public class SocketController { @Resource private WebSocketServer webSocketServer; /** * 給指定用戶推送消息 * @param userName 用戶名 * @param message 消息 * @throws IOException */ @RequestMapping(value = "/socket", method = RequestMethod.GET) public void testSocket1(@RequestParam String userName, @RequestParam String message){ webSocketServer.sendInfo(userName, message); } /** * 給全部用戶推送消息 * @param message 消息 * @throws IOException */ @RequestMapping(value = "/socket/all", method = RequestMethod.GET) public void testSocket2(@RequestParam String message){ try { webSocketServer.onMessage(message); } catch (IOException e) { e.printStackTrace(); } } }
七:測試
訪問 http://localhost:8080/index 和 http://localhost:8080/client 分別打開兩個頁面並鏈接到websocket,http://localhost:8080/socket?userName=lily&message=helloworld 給lily發送消息,http://localhost:8080/socket/all?message=LOL 給所有在線用戶發送消息: