• tomcat 7下spring 4.x mvc集成websocket以及sockjs完全参考指南(含nginx/https支持)


    之所以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。
  • 相关阅读:
    PAT 1010. 一元多项式求导 (25)
    PAT 1009. 说反话 (20) JAVA
    PAT 1009. 说反话 (20)
    PAT 1007. 素数对猜想 (20)
    POJ 2752 Seek the Name, Seek the Fame KMP
    POJ 2406 Power Strings KMP
    ZOJ3811 Untrusted Patrol
    Codeforces Round #265 (Div. 2) 题解
    Topcoder SRM632 DIV2 解题报告
    Topcoder SRM631 DIV2 解题报告
  • 原文地址:https://www.cnblogs.com/zhjh256/p/6052102.html
Copyright © 2020-2023  润新知