WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。 WebSocket通信协议于2011年被IETF定为标准RFC 6455,WebSocketAPI被W3C定为标准。 在WebSocket API中,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

以上摘自Wikipedia.

本文将说明在Intellij IDEA下使用Gradle构建SpringMVC+WebSocket实现消息推送.为了更好的阅读体验,请点击阅读原文😉.

创建项目

在之前的文章中有详细的说明,此篇不做赘述.可参考IDEA+Gradle创建MyBatis+SpringMVC项目

导入依赖

build.gradle中导入denpendies

1
2
3
4
5
6
7
8
9
10
11
12
13
// ------------------------   Spring SpringMVC  start -------------------------
compile group: 'org.springframework', name: 'spring-webmvc', version: '4.2.4.RELEASE'
compile group: 'org.springframework', name: 'spring-context-support', version: '4.2.4.RELEASE'
// ------------------------ Spring SpringMVC end -------------------------

// ------------------------ WebSocket start -------------------------
compile group: 'org.springframework', name: 'spring-websocket', version: '4.2.4.RELEASE'
compile group: 'org.springframework', name: 'spring-messaging', version: '4.2.4.RELEASE'
compile group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
// ------------------------ WebSocket end -------------------------

compile group: 'log4j', name: 'log4j', version: '1.2.17'
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.2'

此处需要注意的是Spring4.0+版本开始支持WebSocket,而servlet-api需要为3.0+版本

配置文件

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

<!-- 初始化spring 容器 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:config/spring/applicationContext-*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- 配置前端控制器 -->
<servlet>
<servlet-name>index-dispather</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:config/spring/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>

<servlet-mapping>
<servlet-name>index-dispather</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

<!-- 解决post乱码问题 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

</web-app>

此处需要给servletfilter添加异步<async-supported>true</async-supported>.

spring-mvc.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">

<!-- 自动扫描控制器,webSocket -->
<context:component-scan base-package="com.lhalcyon.king.controller,com.lhalcyon.king.socket"/>
<!-- 视图渲染 -->
<bean id="internalResourceViewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!-- 控制器映射器和控制器适配器 -->
<mvc:annotation-driven>

</mvc:annotation-driven>

<!-- 静态资源映射器 -->
<mvc:resources mapping="/statics/**" location="/WEB-INF/statics/" />
</beans>

此处需配置扫描controller和websocket所在包

还有一个applicationContext-websocket.xml配置文件我们在代码实现中说明

代码实现

握手拦截器

HandshakeInterceptor

拦截器说明

An interceptor to copy information from the HTTP session to the “handshake
attributes” map to made available via WebSocketSession.getAttributes()
Copies a subset or all HTTP session attributes and/or the HTTP session id

拦截器主要用于用户登录标识的记录,便于后面获取指定用户的会话标识并向指定用户发送消息,

这里我们继承HttpSessionHandshakeInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor {

private Logger logger = Logger.getLogger(HandshakeInterceptor.class);

// 握手前
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {

logger.info("++ HandshakeInterceptor: beforeHandshake ++"+attributes);
return super.beforeHandshake(request, response, wsHandler, attributes);
}

// 握手后
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {

logger.info("++ HandshakeInterceptor: afterHandshake ++");
super.afterHandshake(request, response, wsHandler, ex);
}
}

beforeHandshake(..)

Invoked before the handshake is processed.

afterHandshake(..)

Invoked after the handshake is done. The response status and headers indicate the results of the handshake, i.e. whether it was successful or not.

两个方法调用时机均为字面所述,握手前后分别调用.主要是在握手前后去做一些事,比如将需要的数据设置到attributes里,之后在WebSocketHandler的session中获取这些数据.

处理类

WebSocketHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class MyWebSocketHandler implements WebSocketHandler {


private static final Logger log = Logger.getLogger(MyWebSocketHandler.class);

// 保存所有的用户session
private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();


@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("connect websocket success.......");

users.add(session);
}

@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
Gson gson = new Gson();
// 将消息JSON格式通过Gson转换成Map
// message.getPayload().toString() 获取消息具体内容
Map<String, Object> msg = gson.fromJson(message.getPayload().toString(),
new TypeToken<Map<String, Object>>() {}.getType());
log.info("handleMessage......."+message.getPayload()+"..........."+msg);
String content = message.getPayload().toString();
// 处理消息 msgContent消息内容
TextMessage textMessage = new TextMessage(content, true);
// 调用方法(发送消息给所有人)
sendMsgToAllUsers(textMessage);
}

@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.warn("handleTransportError");
users.remove(session);
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.info("connect websocket closed.......");
users.remove(session);

}

// 给所有用户发送 信息
public void sendMsgToAllUsers(WebSocketMessage<?> message) throws Exception{
for (WebSocketSession user : users) {
user.sendMessage(message);
}
}

@Override
public boolean supportsPartialMessages() {
return false;
}
}

