WebSocket是H5的新特性,用于客户端和服务端的实时消息交互,例如聊天室。有些WEB容器的新版本也对websocket提供了支持。例如,resin4以上版本,tomcat8以上版本。但是如果你不想升级你的Web容器版本,也可以实现,就是spring4以上版本支持websocket。
一、先从web.xml配置文件说起,在web.xml里面需要做的事情就是让websocket请求走springmvc的过滤器。配置如下:
<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:springmvc-default.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>*.htm</url-pattern><!-- 这是普通的请求 -->
<url-pattern>/ws/*</url-pattern><!-- 这是websocket的请求 -->
</servlet-mapping>
二、在applicationContext.xml中声明websocket处理Bean,配置如下:
在头部添加xmlns:websocket="http://www.springframework.org/schema/websocket"
和
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd
配置
<bean id="websocket" class="cn.remotejob.websocket.WebSocketEndPoint"/>
<websocket:handlers allowed-origins="*">
<websocket:mapping path="/websocket.ws" handler="websocket"/>
<websocket:handshake-interceptors>
<bean class="cn.remotejob.websocket.HandShakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>
其中,WebSocketEndPoint和HandShakeInterceptor是自己写的处理类和拦截器。先把这两个类的代码贴出来吧!
package cn.remotejob.websocket;
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.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import cn.remotejob.constant.Constant;
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");
}
System.out.println("Before Handshake");
if(request instanceof ServletServerHttpRequest){
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession(false);
if(session != null){
attributes.put(Constant.WEBSOCKET_SESSIONID, session.getId());
}
}
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);
}
}
package cn.remotejob.websocket;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.task.TaskExecutor;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import cn.remotejob.constant.Constant;
public class WebSocketEndPoint extends TextWebSocketHandler {
private static Logger logger = LoggerFactory.getLogger(WebSocketEndPoint.class);
private static final List<WebSocketSession> WEB_SOCKET_SESSIONS = new ArrayList<WebSocketSession>();
@Resource
private TaskExecutor taskExecutor;
@Override
protected void handleTextMessage(WebSocketSession session,
TextMessage message) throws Exception {
session.sendMessage(message);
// sendAll(message);
}
@Override
public void afterConnectionEstablished(WebSocketSession session)
throws Exception {
logger.info("connect to the websocket success......"+session.getAttributes().get(Constant.WEBSOCKET_SESSIONID));
WEB_SOCKET_SESSIONS.add(session);
session.sendMessage(new TextMessage("Server:connected OK!"));
}
@Override
public void handleTransportError(WebSocketSession session,
Throwable exception) throws Exception {
if (session.isOpen()) {
session.close();
}
logger.info("websocket connection closed......"+session.getAttributes().get(Constant.WEBSOCKET_SESSIONID));
WEB_SOCKET_SESSIONS.remove(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session,
CloseStatus status) throws Exception {
logger.info("websocket connection closed......"+session.getAttributes().get(Constant.WEBSOCKET_SESSIONID));
WEB_SOCKET_SESSIONS.remove(session);
}
/**
* 给所有在线用户发送消息
* @param message
*/
public void sendAll(final TextMessage message){
for(final WebSocketSession session : WEB_SOCKET_SESSIONS){
taskExecutor.execute(new Runnable() {
@Override
public void run() {
try {
logger.info("The current thread is: "+Thread.currentThread().getId()+"**"+Thread.currentThread().getName());
session.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
/**
* 给某个用户发送消息
* @param sessionId
* @param message
*/
public void sendToUser(final String sessionId,final TextMessage message){
taskExecutor.execute(new Runnable() {
@Override
public void run() {
for(WebSocketSession session : WEB_SOCKET_SESSIONS){
if(sessionId.equals(session.getAttributes().get(Constant.WEBSOCKET_SESSIONID))){
try {
session.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
break;
}
}
}
});
}
}
先看看HandShakeInterceptor拦截器。websocket连接的时候需要先握手,这个拦截器可以在握手前后做一些额外的处理,比如记录日志。但在这里我觉得最有用的还是用来把会话session的id保存到WebSocketSession的atributes里面,这样可以向指定的用户发送消息。
在WebSocketEndPoint类中我用到了线程池执行器taskExecutor,配置如下:
<!--线程池执行器-->
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5"/>
<property name="maxPoolSize" value="20"/>
<property name="queueCapacity" value ="100" />
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
</property>
</bean>
WebSocketEndPoint里面对消息的处理很简单,就是接收到消息后直接返回给客户端,但真实场景通常都是针对接收到的消息做一些业务处理,把业务处理的结果发送给客户端。如果业务处理比较耗时或者并发量比较高,用线程池是很好的选择。
类中还用到了一个集合WEB_SOCKET_SESSIONS,用来保存建立起来的连接。遍历它,可以给所有客户端发送消息。
最后,通过页面来测试一下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Web Socket JavaScript Echo Client</title>
<%
request.setAttribute("wsPath", request.getServerName()+":"+request.getServerPort()+request.getContextPath());
%>
<script type="text/javascript">
var ws;
function init() {
output = document.getElementById("output");
send_echo();
}
function send_echo() {
var wsUrl;
if (window.location.protocol == 'http:') {
wsUrl = "ws://${wsPath}/ws/websocket.ws";
} else {
wsUrl = "wss://${wsPath}/ws/websocket.ws";
}
writeToScreen("Connecting to "+wsUrl);
if ('WebSocket' in window) {
ws = new WebSocket(wsUrl);
}else if ('MozWebSocket' in window) {
ws = new MozWebSocket(wsUrl);
}else {
alert("您的浏览器版本过低,不支持WebSocket!");
return;
}
ws.onopen = function (evt) {
writeToScreen("Connected !");
};
ws.onmessage = function (evt) {
writeToScreen("Received message: " + evt.data);
//ws.close();
};
ws.onerror = function (evt) {
writeToScreen('<span style="color: red;">ERROR:</span> '
+ evt.data);
ws.close();
};
}
function doSend() {
ws.send(textID.value);
writeToScreen("Sent message: " + textID.value);
}
function writeToScreen(message) {
var pre = document.createElement("p");
pre.style.wordWrap = "break-word";
pre.innerHTML = message;
output.appendChild(pre);
}
window.addEventListener("load", init, false);
</script>
</head>
<body>
<h1>Echo Server</h1>
<div style="text-align: left;">
<form action="">
<input οnclick="doSend()" value="发送socket请求" type="button">
<input id="textID" name="message" value="Hello World, Web Sockets" type="text">
<br>
</form>
</div>
<div id="output"></div>
</body>
</html>
注意:websocket的请求url是根据<url-pattern>/ws/*</url-pattern><!-- 这是websocket的请求 -->和<websocket:mapping path="/websocket.ws" handler="websocket"/>决定的。所以这里的请求url就是/ws/websocket.ws。如果改成如下配置:
<url-pattern>*.ws</url-pattern>
那么请求url就是/websocket.ws了。