Bootstrap

SpringMVC结合WebSocket项目开发问题总结

webScoket:基于 TCP 协议,协议名为”ws” “wss” ,建立连接需要握手,客户端(浏览器)首先向服务器(web server)发起一条特殊的http请求,web server解析后生成应答到浏览器,这样子一个websocket连接就建立了,直到某一方关闭连接.
客户端
建立 WebSocket 连接时要发送一个 header 标记了 Upgrade 的 HTTP 请求,表示请求协议升级
浏览器发起websockt连接请求

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

服务端
服务端在现有的 HTTP 服务器软件和现有的端口上实现 WebSocket 协议,重用现有代码(比如解析和认证这个 HTTP 请求),然后再回一个状态码为 101 的 HTTP 响应完成握手,之后的数据传输与http无关

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

部分代码如下:

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Resource;
import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.SpringConfigurator;

import com.alibaba.fastjson.JSONObject;
import com.figure.mysight.cache.DataCache;
import com.figure.mysight.model.Member;
import com.figure.mysight.service.IMemberService;
import com.figure.mysight.util.JsonUtil;
import com.figure.mysight.util.StringUtil;
import com.figure.mysight.vo.UserInfo;

import lombok.Getter;
import lombok.Setter;

/**
 * 类似Servlet的注解mapping。无需在web.xml中配置。 
 * value="/pushServer" 不能为空,否则报错
 * configurator = SpringConfigurator.class是为了使该类可以通过Spring注入。
 */
@ServerEndpoint(value = "/pushServer", configurator = SpringConfigurator.class)
public class WebsocketEndpoint {
    @Resource
    private IUserService userService;
    private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<String, Session>();
    public WebsocketEndpoint() {

    }
    @Setter @Getter
    private Session session;

    @OnOpen
    public void onOpen(Session session) {
        System.out.println("onOpen:::id=" + session.getId() + "的用户开始连接到服务器");
        sessionMap.put(session.getId(), session);
        this.session = session;
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            System.out.println("onMessage:::id=" + session.getId() + "的用户发来数据[" + message + "]");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        System.out.println("onClose:::id=" + session.getId() + "的用户已经关闭连接,原因:" + closeReason);
        sessionMap.remove(session.getId());
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        System.out.println("onError:::id=" + session.getId() + "的用户,连接出错,原因:" + throwable);
        sessionMap.remove(session.getId());
    }

    /**
     * @param message
     */
    public void broadcastAll(String type, String message) {
        Set<Map.Entry<String, Session>> set = sessionMap.entrySet();
        for (Map.Entry<String, Session> i : set) {
            try {
                i.getValue().getBasicRemote().sendText(packData(type, message));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void boardSingle(String type, List<?> datas, Session session) {
        try {
            if (session.isOpen()) {
                if (datas != null && !datas.isEmpty()) {
                    session.getBasicRemote().sendText(packData(type, JsonUtil.toJson(datas)));
                } else {
                    session.getBasicRemote().sendText("");
                }
            }
        } catch (Exception e) {

        }
    }

    public void boardSingle(String type, List<?> datas) {
        try {
            if (session.isOpen()) {
                if (datas != null && !datas.isEmpty()) {
                    session.getBasicRemote().sendText(packData(type, JsonUtil.toJson(datas)));
                } else {
                    session.getBasicRemote().sendText("");
                }
            }
        } catch (Exception e) {

        }
    }

    public void boardListToSingle(String type, Object datas, Session session) {
        try {
            if (session.isOpen()) {
                if (datas != null) {
                    session.getBasicRemote().sendText(packData(type, JSONObject.toJSON(datas).toString()));
                } else {
                    session.getBasicRemote().sendText("");
                }
            }
        } catch (Exception e) {

        }
    }

    private String packData(String type, String data) {
        return "{type:'" + type + "',data:'" + data + "'}";
    }
}

问题1:启动过程中出现以下错误

ERROR [http-nio-9090-exec-2] - Failed to find the root WebApplicationContext. Was ContextLoaderListener not used?
29-Mar-2017 15:44:49.808 严重 [http-nio-9090-exec-2] org.apache.coyote.AbstractProtocol$ConnectionHandler.process Error reading request, ignored
 java.lang.IllegalStateException: Failed to find the root WebApplicationContext. Was ContextLoaderListener not used?
    at org.springframework.web.socket.server.standard.SpringConfigurator.getEndpointInstance(SpringConfigurator.java:68)
    at org.apache.tomcat.websocket.pojo.PojoEndpointServer.onOpen(PojoEndpointServer.java:44)
    at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.init(WsHttpUpgradeHandler.java:133)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:813)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1347)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)

原因是:SpringConfigurator上通过ContextLoader来获取SpringContext的,所以在web.xml文件中需要配置ContextLoaderListerner

<listener>  
 <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
</listener>

问题2:加入上面的配置后,重新启动服务,再次报错:

 org.springframework.beans.factory.BeanDefinitionStoreException: IOException parsing XML document from ServletContext resource [/WEB-INF/applicationContext.xml]; nested exception is java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/applicationContext.xml]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:344)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:304)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:181)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:217)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:188)
    at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:125)
    at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:94)
    at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:129)
    at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:613)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:514)
    at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:444)
    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:326)
    at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:107)
    at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4717)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5179)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1419)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1409)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/applicationContext.xml]
    at org.springframework.web.context.support.ServletContextResource.getInputStream(ServletContextResource.java:141)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:330)
    ... 21 more

原因:ContextLoaderListerner需要一个root context,默认会去加载/WEB-INF/applicationContext.xml,

<servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 配置Spring mvc下的配置文件的位置和名称 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/applicationContext.xml</param-value>
        </init-param> 
        <load-on-startup>1</load-on-startup>
    </servlet>

在servlet之间配置的不能被ContextLoaderListerner加载,但servlet可以加载parent context 或者 root context
解决方法:在web.xml中增加以下配置,在servlet的contextConfigLocation可以去掉

<context-param>
 <param-name>contextConfigLocation</param-name>
 <param-value>classpath:spring/applicationContext.xml</param-value>
</context-param>

问题3:在部署的时候,使用nginx反向代理,导致webscoket无法连接,
解决方法:在nginx.conf中加入以下配置:

    #websocket配置
    location ~* /pushServer* {
        proxy_pass http://server;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 300s;
    }
;