1.1 服務器推送
WebSocket作為一種通信協議,屬於服務器推送技術的一種,IE10+支持。
服務器推送技術不止一種,有短輪詢、長輪詢、WebSocket、Server-sent Events(SSE)等,他們各有優缺點:
# | 短輪詢 | 長輪詢 | Websocket | sse |
---|---|---|---|---|
通訊方式 | http | http | 基於TCP長連接通訊 | http |
觸發方式 | 輪詢 | 輪詢 | 事件 | 事件 |
優點 | 兼容性好容錯性強,實現簡單 | 比短輪詢節約資源 | 全雙工通訊協議,性能開銷小、安全性高,有一定可擴展性 | 實現簡便,開發成本低 |
缺點 | 安全性差,占較多的內存資源與請求數 | 安全性差,占較多的內存資源與請求數 | 傳輸數據需要進行二次解析,增加開發成本及難度 | 只適用高級瀏覽器 |
適用范圍 | b/s服務 | b/s服務 | 網絡游戲、銀行交互和支付 | 服務端到客戶端單向推送 |
短輪詢最簡單,在一些簡單的場景也會經常使用,就是隔一段時間就發起一個ajax請求。那么長輪詢是什么呢?
長輪詢(Long Polling)是在Ajax輪詢基礎上做的一些改進,在沒有更新的時候不再返回空響應,而且把連接保持到有更新的時候,客戶端向服務器發送Ajax請求,服務器接到請求后hold住連接,直到有新消息才返回響應信息並關閉連接,客戶端處理完響應信息后再向服務器發送新的請求。它是一個解決方案,但不是最佳的技術方案。
如果說短輪詢是客戶端不斷打電話問服務端有沒有消息,服務端回復后立刻掛斷,等待下次再打;長輪詢是客戶端一直打電話,服務端接到電話不掛斷,有消息的時候再回復客戶端並掛斷。
SSE(Server-Sent Events)與長輪詢機制類似,區別是每個連接不只發送一個消息。客戶端發送一個請求,服務端保持這個連接直到有新消息發送回客戶端,仍然保持着連接,這樣連接就可以支持消息的再次發送,由服務器單向發送給客戶端。然而IE直到11都不支持,不多說了....
1.2 WebSocket的特點
為什么已經有了輪詢還要WebSocket呢,是因為短輪詢和長輪詢有個缺陷:通信只能由客戶端發起。
那么如果后端想往前端推送消息需要前端去輪詢,不斷查詢后端是否有新消息,而輪詢的效率低且浪費資源(必須不停 setInterval 或 setTimeout 去連接,或者 HTTP 連接始終打開),WebSocket提供了一個文明優雅的全雙工通信方案。一般適合於對數據的實時性要求比較強的場景,如通信、股票、直播、共享桌面,特別適合於客戶端與服務頻繁交互的情況下,如聊天室、實時共享、多人協作等平台。
特點
- 建立在 TCP 協議之上,服務器端的實現比較容易。
- 與 HTTP 協議有着良好的兼容性。默認端口也是80和443,並且握手階段采用 HTTP 協議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務器。
- 數據格式比較輕量,性能開銷小,通信高效。服務器與客戶端之間交換的標頭信息大概只有2字節;
- 可以發送文本,也可以發送二進制數據。
- 沒有同源限制,客戶端可以與任意服務器通信。
- 協議標識符是
ws
(如果加密,則為wss),服務器網址就是 URL。ex:ws://example.com:80/some/path
- 不用頻繁創建及銷毀TCP請求,減少網絡帶寬資源的占用,同時也節省服務器資源;
- WebSocket是純事件驅動的,一旦連接建立,通過監聽事件可以處理到來的數據和改變的連接狀態,數據都以幀序列的形式傳輸。服務端發送數據后,消息和事件會異步到達。
- 無超時處理。
HTTP與WS協議結構
WebSocket協議標識符用ws
表示。`wss協議表示加密的WebSocket協議,對應HTTPs協議。結構如下:
- HTTP: TCP > HTTP
- HTTPS: TCP > TLS > HTTP
- WS: TCP > WS
- WSS: TCP > TLS > WS
以下是簡單代碼記錄,親測可用,基於springMVC的服務器推送技術websocket。
1.gradle構建,websocket引入的包:"org.springframework:spring-websocket:4.2.4.RELEASE"
2.context.xml配置需要加的地方
xmlns:websocket="http://www.springframework.org/schema/websocket"
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd
3.websocket的配置類
4.websocke接受消息以及處理的類
5.頁面js代碼
6.簡單的運行效果
涉及到的代碼就這幾個,拷貝到springmvc項目里就可以。

1 package com.kitty.activity.common.websocket; 2 3 import org.springframework.stereotype.Service; 4 import org.springframework.web.socket.CloseStatus; 5 import org.springframework.web.socket.TextMessage; 6 import org.springframework.web.socket.WebSocketMessage; 7 import org.springframework.web.socket.WebSocketSession; 8 import org.springframework.web.socket.handler.TextWebSocketHandler; 9 10 import java.io.IOException; 11 import java.util.HashMap; 12 import java.util.Map; 13 import java.util.Set; 14 15 /** 16 * Created by liuxn on 2018/5/22 0022. 17 */ 18 @Service 19 public class MyHandler extends TextWebSocketHandler { 20 //在線用戶列表 21 private static final Map<Integer, WebSocketSession> users; 22 //用戶標識 23 private static final String CLIENT_ID = "userId"; 24 25 static { 26 users = new HashMap<>(); 27 } 28 29 @Override 30 public void afterConnectionEstablished(WebSocketSession session) throws Exception { 31 System.out.println("成功建立連接"); 32 Integer userId = getClientId(session); 33 if (userId != null) { 34 users.put(userId, session); 35 session.sendMessage(new TextMessage("你已成功建立socket連接")); 36 System.out.println(userId); 37 System.out.println(session); 38 } 39 } 40 41 @Override 42 public void handleTextMessage(WebSocketSession session, TextMessage message) { 43 // ... 44 System.out.println("收到客戶端消息:"+message.getPayload()); 45 46 WebSocketMessage message1 = new TextMessage("server:"+message); 47 try { 48 session.sendMessage(message1); 49 } catch (IOException e) { 50 e.printStackTrace(); 51 } 52 } 53 54 @Override 55 public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { 56 if (session.isOpen()) { 57 session.close(); 58 } 59 System.out.println("連接出錯"); 60 users.remove(getClientId(session)); 61 } 62 63 @Override 64 public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { 65 System.out.println("連接已關閉:" + status); 66 users.remove(getClientId(session)); 67 } 68 69 /** 70 * 發送信息給指定用戶 71 * @param clientId 72 * @param message 73 * @return 74 */ 75 public boolean sendMessageToUser(Integer clientId, TextMessage message) { 76 if (users.get(clientId) == null) return false; 77 WebSocketSession session = users.get(clientId); 78 System.out.println("sendMessage:" + session+",msg:"+message.getPayload()); 79 if (!session.isOpen()) return false; 80 try { 81 session.sendMessage(message); 82 } catch (IOException e) { 83 e.printStackTrace(); 84 return false; 85 } 86 return true; 87 } 88 89 /** 90 * 廣播信息 91 * @param message 92 * @return 93 */ 94 public boolean sendMessageToAllUsers(TextMessage message) { 95 boolean allSendSuccess = true; 96 Set<Integer> clientIds = users.keySet(); 97 WebSocketSession session = null; 98 for (Integer clientId : clientIds) { 99 try { 100 session = users.get(clientId); 101 if (session.isOpen()) { 102 session.sendMessage(message); 103 } 104 } catch (IOException e) { 105 e.printStackTrace(); 106 allSendSuccess = false; 107 } 108 } 109 110 return allSendSuccess; 111 } 112 113 @Override 114 public boolean supportsPartialMessages() { 115 return false; 116 } 117 118 /** 119 * 獲取用戶標識 120 * @param session 121 * @return 122 */ 123 private Integer getClientId(WebSocketSession session) { 124 try { 125 Integer clientId = (Integer) session.getAttributes().get(CLIENT_ID); 126 return clientId; 127 } catch (Exception e) { 128 return null; 129 } 130 } 131 }

1 package com.kitty.activity.common.websocket; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.stereotype.Controller; 5 import org.springframework.web.bind.annotation.PathVariable; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 import org.springframework.web.bind.annotation.ResponseBody; 8 import org.springframework.web.servlet.ModelAndView; 9 import org.springframework.web.socket.TextMessage; 10 11 import javax.servlet.http.HttpSession; 12 13 14 @Controller 15 public class SocketController { 16 17 @Autowired 18 MyHandler handler; 19 20 //玩家登錄 21 @RequestMapping("/external/login/{userId}") 22 public ModelAndView login(HttpSession session, @PathVariable("userId") Integer userId) { 23 System.out.println("登錄接口,userId=" + userId); 24 session.setAttribute("userId", userId); 25 System.out.println(session.getAttribute("userId")); 26 27 return new ModelAndView("phone/websocket_test"); 28 } 29 30 //模擬給指定玩家發消息 31 @RequestMapping("/external/message") 32 @ResponseBody 33 public String sendMessage(Integer userId,String message) { 34 boolean hasSend = handler.sendMessageToUser(userId, new TextMessage(message)); 35 System.out.println(hasSend); 36 return "success"; 37 } 38 39 40 //模擬給所有玩家發消息 41 @RequestMapping("/external/message/all") 42 @ResponseBody 43 public String sendAll(String message) { 44 boolean hasSend = handler.sendMessageToAllUsers(new TextMessage(message)); 45 System.out.println(hasSend); 46 return "success"; 47 } 48 49 }

1 package com.kitty.activity.common.websocket; 2 3 import org.springframework.context.annotation.Bean; 4 import org.springframework.context.annotation.Configuration; 5 import org.springframework.web.socket.WebSocketHandler; 6 import org.springframework.web.socket.config.annotation.EnableWebSocket; 7 import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 8 import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 9 10 11 @Configuration 12 @EnableWebSocket 13 public class WebSocketConfig implements WebSocketConfigurer { 14 15 @Override 16 public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 17 registry.addHandler(myHandler(), "/external/myHandler").addInterceptors(new WebSocketInterceptor()); 18 } 19 20 @Bean 21 public WebSocketHandler myHandler() { 22 return new MyHandler(); 23 } 24 }

1 package com.kitty.activity.common.websocket; 2 3 import org.springframework.http.server.ServerHttpRequest; 4 import org.springframework.http.server.ServerHttpResponse; 5 import org.springframework.http.server.ServletServerHttpRequest; 6 import org.springframework.web.socket.WebSocketHandler; 7 import org.springframework.web.socket.server.HandshakeInterceptor; 8 9 import javax.servlet.http.HttpSession; 10 import java.util.Map; 11 12 /** 13 * 14 */ 15 public class WebSocketInterceptor implements HandshakeInterceptor { 16 17 @Override 18 public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map<String, Object> map) throws Exception { 19 if (request instanceof ServletServerHttpRequest) { 20 System.out.println("*****beforeHandshake******"); 21 ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request; 22 HttpSession session = serverHttpRequest.getServletRequest().getSession(); 23 // Map parameterMap = serverHttpRequest.getServletRequest().getParameterMap(); 24 // System.out.println(parameterMap); 25 if (session != null) { 26 map.put("userId", session.getAttribute("userId")); 27 } 28 29 } 30 return true; 31 } 32 33 @Override 34 public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) { 35 System.out.println("******afterHandshake******"); 36 } 37 }

1 <%@ page contentType="text/html;charset=UTF-8" language="java" %> 2 <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> 4 <c:set var="ctx" value="${pageContext.request.contextPath}"/> 5 <html> 6 <head> 7 <meta charset="utf-8"> 8 <meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport"> 9 <meta content="yes" name="apple-mobile-web-app-capable"> 10 <meta content="black" name="apple-mobile-web-app-status-bar-style"> 11 <meta content="telephone=no" name="format-detection"> 12 <meta content="email=no" name="format-detection"> 13 <meta name="baseUrl" content="${ctx}"/> 14 <meta name="roleId" content="${roleId}"/> 15 <title>websocket</title> 16 <script src="${ctx}/assets/js/jquery.min.js"></script> 17 <script src="http://cdn.jsdelivr.net/sockjs/1.0.1/sockjs.min.js"></script> 18 19 </head> 20 21 <body> 22 <div class="act-outline" width="100%"> 23 it is work. 24 </div> 25 <script> 26 //websocket 27 $(function () { 28 var baseUrl = $('meta[name="baseUrl"]').attr("content"); 29 //判斷當前瀏覽器是否支持WebSocket 30 var websocket; 31 if ('WebSocket' in window) { 32 websocket = new WebSocket('ws://' + window.location.host + '/activity/external/myHandler'); 33 // var ws = new WebSocket('ws://192.168.3.26:8999/activity/external/myHandler') //也可以指定ip 34 } else if ('MozWebSocket' in window) { 35 websocket = new MozWebSocket("ws://" + window.location.host + '/activity/external/myHandler'); //未測試 36 } else { 37 websocket = new SockJS("http://" + window.location.host + '/activity/external/myHandler'); //未測試 38 } 39 40 websocket.onopen = function () { 41 console.log("正在打開連接,准備發消息給服務器..."); 42 websocket.send("{text:hello}"); 43 } 44 websocket.onclose = function () { 45 console.log("服務器關閉連接:onclose"); 46 } 47 48 websocket.onmessage = function (msg) { 49 console.log("收到服務器推送數據:"+msg.data); 50 } 51 52 53 }) 54 </script> 55 </body> 56 </html>
備注:http://ip:port/activity 是項目根目錄。
參考鏈接地址:
https://blog.csdn.net/u014520745/article/details/62046396
https://www.cnblogs.com/interdrp/p/7903736.html