以下对主要方法进行说明:

afterConnectionEstablished(..)

连接建立后调用,常用于记录用户的连接标识,便于后面信息发送.

handleTextMessage(..)

对消息进行处理.

handleTransportError(..)

连接异常处理.需要关闭出错会话连接

afterConnectionClosed(..)

连接关闭处理

此处我们对消息的处理很简单,即在接受消息后发送给所有连接的用户,类似一个匿名群聊室.

注册

完成了WebSocket处理类,还需要对其进行注册生效.这里有两种方式,择其一即可.

创建配置类,并通过注解注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableWebMvc
@EnableWebSocket
public class MyWebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//前台 可以使用websocket环境
registry.addHandler(myWebSocketHandler(),"/websocket").addInterceptors(new HandshakeInterceptor()).setAllowedOrigins("*");

//前台 不可以使用websocket环境,则使用sockjs进行模拟连接
registry.addHandler(myWebSocketHandler(), "/sockjs/websocket").addInterceptors(new HandshakeInterceptor())
.withSockJS();
}

// websocket 处理类
@Bean
public WebSocketHandler myWebSocketHandler(){
return new MyWebSocketHandler();
}
}

同时还需要配置上文提到过的Spring扫描配置类.

1
2
<!-- 自动扫描控制器,webSocket -->
<context:component-scan base-package="com.lhalcyon.king.controller,com.lhalcyon.king.socket"/>
通过xml配置注册

applicationContext-websocket.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">



<!-- websocket处理类 -->
<bean id="myHandler" class="com.lhalcyon.king.socket.MyWebSocketHandler"/>

<!-- 握手接口/拦截器 -->
<bean id="myInterceptor" class="com.lhalcyon.king.socket.HandshakeInterceptor"/>

<websocket:handlers>
<websocket:mapping path="/websocket" handler="myHandler"/>
<websocket:handshake-interceptors>
<ref bean="myInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>

<!-- 注册 sockJS -->
<websocket:handlers>
<websocket:mapping path="/websocket" handler="myHandler"/>
<websocket:handshake-interceptors>
<ref bean="myInterceptor"/>
</websocket:handshake-interceptors>
<websocket:sockjs />
</websocket:handlers>

</beans>

以上服务端代码实现基本完成,接下来对客户端测试页面做一个简单的实现.

客户端页面

index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE HTML>
<html>
<head>
<title>首页</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="renderer" content="webkit">

<!-- 引入 JQuery -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

<!-- 引入 sockJS -->
<script type="text/javascript" src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js" ></script>

<script type="text/javascript">
$(function() {
var websocket;

// 首先判断是否 支持 WebSocket

var url = 'ws://' + window.location.host + '/word-king/websocket';

if('WebSocket' in window) {
websocket = new WebSocket(url);
} else if('MozWebSocket' in window) {
websocket = new MozWebSocket(url);
} else {
url = "http://"+ window.location.host +"/word-king/sockjs/websocket";
websocket = new SockJS(url);
}
// 打开时
websocket.onopen = function(evnt) {
console.log(" websocket.onopen ");
};
// 处理消息时
websocket.onmessage = function(evnt) {
$("#msg").append("<p>(<font color='red'>" + evnt.data + "</font>)</p>");
console.log(" websocket.onmessage ");
};
websocket.onerror = function(evnt) {
alert("onerror");
console.log(" websocket.onerror ");
};
websocket.onclose = function(evnt) {
console.log(" websocket.onclose ");
alert("onclose");
};
// 点击了发送消息按钮的响应事件
$("#TXBTN").click(function(){
// 获取消息内容
var text = $("#tx").val();
// 判断
if(text == null || text == ""){
alert(" content can not empty!!");
return false;
}
var msg = {
msgContent: text,
postsId: 1
};
// 发送消息
websocket.send(JSON.stringify(msg));
});
});
</script>
</head>
<body>
<!-- 最外边框 -->
<div style="margin: 20px auto; border: 1px solid blue; width: 300px; height: 500px;">

<!-- 消息展示框 -->
<div id="msg" style="width: 100%; height: 70%; border: 1px solid yellow;overflow: auto;"></div>

<!-- 消息编辑框 -->
<textarea id="tx" style="width: 100%; height: 20%;"></textarea>

<!-- 消息发送按钮 -->
<button id="TXBTN" style="width: 100%; height: 8%;">发送数据</button>
</div>
</body>
</html>

需要注意的是此处引入JQuery时,如果是本地的文件,可能存在无效的情况,需要去设置静态资源映射路径,可自行🔍解决.

本文采用的是引入在线地址

1
2
3
<!-- 引入 JQuery  -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.5/jquery.min.js"></script>

地址写入后需Download下来

代码不高亮警告后即能生效.

最后我们来看看效果


  1. 参考: http://blog.csdn.net/mybook201314/article/details/70173674