一、现状分析
目前,我们的项目中还是有很多场景用得到消息推送,目前大都采用的是Ajax轮询、dwr、amq等方式,这些本质上其实都是http请求轮询,而这种方式是比较占用宽带且容易给服务器造成压力的。
WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:
- WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动的向对方发送或接收数据,就像 Socket 一样。
- WebSocket 需要类似 TCP 的客户端和服务器端通过握手连接,连接成功后才能相互通信。
那么传统的http轮回模式和websocket模式客户端与服务器的交互如下:
上图对比可以看出,相对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket 是类似 Socket 的 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。在客户端断开 WebSocket 连接或 Server 端断掉连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。
早期 HTML5 并没有形成业界统一的规范,各个浏览器和应用服务器厂商有着各异的类似实现,如 IBM 的 MQTT,Comet 开源框架等,直到 2014 年,HTML5 在 IBM、微软、Google 等巨头的推动和协作下终于尘埃落地,正式从草案落实为实际标准规范,各个应用服务器及浏览器厂商逐步开始统一,在 JavaEE7 中也实现了 WebSocket 协议,从而无论是客户端还是服务端的 WebSocket 都已完备。
二、Websocket支持
WebSocket 的实现分为客户端和服务端两部分,客户端(通常为浏览器)发出 WebSocket 连接请求,服务端响应,实现类似 TCP 握手的动作,从而在浏览器客户端和 WebSocket 服务端之间形成一条 HTTP 长连接快速通道。两者之间后续进行直接的数据互相传送,不再需要发起连接和相应。
WebSocket 服务端
WebSocket 服务端在各个主流应用服务器厂商中已基本获得符合 JEE JSR356 标准规范 API 的支持,以下列举了部分常见的商用及开源应用服务器对 WebSocket Server 端的支持情况:
厂商 | 应用服务器 | 备注 |
---|---|---|
IBM | WebSphere | WebSphere 8.0 以上版本支持,7.X 之前版本结合 MQTT 支持类似的 HTTP 长连接 |
甲骨文 | WebLogic | WebLogic 12c 支持,11g 及 10g 版本通过 HTTP Publish 支持类似的 HTTP 长连接 |
微软 | IIS | IIS 7.0+支持 |
Apache | Tomcat | Tomcat 7.0.5+支持,7.0.2X 及 7.0.3X 通过自定义 API 支持 |
Apache | Jetty | Jetty 7.0+支持 |
WebSocket 客户端
对于 WebSocket 客户端,主流的浏览器(包括 PC 和移动终端)现已都支持标准的 HTML5 的 WebSocket API,这意味着客户端的 WebSocket JavaScirpt 脚本具备良好的一致性和跨平台特性,以下列举了常见的浏览器厂商对 WebSocket 的支持情况:
厂商 | 版本支持 |
---|---|
Chrome | Chrome version 4+支持 |
Firefox | Firefox version 5+支持 |
IE | IE version 10+支持 |
Safari | IOS 5+支持 |
Android Brower | Android 4.5+支持 |
三、 Websocket实现
客户端实现
客户端 WebSocket API 基本上已经在各个主流浏览器厂商中实现了统一,因此使用标准 HTML5 定义的 WebSocket 客户端的 JavaScript API 即可,Demo实例如下:
<!DOCTYPE html>
<html>
<head>
<title>Testing websockets</title>
</head>
<body>
<div>
<input type="submit" value="Start" onclick="start()" />
</div>
<div id="messages"></div>
<script type="text/javascript">
var webSocket =
new WebSocket('ws://localhost:8088/byteslounge/websocket');
webSocket.onerror = function(event) {//发生异常的处理
alert(event.data);
};
webSocket.onopen = function(event) {//建立连接的处理
document.getElementById('messages').innerHTML = 'Connection established';
};
webSocket.onmessage = function(event) {//接收到消息的处理
document.getElementById('messages').innerHTML + '<br />' + event.data;
};
function start() {//页面发送消息给服务器
webSocket.send('hello');
return false;
}
</script>
</body>
</html>
服务端实现
服务端的实现可以分为两种:J2EE的websocket和Spring提供的websocket支持。下面分别介绍下两种实现方式的代码:
J2EE的实现
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint(value = "/websocket")
public class WebSocketTest {
public static CopyOnWriteArraySet<Session> sessions = new CopyOnWriteArraySet<Session>();
@OnMessage
public void onMessage(String message, Session session) throws IOException, InterruptedException {
System.out.println("Got your message (" + message + ").Thanks !");
System.out.println("Received: " + message);
for(Session s: WebSocketTest.sessions){
if(message!=null&&!"".equals(message)){
System.out.println("批量发送:"+message);
s.getBasicRemote().sendText(message);
}
}
}
@OnOpen
public void onOpen(Session session) {
sessions.add(session);
}
@OnClose
public void onClose (Session session) {
sessions.remove(session);
}
@OnError
public void onError(Session session) {
sessions.remove(session);
}
}
- 只要对一个类进行@ServerEndpoint注解,既可以标示该类为一个websocket服务。我们可以选择实现onMessage、onOpen等方法进行相应的消息处理。
- 这里的CopyOnWriteArraySet是为了收集所有的连接,以便我们在推送消息时能够发送到所有的客户端。
- 环境:JDK1.7+,tomcat7.0.5+,本例中的环境:JDK1.8,tomcat7.0.68
- 相关依赖:这里是为了编译而添加的jar包,最好2个jar包都添加上
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
Spring websocket的实现
需要实现三个类,这里简单介绍下每个类的作用及一些注意事项:
1.WebSocketConfig.java
@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
/**
* 支持websocket 的 connection
*/
registry.addHandler(new WebSocketHander(),"/websocket").addInterceptors(new HandshakeInterceptor());
/**
* 不支持websocket的connenction,采用sockjs
*/
registry.addHandler(new WebSocketHander(),"/sockjs/websocket").addInterceptors(new HandshakeInterceptor()).withSockJS();
}
}
@Configuration @EnableWebMvc @EnableWebSocket这三个大致意思是使这个类支持以@Bean的方式加载bean,并且支持springmvc和websocket,不是很准确大致这样,试了一下@EnableWebMvc不加也没什么影响,@Configuration本来就支持springmvc的自动扫描。
另外,使用sockjs的方式这里不多讨论。
2.HandshakeInterceptor.java
import java.util.Map;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
public class HandshakeInterceptor implements org.springframework.web.socket.server.HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
if(request.getHeaders().containsKey("Sec-WebSocket-Extensions")) {
request.getHeaders().set("Sec-WebSocket-Extensions", "permessage-deflate");
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
}
}
这个类的作用可以理解为拦截器,在websocket的方法握手前后的处理,这里我并没有实现什么业务,我们可以在这里取一些用户信息等操作
3.WebSocketHander.java
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
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 WebSocketHander implements WebSocketHandler {
public static CopyOnWriteArraySet<WebSocketSession> sessions = new CopyOnWriteArraySet<WebSocketSession>();
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
}
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
TextMessage message = new TextMessage(webSocketMessage.getPayload() + "hello");
for (WebSocketSession user : sessions) {
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
if(webSocketSession.isOpen()){
webSocketSession.close();
}
sessions.remove(webSocketSession);
}
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
sessions.remove(webSocketSession);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
- 我们可以看到继承了WebsocketHandle之后,其方法类型与J2EE很类似,分别为建立连接,关闭连接,接收消息等。
- 如果只考虑Websocket不考虑socketJs,只需添加SpringWebsocket相关依赖即可:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
效果图
我们在IE11和chrome下分别打开页面,无论是在chrome中发送消息还是在IE中发送消息,两边都能接收到服务器推送的消息。
四、常见错误
- spring security与WebSocket结合使用时报302循环重定向错误。spring security的使用
原因:springsecurity的过滤器DelegatingFilterProxy拦截了我们的ws请求,我们可以在自定义的CsrfSecurityRequestMatcher中添加不过滤我们的请求:
- 在使用struts2时,服务器连接不上。
原因:struts2的拦截器拦截掉了我们的请求,可以添加:
<constant name="struts.action.excludePattern" value="/websocket"></constant>
如果还是有问题的话,就需要对struts2的拦截器拦截范围做一定限制。
一般很多问题都是由于ws的请求被拦截,所以需要注意自己项目中的拦截器定义,并仔细检查是不是被拦截掉了。