隨着互聯網的發展,傳統的HTTP協議已經很難知足Web應用日益複雜的需求了。近年來,隨着HTML5的誕生,WebSocket協議被提出,它實現了瀏覽器與服務器的全雙工通訊,擴展了瀏覽器與服務端的通訊功能,使服務端也能主動向客戶端發送數據。
咱們知道,傳統的HTTP協議是無狀態的,每次請求(request)都要由客戶端(如 瀏覽器)主動發起,服務端進行處理後返回response結果,而服務端很難主動向客戶端發送數據;這種客戶端是主動方,服務端是被動方的傳統Web模式 對於信息變化不頻繁的Web應用來講形成的麻煩較小,而對於涉及實時信息的Web應用卻帶來了很大的不便,如帶有即時通訊、實時數據、訂閱推送等功能的應 用。在WebSocket規範提出以前,開發人員若要實現這些實時性較強的功能,常常會使用折衷的解決方法:輪詢(polling)和Comet技術。其實後者本質上也是一種輪詢,只不過有所改進。
輪詢是最原始的實現實時Web應用的解決方案。輪詢技術要求客戶端以設定的時間間隔週期性地向服務端發送請求,頻繁地查詢是否有新的數據改動。明顯地,這種方法會致使過多沒必要要的請求,浪費流量和服務器資源。
Comet技術又能夠分爲長輪詢和流技術。長輪詢改進了上述的輪詢技術,減少了無用的請求。它會爲某些數據設定過時時間,當數據過時後纔會向服務端發送請求;這種機制適合數據的改動不是特別頻繁的狀況。流技術一般是指客戶端使用一個隱藏的窗口與服務端創建一個HTTP長鏈接,服務端會不斷更新鏈接狀態以保持HTTP長鏈接存活;這樣的話,服務端就能夠經過這條長鏈接主動將數據發送給客戶端;流技術在大併發環境下,可能會考驗到服務端的性能。
這兩種技術都是基於請求-應答模式,都不算是真正意義上的實時技術;它們的每一次請求、應答,都浪費了必定流量在相同的頭部信息上,而且開發複雜度也較大。
伴隨着HTML5推出的WebSocket,真正實現了Web的實時通訊,使B/S模式具有了C/S模式的實時通訊能力。WebSocket的工做流程是這 樣的:瀏覽器經過JavaScript向服務端發出創建WebSocket鏈接的請求,在WebSocket鏈接創建成功後,客戶端和服務端就能夠經過 TCP鏈接傳輸數據。由於WebSocket鏈接本質上是TCP鏈接,不須要每次傳輸都帶上重複的頭部數據,因此它的數據傳輸量比輪詢和Comet技術小 了不少。本文不詳細地介紹WebSocket規範,主要介紹下WebSocket在Java Web中的實現。
JavaEE 7中出了JSR-356:Java API for WebSocket規範。很多Web容器,如Tomcat,Nginx,Jetty等都支持WebSocket。Tomcat從7.0.27開始支持 WebSocket,從7.0.47開始支持JSR-356,下面的Demo代碼也是須要部署在Tomcat7.0.47以上的版本才能運行。
WebSocket是HTML5開始提供的一種在單個 TCP 鏈接上進行全雙工通信的協議。
在WebSocket API中,瀏覽器和服務器只須要作一個握手的動做,而後,瀏覽器和服務器之間就造成了一條快速通道。二者之間就直接能夠數據互相傳送。
瀏覽器經過 JavaScript 向服務器發出創建 WebSocket 鏈接的請求,鏈接創建之後,客戶端和服務器端就能夠經過 TCP 鏈接直接交換數據。
當你獲取 Web Socket 鏈接後,你能夠經過 send() 方法來向服務器發送數據,並經過 onmessage 事件來接收服務器返回的數據。
如下 API 用於建立 WebSocket 對象。
var Socket = new WebSocket(url, [protocol] );
以上代碼中的第一個參數 url, 指定鏈接的 URL。第二個參數 protocol 是可選的,指定了可接受的子協議。javascript
經常使用的 Node 實現有如下三種。html
使用這種方式無需別的任何配置,只需服務端一個處理類前端
服務端java
/** * 服務器 * @ClassName: WebSocket * @Description: TODO * @author OnlyMate * @Date 2018年8月16日 下午2:46:54 * */ @ServerEndpoint("/webSocketByTomcat/{username}") public class WebSocket { private static int onlineCount = 0; private static Map<String, WebSocket> clients = new ConcurrentHashMap<String, WebSocket>(); private Session session; private String username; @OnOpen public void onOpen(@PathParam("username") String username, Session session) throws IOException { this.username = username; this.session = session; addOnlineCount(); clients.put(username, this); System.out.println("已鏈接"); } @OnClose public void onClose() throws IOException { clients.remove(username); subOnlineCount(); } @OnMessage public void onMessage(String message) throws IOException { JSONObject jsonTo = JSONObject.parseObject(message); System.out.println(jsonTo.getString("to") +":"+ jsonTo.getString("msg")); if (!jsonTo.getString("to").toLowerCase().equals("all")){ sendMessageTo(jsonTo.getString("msg"), jsonTo.getString("to")); }else{ sendMessageAll(jsonTo.getString("msg")); } } @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } public void sendMessageTo(String message, String To) throws IOException { // session.getBasicRemote().sendText(message); //session.getAsyncRemote().sendText(message); for (WebSocket item : clients.values()) { if (item.username.equals(To) ) item.session.getAsyncRemote().sendText(message); } } public void sendMessageAll(String message) throws IOException { for (WebSocket item : clients.values()) { item.session.getAsyncRemote().sendText(message); } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocket.onlineCount++; } public static synchronized void subOnlineCount() { WebSocket.onlineCount--; } public static synchronized Map<String, WebSocket> getClients() { return clients; } }
客戶端git
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> <c:set var="ctx" value="${pageContext.request.contextPath}" /> <c:set var="ctxpath" value="${pageContext.request.scheme}${'://'}${pageContext.request.serverName}${':'}${pageContext.request.serverPort}${pageContext.request.contextPath}" /> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta charset=UTF-8"> <title>登陸測試</title> </head> <body> <h2>Hello World!</h2> <div> <span>sessionId:</span> <% HttpSession s= request.getSession(); out.println(s.getId()); %> </div> <input id="sessionId" type="hidden" value="<%=session.getId() %>" /> <input id="text" type="text" /> <button onclick="send()">發送消息</button> <hr /> <button onclick="closeWebSocket()">關閉WebSocket鏈接</button> <hr /> <div id="message"></div> </body> <script type="text/javascript" src="http://localhost:8088/static/js/sockjs-0.3.min.js"></script> <script type="text/javascript"> var websocket = null; if('WebSocket' in window) { websocket = new WebSocket("ws://localhost:8088/websocket/webSocketByTomcat/"+document.getElementById('sessionId').value); } else if('MozWebSocket' in window) { websocket = new MozWebSocket("ws://localhost:8088/websocket/webSocketByTomcat/"+document.getElementById('sessionId').value); } else { websocket = new SockJS("localhost:8088/websocket/webSocketByTomcat/"+document.getElementById('sessionId').value); } //鏈接發生錯誤的回調方法 websocket.onerror = function () { setMessageInnerHTML("WebSocket鏈接發生錯誤"); }; //鏈接成功創建的回調方法 websocket.onopen = function () { setMessageInnerHTML("WebSocket鏈接成功"); } //接收到消息的回調方法 websocket.onmessage = function (event) { setMessageInnerHTML(event.data); } //鏈接關閉的回調方法 websocket.onclose = function () { setMessageInnerHTML("WebSocket鏈接關閉"); } //監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket鏈接,防止鏈接還沒斷開就關閉窗口,server端會拋異常。 window.onbeforeunload = function () { closeWebSocket(); } //將消息顯示在網頁上 function setMessageInnerHTML(innerHTML) { document.getElementById('message').innerHTML += innerHTML + '<br/>'; } //關閉WebSocket鏈接 function closeWebSocket() { websocket.close(); } //發送消息 function send() { var message = document.getElementById('text').value; websocket.send(message); } </script> </html>
注意導入socketjs時要使用地址全稱,而且鏈接使用的是http而不是websocket的wsgithub
服務端如何向客戶端推送消息呢?web
代碼以下spring
/** * 服務端推送消息對客戶端 * @ClassName: ServiceClientController * @Description: TODO * @author OnlyMate * @Date 2018年8月16日 下午2:45:22 * */ @Controller @RequestMapping(value="webSocketByTomcat/serviceToClient") public class ServiceClientByTomcatController { private WebSocket websocket = new WebSocket(); @RequestMapping public void sendMsg(HttpServletRequest request, HttpServletResponse response) throws IOException { JSONObject json = new JSONObject(); json.put("to", request.getSession().getId()); json.put("msg", "歡迎鏈接WebSocket!!!!"); websocket.onMessage(json.toJSONString()); } }
springboot對websocket支持很友好,只須要繼承webSocketHandler類,重寫幾個方法就能夠了json
這個類是對消息的一些處理,好比是發給一我的,仍是發給全部人,而且前端鏈接時觸發的一些動做跨域
/** * 建立一個WebSocket server * * @ClassName: CustomWebSocketHandler * @Description: TODO * @author OnlyMate * @Date 2018年8月16日 下午3:17:34 * */ @Service public class CustomWebSocketHandler extends TextWebSocketHandler implements WebSocketHandler { private Logger logger = LoggerFactory.getLogger(CustomWebSocketHandler.class); // 在線用戶列表 private static final Map<String, WebSocketSession> users; // 用戶標識 private static final String CLIENT_ID = "mchNo"; static { users = new HashMap<>(); } @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { logger.info("成功創建websocket-spring鏈接"); String mchNo = getMchNo(session); if (StringUtils.isNotEmpty(mchNo)) { users.put(mchNo, session); session.sendMessage(new TextMessage("成功創建websocket-spring鏈接")); logger.info("用戶標識:{},Session:{}", mchNo, session.toString()); } } @Override public void handleTextMessage(WebSocketSession session, TextMessage message) { logger.info("收到客戶端消息:{}", message.getPayload()); JSONObject msgJson = JSONObject.parseObject(message.getPayload()); String to = msgJson.getString("to"); String msg = msgJson.getString("msg"); WebSocketMessage<?> webSocketMessageServer = new TextMessage("server:" +message); try { session.sendMessage(webSocketMessageServer); if("all".equals(to.toLowerCase())) { sendMessageToAllUsers(new TextMessage(getMchNo(session) + ":" +msg)); }else { sendMessageToUser(to, new TextMessage(getMchNo(session) + ":" +msg)); } } catch (IOException e) { logger.info("handleTextMessage method error:{}", e); } } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { if (session.isOpen()) { session.close(); } logger.info("鏈接出錯"); users.remove(getMchNo(session)); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { logger.info("鏈接已關閉:" + status); users.remove(getMchNo(session)); } @Override public boolean supportsPartialMessages() { return false; } public void sendMessage(String jsonData) { logger.info("收到客戶端消息sendMessage:{}", jsonData); JSONObject msgJson = JSONObject.parseObject(jsonData); String mchNo = StringUtils.isEmpty(msgJson.getString(CLIENT_ID)) ? "陌生人" : msgJson.getString(CLIENT_ID); String to = msgJson.getString("to"); String msg = msgJson.getString("msg"); if("all".equals(to.toLowerCase())) { sendMessageToAllUsers(new TextMessage(mchNo + ":" +msg)); }else { sendMessageToUser(to, new TextMessage(mchNo + ":" +msg)); } } /** * 發送信息給指定用戶 * @Title: sendMessageToUser * @Description: TODO * @Date 2018年8月21日 上午11:01:08 * @author OnlyMate * @param mchNo * @param message * @return */ public boolean sendMessageToUser(String mchNo, TextMessage message) { if (users.get(mchNo) == null) return false; WebSocketSession session = users.get(mchNo); logger.info("sendMessage:{} ,msg:{}", session, message.getPayload()); if (!session.isOpen()) { logger.info("客戶端:{},已斷開鏈接,發送消息失敗", mchNo); return false; } try { session.sendMessage(message); } catch (IOException e) { logger.info("sendMessageToUser method error:{}", e); return false; } return true; } /** * 廣播信息 * @Title: sendMessageToAllUsers * @Description: TODO * @Date 2018年8月21日 上午11:01:14 * @author OnlyMate * @param message * @return */ public boolean sendMessageToAllUsers(TextMessage message) { boolean allSendSuccess = true; Set<String> mchNos = users.keySet(); WebSocketSession session = null; for (String mchNo : mchNos) { try { session = users.get(mchNo); if (session.isOpen()) { session.sendMessage(message); }else { logger.info("客戶端:{},已斷開鏈接,發送消息失敗", mchNo); } } catch (IOException e) { logger.info("sendMessageToAllUsers method error:{}", e); allSendSuccess = false; } } return allSendSuccess; } /** * 獲取用戶標識 * @Title: getMchNo * @Description: TODO * @Date 2018年8月21日 上午11:01:01 * @author OnlyMate * @param session * @return */ private String getMchNo(WebSocketSession session) { try { String mchNo = session.getAttributes().get(CLIENT_ID).toString(); return mchNo; } catch (Exception e) { return null; } } }
這個類的做用就是在鏈接成功前和成功後增長一些額外的功能
咱們但願可以把websocketSession和httpsession對應起來,這樣就能根據當前不一樣的session,定向對websocketSession進行數據返回;在查詢資料以後,發現spring中有一個攔截器接口,HandshakeInterceptor,能夠實現這個接口,來攔截握手過程,向其中添加屬性
/** * WebSocket握手時的攔截器 * @ClassName: CustomWebSocketInterceptor * @Description: TODO * @author OnlyMate * @Date 2018年8月16日 下午3:17:04 * */ public class CustomWebSocketInterceptor implements HandshakeInterceptor { private Logger logger = LoggerFactory.getLogger(CustomWebSocketInterceptor.class); /** * 關聯HeepSession和WebSocketSession, * beforeHandShake方法中的Map參數 就是對應websocketSession裏的屬性 */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map<String, Object> map) throws Exception { if (request instanceof ServletServerHttpRequest) { logger.info("*****beforeHandshake******"); HttpServletRequest httpServletRequest = ((ServletServerHttpRequest) request).getServletRequest(); HttpSession session = httpServletRequest.getSession(true); logger.info("mchNo:{}", httpServletRequest.getParameter("mchNo")); if (session != null) { map.put("sessionId",session.getId()); map.put("mchNo", httpServletRequest.getParameter("mchNo")); } } return true; } @Override public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) { logger.info("******afterHandshake******"); } }
這個類是配置類向Spring中注入handler
/** * websocket的配置類 * @ClassName: CustomWebSocketConfig * @Description: TODO * @author OnlyMate * @Date 2018年8月16日 下午3:17:26 * */ @Configuration @EnableWebSocket public class CustomWebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(customWebSocketHandler(), "/webSocketBySpring/customWebSocketHandler").addInterceptors(new CustomWebSocketInterceptor()).setAllowedOrigins("*"); registry.addHandler(customWebSocketHandler(), "/sockjs/webSocketBySpring/customWebSocketHandler").addInterceptors(new CustomWebSocketInterceptor()).setAllowedOrigins("*").withSockJS(); } @Bean public WebSocketHandler customWebSocketHandler() { return new CustomWebSocketHandler(); } }
補充說明:
setAllowedOrigins("*")必定要加上,否則只有訪問localhost,其餘的不予許訪問
setAllowedOrigins(String[] domains),容許指定的域名或IP(含端口號)創建長鏈接,若是隻容許自家域名訪問,這裏輕鬆設置。若是不限時使用"*"號,若是指定了域名,則必需要以http或https開頭
經查閱官方文檔springwebsocket 4.1.5版本前默認支持跨域訪問,以後的版本默認不支持跨域,須要設置
使用withSockJS()的緣由:
一些瀏覽器中缺乏對WebSocket的支持,所以,回退選項是必要的,而Spring框架提供了基於SockJS協議的透明的回退選項。
SockJS的一大好處在於提供了瀏覽器兼容性。優先使用原生WebSocket,若是在不支持websocket的瀏覽器中,會自動降爲輪詢的方式。
除此以外,spring也對socketJS提供了支持。
若是代碼中添加了withSockJS()以下,服務器也會自動降級爲輪詢。
registry.addEndpoint("/coordination").withSockJS();
SockJS的目標是讓應用程序使用WebSocket API,但在運行時須要在必要時返回到非WebSocket替代,即無需更改應用程序代碼。
SockJS是爲在瀏覽器中使用而設計的。它使用各類各樣的技術支持普遍的瀏覽器版本。對於SockJS傳輸類型和瀏覽器的完整列表,能夠看到SockJS客戶端頁面。
傳輸分爲3類:WebSocket、HTTP流和HTTP長輪詢(按優秀選擇的順序分爲3類)
客戶端
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> <c:set var="ctx" value="${pageContext.request.contextPath}" /> <c:set var="ctxpath" value="${pageContext.request.scheme}${'://'}${pageContext.request.serverName}${':'}${pageContext.request.serverPort}${pageContext.request.contextPath}" /> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta charset=UTF-8"> <title>登陸測試</title> </head> <body> <h2>Hello World! Web Socket by Spring</h2> <div> <span>sessionId:</span> <% HttpSession s= request.getSession(); out.println(s.getId()); %> </div> <input id="sessionId" type="hidden" value="<%=session.getId() %>" /> <input id="text" type="text" /> <button onclick="send()">發送消息</button> <hr /> <button onclick="closeWebSocket()">關閉WebSocket鏈接</button> <hr /> <div id="message"></div> </body> <script type="text/javascript" src="http://localhost:8088/static/js/sockjs-0.3.min.js"></script> <script type="text/javascript"> var websocket = null; //判斷當前瀏覽器是否支持WebSocket //判斷當前瀏覽器是否支持WebSocket if('WebSocket' in window) { websocket = new WebSocket("ws://localhost:8088/websocket/webSocketBySpring/customWebSocketHandler?mchNo="+ 123); } else if('MozWebSocket' in window) { websocket = new MozWebSocket("ws://localhost:8088/websocket/webSocketBySpring/customWebSocketHandler?mchNo="+ 123); } else { websocket = new SockJS("http://localhost:8088/websocket/sockjs/webSocketBySpring/customWebSocketHandler?mchNo="+ 123); } //鏈接發生錯誤的回調方法 websocket.onerror = function () { setMessageInnerHTML("WebSocket鏈接發生錯誤"); }; //鏈接成功創建的回調方法 websocket.onopen = function () { setMessageInnerHTML("WebSocket鏈接成功"); } //接收到消息的回調方法 websocket.onmessage = function (event) { setMessageInnerHTML(event.data); } //鏈接關閉的回調方法 websocket.onclose = function () { setMessageInnerHTML("WebSocket鏈接關閉"); } //監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket鏈接,防止鏈接還沒斷開就關閉窗口,server端會拋異常。 window.onbeforeunload = function () { closeWebSocket(); } //將消息顯示在網頁上 function setMessageInnerHTML(innerHTML) { document.getElementById('message').innerHTML += innerHTML + '<br/>'; } //關閉WebSocket鏈接 function closeWebSocket() { websocket.close(); } //發送消息 function send() { var message = document.getElementById('text').value; websocket.send(message); } </script> </html>
完畢