在开发过程中,时常遇到需要前后端实时通讯的需求。比如客户和客服的在线沟通。而通常会有使用轮询的方式,每隔几秒钟发送一次HTTP请求,来检查客户或客服有没有发送新的消息。这是因为HTTP请求的惰性特点,只能由客户端发起请求,而不能由服务端主动推送消息给客户端。由此,可以考虑使用WebSocket技术建立双端都可主动推送消息的长连接来解决类似场景的问题。
本次讲述的是Spring+SpringMVC框架下开放WebSocket的方式。
开放WebSocket接口可以采用以下步骤:
1、配置WebSocket接口信息。
2、自定义WebSocket消息处理实现。
3、自定义WebSocket消息拦截器(可选)。
4、将WebSocket配置实例装入Spring容器。可选用扫描方式或bean配置方式。此示例中采用扫描方式。
1、配置WebSocket接口信息。
1)首先创建websocket专属的package(规范问题,实现某一功能的源码应放到统一路径下)。
2)创建WebSocketConfig类,继承WebMvcConfigurationSupport类,实现WebSocketConfigurer接口并实现其定义的方法。添加类注解
@Configuration
@EnableWebMvc
@EnableWebSocket
3)实现的registerWebSocketHandlers方法中,使用参数WebSocketHandlerRegistry的对象开放WebSocket接口。
registry.addHandler(websocket消息处理实现类实例,websocket地址).setAllowedOrigins("*").addInterceptors(拦截器实例);
其中setAllowedOrigins为设置访问权限,通常不限制访问时使用“*”;addInterceptors为添加拦截器,如果不需要拦截器则可不调用此方法。
通常开放WebSocket接口开放两个,因浏览器支持的问题,前端只能使用SockJS插件实现WebSocket连接,所以我们还要开放一个专为SockJS服务的WebSocket接口,只需调用withSockJS接口即可。
registry.addHandler(websocket消息处理实现类实例,websocket地址).setAllowedOrigins("*").addInterceptors(拦截器实例).withSockJS();
至此,第一步,配置WebSocket信息已完成。
2、自定义WebSocket消息处理实现。
1)创建WebSocket消息处理接口,继承WebSocketHandler接口(为开放操作接口给其他服务调用,一般与业务相关,服务端希望推送消息时调用此接口,同样关乎规范,亦可使用其他方式实现)。
2)创建WebSocket消息处理实现类,实现第一步创建的接口。其中,业务接口请自行实现,从WebSocketHandler继承实现的接口有:
afterConnectionEstablished:建立WebSocket连接成功时触发。
handleMessage:收到客户端发送的消息时触发。
handleTransportError:发生异常时触发。
afterConnectionClosed:与客户端断开连接时触发。
supportsPartialMessages:是否支持部分消息处理(一般返回false,此标志一般用来拆分大型或未知大小的WebSocket消息,当支持部分消息时,一个消息会拆分成多个消息调用handleMessage,可以利用WebSocketMessage的isLast方法判断是否最后一次消息)。
3、自定义WebSocket消息拦截器(可选)
1)创建WebSocket拦截器,实现HandshakeInterceptor接口。实现方法有:
beforeHandshake:消息处理前。
afterHandshake:消息处理后。
4、将WebSocket配置实例装入Spring容器
此处使用配置扫描包的形式,在配置文件中添加<context:component-scan base-package="com.???.???.???.websocket" />
其中base-package可以根据自己项目的实际包路径配置。
下面就是实例代码,细节处可根据实际项目修改:
- WebSocket配置信息
package com.???.???.???.websocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* websocket配置类
*
* @author Heller.Zhang
* @since 2018年11月8日 上午10:23:04
*/
@Configuration
@EnableWebMvc
@EnableWebSocket
public class MyWebSocketConfig extends WebMvcConfigurationSupport implements WebSocketConfigurer {
private Logger logger = LoggerFactory.getLogger(MyWebSocketConfig.class);
/* (non-Javadoc)
* @see org.springframework.web.socket.config.annotation.WebSocketConfigurer#registerWebSocketHandlers(org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry)
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
logger.debug("----------开始注册MyWebSocketConfig----------");
logger.debug("----------注册普通websocket----------");
registry.addHandler(msgSocketHandle(), "/websocket/myWebSocketServer").setAllowedOrigins("*");
logger.debug("----------注册sockjs/websocket----------");
registry.addHandler(msgSocketHandle(), "/websocket/sockjs/myWebSocketServer").setAllowedOrigins("*").withSockJS();
}
@Bean(name = "myMsgScoketHandle", autowire = Autowire.BY_NAME)
public MyMsgScoketHandle msgSocketHandle() {
return new MyMsgScoketHandleImpl();
}
}
- 自定义WebSocket消息处理接口
package com.???.???.???.websocket;
import org.springframework.web.socket.WebSocketHandler;
/**
* websocket消息处理器
*
* @author Heller.Zhang
* @since 2018年11月13日 下午5:13:20
*/
public interface MyMsgScoketHandle extends WebSocketHandler {
public void sendMessage(Long userId, Integer num);
}
- 自定义WebSocket消息处理接口实现
package com.???.???.???.websocket;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import com.hzsparrow.framework.common.exception.ServiceException;
import com.hzsparrow.framework.utils.JsonUtils;
import com.hzsparrow.framework.utils.NumberUtils;
/**
* websocket消息处理类
*
* @author Heller.Zhang
* @since 2018年11月8日 上午11:15:22
*/
public class MyMsgScoketHandleImpl implements MyMsgScoketHandle {
private Logger logger = LoggerFactory.getLogger(MyMsgScoketHandleImpl.class);
private ConcurrentMap<String, List<WebSocketSession>> webSocketMap = new ConcurrentHashMap<>();
public MyMsgScoketHandleImpl() {
logger.debug("--------init MyMsgScoketHandle-----------");
}
/* (non-Javadoc)
* @see org.springframework.web.socket.WebSocketHandler#afterConnectionEstablished(org.springframework.web.socket.WebSocketSession)
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
logger.debug("-----------建立websocket连接---------");
}
/* (non-Javadoc)
* @see org.springframework.web.socket.WebSocketHandler#handleMessage(org.springframework.web.socket.WebSocketSession, org.springframework.web.socket.WebSocketMessage)
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
logger.debug("-----------处理消息---------");
TextMessage msg = (TextMessage) message;
//建立连接发送的用户id
String msgStr= msg.getPayload();
logger.debug("----------msg:" + msgStr);
if (StringUtils.equals(msgStr, "heartbeat")) {
// 心跳消息,不予处理
return;
}
session.getAttributes().put("mykey", msgStr + "|" + UUID.randomUUID().toString());
//通过用户id获取session集合
List<WebSocketSession> list = webSocketMap.get(msgStr);
//判断集合是否为空
if (CollectionUtils.isNotEmpty(list)) {
list.add(session);
} else {
list = new ArrayList<WebSocketSession>();
list.add(session);
}
//将用户id和session集合绑定到map
webSocketMap.put(msgStr, list);
logger.debug("消息处理完毕");
}
/* (non-Javadoc)
* @see org.springframework.web.socket.WebSocketHandler#handleTransportError(org.springframework.web.socket.WebSocketSession, java.lang.Throwable)
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
logger.debug("-----------处理异常---------");
}
/* (non-Javadoc)
* @see org.springframework.web.socket.WebSocketHandler#afterConnectionClosed(org.springframework.web.socket.WebSocketSession, org.springframework.web.socket.CloseStatus)
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
logger.debug("-----------断开连接---------");
String myKey = session.getAttributes().get("mykey").toString();
String userId = myKey.split("|")[0];
List<WebSocketSession> list = webSocketMap.get(userId);
WebSocketSession delSession = null;
if (CollectionUtils.isNotEmpty(list)) {
for (WebSocketSession webSocketSession : list) {
String delKey = webSocketSession.getAttributes().get("mykey").toString();
if (StringUtils.equals(myKey, delKey)) {
delSession = webSocketSession;
break;
}
}
}
if (delSession != null) {
list.remove(delSession);
}
}
/* (non-Javadoc)
* @see org.springframework.web.socket.WebSocketHandler#supportsPartialMessages()
*/
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 给单个用户发送消息
*
* @param session
* @param type
* @param data
* @throws IOException
* @author Heller.Zhang
* @since 2018年11月8日 下午2:05:32
*/
public void sendMessage(WebSocketSession session, Object data) throws IOException {
if (session.isOpen()) {
String json = JsonUtils.serialize(data);
TextMessage outMsg = new TextMessage(json);
session.sendMessage(outMsg);
}
}
/**
* 发送链接成功消息
*
* @param session
* @throws IOException
* @author Heller.Zhang
* @since 2018年11月10日 下午3:16:35
*/
public void sendSuccessMsg(WebSocketSession session, Integer type) throws IOException {
logger.debug("---------发送链接成功消息-----------");
TextMessage outMsg = new TextMessage("");
session.sendMessage(outMsg);
}
@Override
public void sendMessage(Long userId, Integer num) {
//获取用户下的session集合
List<WebSocketSession> list = webSocketMap.get(NumberUtils.num2Str(userId));
//向每一条session中发送数据
if (CollectionUtils.isNotEmpty(list)) {
for (WebSocketSession webSocketSession : list) {
try {
sendMessage(webSocketSession, num);
} catch (IOException e) {
throw new ServiceException("发送消息失败");
}
}
}
}
}
- 拦截器
package com.???.???.???.websocket;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
/**
* websocket拦截器
*
* @author Heller.Zhang
* @since 2018年11月8日 上午10:40:51
*/
public class MyWebSocketHandshakeInterceptor implements HandshakeInterceptor {
private Logger logger = LoggerFactory.getLogger(MyWebSocketHandshakeInterceptor.class);
/* (non-Javadoc)
* @see org.springframework.web.socket.server.HandshakeInterceptor#beforeHandshake(org.springframework.http.server.ServerHttpRequest, org.springframework.http.server.ServerHttpResponse, org.springframework.web.socket.WebSocketHandler, java.util.Map)
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
logger.debug("---------websocket 拦截器 握手前------------");
return true;
}
/* (non-Javadoc)
* @see org.springframework.web.socket.server.HandshakeInterceptor#afterHandshake(org.springframework.http.server.ServerHttpRequest, org.springframework.http.server.ServerHttpResponse, org.springframework.web.socket.WebSocketHandler, java.lang.Exception)
*/
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
logger.debug("---------websocket 拦截器 握手后------------");
}
}
- 扫描包配置
<?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:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
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/task http://www.springframework.org/schema/task/spring-task-3.0.xsd">
<context:component-scan base-package="com.???.???.???.websocket" />
</beans>