一、 背景:
现在实时web消息推送一般会用到websocket,但是由于此技术并没有推广开来,所以各浏览器对其支持也不同,例如下图显示了各类浏览器的支持情况。
粉红色区域表示不支持Websocket。
至于IE浏览器,以及部分陈旧的桌面浏览器,可以选择Flashsocket作为替代品。
客户端如何把Websocket和Flashsocket结合在一起使用,可借鉴开源项目:web-socket-js (客户端的Websocket实现方案)
二、解决方法
Spring 4.0的一个最大更新是增加了websocket的支持。websocket提供了一个在web应用中的高效、双向的通讯,需要考虑到客户端(浏览器)和服务器之间的高频和低延时消息交换。一般的应用场景有:在线交易、游戏、协作、数据可视化等。同时Spring WebSocket API提供了SockJS的支持,SockJs是一个脚本框架,它可以通过提供类似websocket的编程模式以适应不同的浏览器(包括不支持websocket的浏览器)。针对不同的浏览器环境,传输的协议可以是Http Streaming,long polling等。
三、简单配置和应用
下面简单的搭建一个Spring Websocket+SockJS的web项目,实现在线实时聊天的功能。
web.xml配置:
<web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
其中: version 和 web-app_3_1.xsd 这个两个版本都必须是3.0+。
然后在这个servlet和所有的filter中加入
<async-supported>true</async-supported>
即让程序支持异步运行。
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
pom.xml中添加相关依赖:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.3.3</version> </dependency>
在dispatcher-servlet.xml中添加对注册服务类的扫描:
<context:component-scan base-package="com.milanosoft.RCS.web.webSocket.config" />
<context:component-scan base-package="com.milanosoft.RCS.web.webSocket.hndler">
</context:component-scan>
<context:component-scan base-package="com.milanosoft.RCS.web.webSocket.interceptor">
</context:component-scan>
使用WebSocketConfigurer集中注册WebSocket服务:
package com.milanosoft.RCS.web.webSocket.config;
import com.milanosoft.RCS.web.webSocket.hndler.SystemWebSocketHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import com.milanosoft.RCS.web.webSocket.interceptor.HandshakeInterceptor;
import org.springframework.context.annotation.Bean;
@Configuration
@EnableWebMvc
@EnableWebSocket
//以@Bean的方式加载bean,并且支持springmvc和websocket
public class WebSocketConfig extends WebMvcConfigurerAdapter implements
WebSocketConfigurer {
public WebSocketConfig() {
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//提供符合W3C标准的Websocket数据,用来注册websocket server实现类,第二个参数是访问websocket的地址
registry.addHandler(systemWebSocketHandler(), "/websck").addInterceptors(new HandshakeInterceptor()).setAllowedOrigins("*");
System.out.println("registed!");
//提供符合Sockjs的数据
//.setAllowedOrigins("*")解决跨域问题
registry.addHandler(systemWebSocketHandler(), "/sockjs/websck").addInterceptors(new HandshakeInterceptor()).setAllowedOrigins("*")
.withSockJS().setClientLibraryUrl(null);
}
//websocket的实现类
@Bean
public WebSocketHandler systemWebSocketHandler() {
//return new InfoSocketEndPoint();
return new SystemWebSocketHandler();
}
}
@Configuration
@EnableWebMvc
@EnableWebSocket
这三个大致意思是使这个类支持以@Bean的方式加载bean,并且支持springmvc和websocket。
Spring WebSocket API的核心接口是WebSocketHandler,以下就是处理消息的实现类。包括client与服务端的Connect、Dieconnect和Message的处理
package com.milanosoft.RCS.web.webSocket.hndler;
import java.io.IOException;
import java.util.ArrayList;
import org.springframework.stereotype.Component;
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;
@Component
public class SystemWebSocketHandler implements WebSocketHandler {
private static final ArrayList<WebSocketSession> users;
static {
users = new ArrayList<>();
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("connect to the websocket success......");
System.out.println("connect user"+session.getAttributes().get("user"));
users.add(session);
session.sendMessage(new TextMessage("Server:connected OK!"));
}
//可以根据业务传入在WebSocketMessage<?>传入实体类。
@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"));
sendMessageToUsers(returnMessage);
//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;
}
/**
* 给所有在线用户发送消息
*
* @param message
*/
public void sendMessageToUsers(TextMessage message) {
for (WebSocketSession user : users) {
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 给某个用户发送消息
*
* @param userName
* @param message
*/
public void sendMessageToUser(String userName, TextMessage message) {
for (WebSocketSession user : users) {
if (user.getAttributes().get("user").equals(userName)) {
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
break;
}
}
}
}
监听握手操作:(在握手时可以将连接的用户信息添加到内存中,以此来统计在线人数)。
package com.milanosoft.RCS.web.webSocket.interceptor;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
@Component
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");
}
//return true;
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession(false);
if (session != null) {
//使用userName区分WebSocketHandler,以便定向发送消息
String userName = (String) session.getAttribute("user");
//attributes.put("username", userName);
}
}
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);
}
}
四、其他问题
针对于内存泄漏问题,我采用的是Spring原生的IntrospectorCleanupListener的监听器,在web 应用关闭的时候释放与掉这个web 应用相关的class loader 。
参考文章:
http://blog.csdn.net/hzzhoushaoyu/article/details/49407835
http://blog.csdn.net/linlzk/article/details/50153745
http://blog.csdn.net/sl543001/article/details/19343005
http://blog.csdn.net/yingxiake/article/details/51224569
https://my.oschina.net/ldl123292/blog/304360
http://www.cnblogs.com/davidwang456/p/5321413.html
官方文档:
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html
websocket API:
基于MessageBroker的项目
http://www.jianshu.com/p/60799f1356c5
http://blog.csdn.net/xjyzxx/article/details/24182677
http://blog.csdn.net/pete_lee/article/details/51453352