之所以sockjs會存在,說得不好聽點,就是因為微軟是個流氓,現在使用windows 7的系統仍然有近半,而windows 7默認自帶的是ie 8,有些會自動更新到ie 9,但是大部分非IT用戶其實都不願意或者不會升級(通常我們做IT的認為很簡單的事情,在其他行業的人來看,那就是天書,不要覺得不可能,現實已如此)。
現在言歸正傳,這里完整的講下在spring 4.x集成sockjs,以及運行在tomcat 7下時的一些額外注意事項。
spring websocket依賴jar:
<dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> <version>1.1</version> <scope>provided</scope> <!-- 注意,scope必須為provided,否則runtime會沖突,如果使用tomcat 8,還需要將TOMCAT_HOME/lib下的javax.websocket-api.jar一並刪除 --> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>4.2.8.RELEASE</version> </dependency>
除非使用STOMP協議,否則不需要依賴spring-messaging。
spring通過兩種模式支持websocket,一種是通過原生websocket規范的ws://協議訪問(個人認為如果確定只用標准websocket訪問,還不如tomcat升級到8.x(tomcat 8原生支持JSR 356注解),spring的大量封裝畢竟增加了不少額外負載);另一種則是通過sockjs(也就是js)訪問,兩者目前暫時無法做到兼容。
先完整說明第一種:
1、搭建spring mvc環境,這一點假設讀者已知;
2、pom.xml中引入上面兩個jar包;
3、spring支持websocket總共分為四個小步驟,handler、interceptor、config、web.xml,基本上可以認為spring mvc的翻版。
3.1、創建WebSocketHandler,spring支持兩種方式,一種是實現org.springframework.web.socket.WebSocketHandler接口,另外一種則是繼承TextWebSocketHandler或BinaryWebSocketHandler(現在大部分模板式框架或者插件通常都是在提供了API的基礎上提供了抽象類,把一些能統一的工作提前預置了,以便應用只需要關心業務,我們自己公司的中間件框架很多也是用這個模式實現了)。
/** * */ package com.ld.net.spider.demo.ws; /** * @author zhjh256@163.com * {@link} http://www.cnblogs.com/zhjh256 */ import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketMessage; import org.springframework.web.socket.WebSocketSession; public class DemoWSHandler implements WebSocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { System.out.println("connect to the websocket success......"); session.sendMessage(new TextMessage("Server:connected OK!")); } @Override public void handleMessage(WebSocketSession wss, WebSocketMessage<?> wsm) throws Exception { TextMessage returnMessage = new TextMessage(wsm.getPayload() + " received at server"); System.out.println(wss.getHandshakeHeaders().getFirst("Cookie")); wss.sendMessage(returnMessage); } @Override public void handleTransportError(WebSocketSession wss, Throwable thrwbl) throws Exception { if(wss.isOpen()){ wss.close(); } System.out.println("websocket connection closed......"); } @Override public void afterConnectionClosed(WebSocketSession wss, CloseStatus cs) throws Exception { System.out.println("websocket connection closed......"); } @Override public boolean supportsPartialMessages() { return false; } }
3.2、創建攔截器
/** * */ package com.ld.net.spider.demo.ws; import java.util.Map; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; /** * @author zhjh256@163.com * {@link} http://www.cnblogs.com/zhjh256 */ public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { // 解決The extension [x-webkit-deflate-frame] is not supported問題 if (request.getHeaders().containsKey("Sec-WebSocket-Extensions")) { request.getHeaders().set("Sec-WebSocket-Extensions", "permessage-deflate"); } System.out.println("Before Handshake"); return super.beforeHandshake(request, response, wsHandler, attributes); } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) { System.out.println("After Handshake"); super.afterHandshake(request, response, wsHandler, ex); } }
3.3、bean配置
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:mongo="http://www.springframework.org/schema/data/mongo" xmlns:cache="http://www.springframework.org/schema/cache" xmlns:c="http://www.springframework.org/schema/c" xmlns:amq="http://activemq.apache.org/schema/core" xmlns:websocket="http://www.springframework.org/schema/websocket" xmlns:jms="http://www.springframework.org/schema/jms" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/data/mongo http://www.springframework.org/schema/data/mongo/spring-mongo-1.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <websocket:handlers allowed-origins="*"> <websocket:mapping path="/springws/websocket.ws" handler="demoWSHandler"/> <websocket:handshake-interceptors> <bean class="com.ld.net.spider.demo.ws.HandshakeInterceptor"/> </websocket:handshake-interceptors> </websocket:handlers> <bean id="demoWSHandler" class="com.ld.net.spider.demo.ws.DemoWSHandler"/>
上述需要注意的是,1、spring javadoc的說明是默認情況下,允許所有來源訪問,但我們跑下來發現不配置allowed-origins的話總是報403錯誤。
2、sockjs是不允許有后綴的,否則將無法匹配,后面會專門講到。
3.4、web.xml配置
在web.xml中增加*.ws映射即可(如果原來不是/*的話),如下:
<servlet-mapping> <servlet-name>springMVC</servlet-name> <url-pattern>*.ws</url-pattern> </servlet-mapping>
上述配置完成之后,就可以通過標准的websocket接口進行訪問了,如下所示。
4、websocket客戶端
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Web Socket JavaScript Echo Client</title> <script src="http://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script> <script language="javascript" type="text/javascript"> var echo_websocket; function init() { output = document.getElementById("output"); } function send_echo() { var wsUri = "ws://localhost:28080/springws/websocket.ws"; writeToScreen("Connecting to " + wsUri); echo_websocket = new WebSocket(wsUri); echo_websocket.onopen = function (evt) { writeToScreen("Connected !"); doSend(textID.value); }; echo_websocket.onmessage = function (evt) { writeToScreen("Received message: " + evt.data); echo_websocket.close(); }; echo_websocket.onerror = function (evt) { writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data); echo_websocket.close(); }; } function doSend(message) { echo_websocket.send(message); writeToScreen("Sent message: " + message); } function writeToScreen(message) { var pre = document.createElement("p"); pre.style.wordWrap = "break-word"; pre.innerHTML = message; output.appendChild(pre); } window.addEventListener("load", init, false); </script> </head> <body> <h1>Echo Server</h1> <div style="text-align: left;"> <form action=""> <input onclick="send_echo()" value="發送socket請求" type="button"> <input id="textID" name="message" value="Hello World, Web Sockets" type="text"> <br> </form> </div> <div id="output"></div> </body> </html>
上述前后端均配置完成后,基於標准websocket api的搭建就完成了,試試吧。。
現在再來看下sockjs的配置。
spring對sockjs和websocket支持的差別在於配置,web.xml,以及客戶端,服務實現無差別。
3.3需要調整為如下:
<websocket:handlers> <websocket:mapping path="/springws/websocket" handler="demoWSHandler"/> <websocket:handshake-interceptors> <bean class="com.ld.net.spider.demo.ws.HandshakeInterceptor"/> </websocket:handshake-interceptors> <websocket:sockjs/> </websocket:handlers> <bean id="demoWSHandler" class="com.ld.net.spider.demo.ws.DemoWSHandler"/>
3.4 一定要有到/xxx/*的映射,簡單的可以直接/*,如下所示:
<servlet-mapping> <servlet-name>springMVC</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping>
上述配置完成后,就sockjs直接性的支持而言,就可以沒有問題了。
客戶端則為如下:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Web Socket JavaScript Echo Client</title> <script src="http://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script> <script language="javascript" type="text/javascript"> var echo_websocket; function init() { output = document.getElementById("output"); } function send_echo() { echo_websocket = new SockJS("http://localhost:28080/springws/websocket") ; //初始化 websocket echo_websocket.onopen = function () { console.log('Info: connection opened.'); }; echo_websocket.onmessage = function (event) { console.log('Received: ' + event.data); //處理服務端返回消息 }; echo_websocket.onclose = function (event) { console.log('Info: connection closed.'); console.log(event); }; ws.send("abcabc"); } function doSend(message) { echo_websocket.send(message); writeToScreen("Sent message: " + message); } function writeToScreen(message) { var pre = document.createElement("p"); pre.style.wordWrap = "break-word"; pre.innerHTML = message; output.appendChild(pre); } window.addEventListener("load", init, false); </script> </head> <body> <h1>Echo Server</h1> <div style="text-align: left;"> <form action=""> <input onclick="send_echo()" value="send websocket request" type="button"> <input id="textID" name="message" value="Hello world, Web Sockets" type="text"> <br> </form> </div> <div id="output"></div> </body> </html>
上述配置完成后,如果訪問沒有CORS異常的話,基於sockjs的websocket就完成了。試試吧。。。
典型錯誤及原因、解決方法如下:
Error during WebSocket handshake: Unexpected response code: 404
檢查web.xml servlet-mapping包含了到websocket路徑的映射,比如如果請求不含后綴,就必須包含/*的映射
WebSocket connection to 'ws://localhost:8080/springwebsocket/websocket' failed: Error during WebSocket handshake: Unexpected response code: 403
<websocket:handlers allowed-origins="*">,javadoc說明默認代表所有站點,實際好像並不是,所以需要配置*
sockjs啟用
啟用sockjs后,直接用websocket協議訪問會報
html5ws.html:15 WebSocket connection to 'ws://localhost:28080/springws/websocket.ws' failed: Error during WebSocket handshake: Unexpected response code: 200
直接改為sockjs后,會報
XMLHttpRequest cannot load http://localhost:28080/springws/websocket.ws/info?t=1478758042205. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access. The response had HTTP status code 404.
需要在web.xml中配置CORS過濾(注意,如果apache有自帶的類庫,建議直接使用,不要隨意聽信網上的自己實現過濾器的搞法,這些庫一天的運行次數可能就比自己寫的運行到淘汰還多,所以幾乎常見的問題都不可能遺漏):
<filter> <filter-name>CorsFilter</filter-name> <filter-class>org.apache.catalina.filters.CorsFilter</filter-class> <init-param> <param-name>cors.allowed.methods</param-name> <param-value>GET,POST,HEAD,OPTIONS,PUT</param-value> </init-param> <init-param> <param-name>cors.allowed.headers</param-name> <!--注意,若你的應用中不只有這些文件頭,則需要將你應用中需要傳的文件頭也加上; 例如:我的應用中需要在header中傳token,所以這里的值就應該是下面的配置,在原有基礎上將token加上,否則,應用就不會被允許調用 <param-value>token,Access-Control-Allow-Origin,Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers</param-value> --> <param-value>Access-Control-Allow-Origin,Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers</param-value> </init-param> <async-supported>true</async-supported> </filter> <filter-mapping> <filter-name>CorsFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
使用sockjs還有一點需要注意的是:
因為sockjs會自動在url之后增加/info?t=XXXX等路徑,如果這里url-pattern攔截類似於*.ws這種帶后綴的就找不到映射,比如想通過sockjs訪問地址/springws/websocket.ws,但是sockjs框架會先訪問/springws/websocket.ws/info這個地址,但是這個地址又不可被spring框架識別,所以導致不可用。
到此為止,tomcat 7下spring 4.x mvc集成websocket以及sockjs的配置就全部介紹完成。
今天看群里一個消息的時候,提到HA時一台服務器掛掉的問題,這就回到socket的思路了,客戶端也得加上個定時的心跳邏輯,萬一某台服務器掛了或者斷網可以failover並自動重新建立連接。在我們的業務中,可靠性這一點是很關鍵的。
默認情況下,ws://走的時候http協議,即使主頁面是通過https訪問,此時會出現連接時異常"[blocked] The page at 'https://localhost:8443/endpoint-wss/index.jsp' was loaded over HTTPS, but ran insecure content from 'ws://localhost:8080/endpoint-wss/websocket': this content should also be loaded over HTTPS.Uncaught SecurityError: Failed to construct 'WebSocket': An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.",此時需要使用如下連接:
websocket:wss://localhost:8080/endpoint-wss/websocket
sockJS:https://localhost:8080/endpoint-wss/socketJS
配置nginx支持websocket,默認情況下,nginx不支持自動升級至websocket協議,否則js中會出現連接時異常"Error during WebSocket handshake: Unexpected response code: 400",需在恰當的位置加上如下設置:
server { listen 8020; location / { proxy_pass http://websocket;
proxy_set_header Host $host:8020; #注意, 原host必須配置, 否則傳遞給后台的值是websocket,端口如果沒有輸入的話會是80, 這會導致連接失敗 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; } }
upstream websocket { server 192.168.100.10:8081; }
經上述調整后,websocket就可以同時支持通過nginx代理的https協議,結合MQ機制,可以做到B端實時推送、B端/C端實時通信。
nginx的https(自動跳轉http->https)+nginx+websocket的完整配置可參考http://www.cnblogs.com/zhjh256/p/6262620.html。