【开发笔记】spring + websocket 实现服务端推送
Title: spring + websocket 实现服务端推送消息(附几个坑)
背景
原先采用ajax轮询
的方式,去更新推送信息。但是如果你目标的数据对于一次请求来说,只是一个很小的值,那采用轮询的方式就会很浪费带宽。
现在尝试用websocket实现产生推送信息时实时推送给前端。
Spring 4.0的一个最大更新是增加了websocket的支持。websocket提供了一个在web应用中的高效、双向的通讯,需要考虑到客户端(浏览器)和服务器之间的高频和低延时消息交换。一般的应用场景有:在线交易、游戏、协作、数据可视化等。
使用websocket需要考虑的浏览器的支持(IE<10不支持),目前主流的浏览器都能很好的支持websocket。
websocket协议中有一些子协议,可以从更高的层次实现编程模型,就像我们使用HTTP而不是TCP一样。这些子协议有STOMP,WAMP等。
基础知识
websocket与http
WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)
首先HTTP有 1.1 和 1.0 之说,也就是所谓的 keep-alive ,把多个HTTP请求合并为一个,但是 Websocket 其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充。
有交集,但是并不是全部。
另外Html5是指的一系列新的API,或者说新规范,新技术。Http协议本身只有1.0和1.1,而且跟Html本身没有直接关系,通俗来说,你可以用HTTP协议传输非Html数据。
websocket
HTTP的生命周期通过 Request
来界定,也就是一个 Request
一个 Response
,那么在 HTTP1.0 中,这次HTTP请求就结束了。
在HTTP1.1中进行了改进,使得有一个keep-alive
,也就是说,在一个HTTP连接中,可以发送多个Request
,接收多个Response
。但是请记住 Request = Response
, 在HTTP中永远是这样,也就是说一个request
只能有一个response
。而且这个response
也是被动的,不能主动发起。
说了这么多,再来看下websocket。
首先Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手。
首先我们来看个典型的 Websocket 握手(借用Wikipedia的。。)
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的童鞋可能发现了,这段类似HTTP协议的握手请求中,多了几个东西。我会顺便讲解下作用。
Upgrade: websocket
Connection: Upgrade
这个就是Websocket的核心了,告诉 Apache 、 Nginx 等服务器:注意啦,我发起的是Websocket协议,快点帮我找到对应的助理处理~不是那个老土的HTTP。
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
首先,Sec-WebSocket-Key
是一个 Base64 encode 的值,这个是浏览器随机生成的,告诉服务器:泥煤,不要忽悠窝,我要验证尼是不是真的是Websocket助理。
然后,Sec_WebSocket-Protocol
是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
最后, Sec-WebSocket-Version
是告诉服务器所使用的 Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。。不过现在还好,已经定下来啦~大家都使用的一个东西。
然后服务器会返回下列东西,表示已经接受到请求,成功建立Websocket啦!
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~
`Upgrade: websocket
Connection: Upgrade`
依然是固定的,告诉客户端即将升级的是 Websocket 协议,而不是mozillasocket,lurnarsocket或者shitsocket。
然后, Sec-WebSocket-Accept
这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key
。 服务器:好啦好啦,知道啦,给你看我的ID CARD来证明行了吧。。
后面的, Sec-WebSocket-Protocol
则是表示最终使用的协议。
至此,HTTP已经完成它所有工作了,接下来就是完全按照Websocket协议进行了。具体的协议就不在这阐述了。
实战
服务端
maven 配置
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
拦截器实现
/**
* @Author: Richard_yyf
* @Description:
* @Date: Created in 15:28 2018/2/24
*/
public class MyInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler webSocketHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
// 获取请求参数,首先我们要获取HttpServletRequest对象才能获取请求参数;当ServerHttpRequset的层次结构打开后其子类可以获取到我们想要的http对象,那么就简单了。
// 我这里是把获取的请求数据绑定到session的map对象中(attributes)
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
// 标记用户
String uid = servletRequest.getParameter("uid");
attributes.put("uid", uid);
if(uid!=null){
attributes.put("uid", uid);
}else{
return false;
}
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
}
}
Handler处理类实现
/**
* @Author: Richard_yyf
* @Description:
* @Date: Created in 15:31 2018/2/24
*/
public class MySocketHandler implements WebSocketHandler {
private static final Logger logger = Logger.getLogger(MySocketHandler.class);
// websocket连接建立
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String uid = (String) session.getHandshakeAttributes().get("uid");
logger.info("Websocket Connection established");
logger.info(String.format("userId:%s", uid));
logger.info(String.format("getLocalAddress:%s", session.getLocalAddress().toString()));
logger.info(String.format("getUri:%s", session.getUri().toString()));
session.sendMessage(new TextMessage("Server:connected OK!"));
if (SocketSessionMap.socketSession.get(uid) == null) {
SocketSessionMap.socketSession.put(uid, session);
}
}
// 消息处理
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
}
// 消息传输错误
@Override
public void handleTransportError(WebSocketSession session, Throwable throwable) throws Exception {
logger.error("Websocket Transport Error");
if (session.isOpen()) {
session.close();
}
removeSession(session);
}
// websocket 连接关闭
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
// TODO Auto-generated method stub
logger.info(String.format("Websocket:[userId=%s]已经关闭",(String) session.getHandshakeAttributes().get("uid")));
removeSession(session);
}
// 移除Socket会话
private void removeSession(WebSocketSession session) {
Iterator<Map.Entry<String, WebSocketSession>> iter = SocketSessionMap.socketSession.entrySet().iterator();
while (iter.hasNext()) {
ConcurrentHashMap.Entry entry = iter.next();
if (entry.getValue() == session) {
SocketSessionMap.socketSession.remove(entry.getKey());
logger.info(String.format("Socket会话已经移除:用户ID:%s", entry.getKey()));
}
}
}
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 给某个用户发送消息
*
* @param uid
* @param message
* @throws IOException
*/
public void sendMessageToUser(String uid, TextMessage message)
throws IOException {
WebSocketSession session = SocketSessionMap.socketSession.get(uid);
if (session != null && session.isOpen()) {
session.sendMessage(message);
}
}
}
配置类实现
/**
* @Author: Richard_yyf
* @Description:
* @Date: Created in 15:24 2018/2/24
*/
@Configuration
@EnableWebMvc
@EnableWebSocket
public class MySocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
@Autowired
private MySocketHandler mySocketHandler;
@Autowired
private MyInterceptor myInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(mySocketHandler, "/websocket")
.addInterceptors(myInterceptor);
}
}
springmvc 配置
扫包
<context:component-scan base-package="com.fingard.rh.msp.socket" />
这里使用的是xml,也可以使用注解配置,具体就不细讲了。
<bean id="mySocketHandler" class="com.fingard.rh.msp.socket.MySocketHandler"/>
<!-- 握手接口/拦截器 -->
<bean id="myInterceptor" class="com.fingard.rh.msp.socket.MyInterceptor"/>
<websocket:handlers >
<websocket:mapping path="/websocket" handler="mySocketHandler"/>
<websocket:handshake-interceptors>
<ref bean="myInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>
dispatcherServlet配置
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/websocket</url-pattern>
</servlet-mapping>
<servlet>
前端(Client)
//webScoket连接事件
function socket() {
try{
var websocket;
var url = "ws://host:port/项目名/websocket";
var uid = $("#userId").val();
if ('WebSocket' in window) {
// websocket = new WebSocket(url+"?uid="+uid);
websocket = new ReconnectingWebSocket(url+"?uid="+uid);
} else { // 如果程序不支持,使用轮询的方式
// url = $("#sockjs").val();
// websocket = new SockJS(url+"?uid="+uid);
timerRefresh();
}
websocket.onopen = function(event) {
console.log("WebSocket:已连接");
console.log(event);
};
websocket.onmessage = function(event) {
console.log("WebSocket:收到一条消息",event.data);
show("您有新的消息!!!");
setTimeout("closeDiv()",2000); // 最好设置成可以自己点掉的
loadMail();
};
websocket.onerror = function(event) {
console.log("WebSocket:发生错误 ");
console.log(event);
};
websocket.onclose = function(event) {
console.log("WebSocket:已关闭");
console.log(event);
}
}catch(e){
}
}
注:使用reconnecting-websocket.js
来实现websocket重连。只用把new WebSocket()
替换成new ReconnectingWebSocket()
即可。
几个坑
1、 跨域问题
经查阅官方文档springwebsocket 4.1.5版本前默认支持跨域访问,之后的版本默认不支持跨域,需要设置
.setAllowedOrigins("*")
我demo的版本是默认支持跨域的,setAllowedOrigins()
这个方法也是之后版本才有的。
2、 版本问题
Spring websocket 版本要和 Spring版本一致,不然会报错
3、 sockjs问题
sockjs是当浏览器不支持websocket的一种兼容性方案(轮询),但是在使用的时候有很多问题,比如在建立握手协议的时候会自动在原有的url上面加上/info
的后缀和t
的携带参数。这里我在浏览器不支持websocket时(少数情况),自己写了一个轮询。等之后调出来了sockjs,再添加内容吧。