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;
}