SpringBoot项目中使用WebSocket实现服务端推送消息
之前了解过使用webSocket实现服务端消息推送,一直没有真正实践过,上周公司有一个后端新消息推送的业务需要处理,立即想到使用WebSocke做,由于项目大体结构采用SpringBoot+SpringSecurity+solr做的,看到网上有好多相关的示例不能满足需要,自己总结一波,关于webSocket的基本了解,请自行百度,本文重点贴出如何使用,以及业务处理。
一、使用场景
- 数据流状态: 比如说上传下载文件,文件进度,文件是否上传成功。
- 协同编辑文档: 同一份文档,编辑状态得同步到所有参与的用户界面上。
- 多玩家游戏: 很多游戏都是协同作战的,玩家的操作和状态肯定需要及时同步到所有玩家。
- 多人聊天: 很多场景下都需要多人参与讨论聊天,用户发送的消息得第一时间同步到所有用户。
- 社交订阅: 有时候我们需要及时收到订阅消息,比如说开奖通知,比如说在线邀请,支付结果等。
- 股票虚拟货币价格: 股票和虚拟货币的价格都是实时波动的,价格跟用户的操作息息相关,及时推送对用户跟盘有很大的帮助。
二、WebSocket与http区别
- WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)
- 首先HTTP有 1.1 和 1.0 之说,也就是所谓的 keep-alive ,把多个HTTP请求合并为一个,但是 Websocket 其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,有交集,但是并不是全部。
- 另外Html5是指的一系列新的API,或者说新规范,新技术。
- Websocket协议解决了服务器与客户端全双工通信的问题。
三、WebSocket与Socket区别
1.WebSocket:
- websocket通讯的建立阶段是依赖于http协议的。最初的握手阶段是http协议,握手完成后就切换到websocket协议,并完全与http协议脱离了。
- 建立通讯时,也是由客户端主动发起连接请求,服务端被动监听。
- 通讯一旦建立连接后,通讯就是“全双工”模式了。也就是说服务端和客户端都能在任何时间自由得发送数据,非常适合服务端要主动推送实时数据的业务场景。
- 交互模式不再是“请求-应答”模式,完全由开发者自行设计通讯协议。
- 通信的数据是基于“帧(frame)”的,可以传输文本数据,也可以直接传输二进制数据,效率高。当然,开发者也就要考虑封包、拆包、编号等技术细节。
2.Socket:
- 服务端监听通讯,被动提供服务;客户端主动向服务端发起连接请求,建立起通讯。
- 每一次交互都是:客户端主动发起请求(request),服务端被动应答(response)。
- 服务端不能主动向客户端推送数据。
- 通信的数据是基于文本格式的。二进制数据(比如图片等)要利用base64等手段转换为文本后才能传输。
三、SpringBoot项目中使用WebSocket
- 导入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>
- 新建WebSocket配置类
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
webSocketHandlerRegistry.addHandler(myHandler(),"/sendMessage/{ID}")
.setAllowedOrigins("*")
.addInterceptors(new WebSocketInterceptor());
}
private WebSocketServer myHandler(){
return new WebSocketServer();
}
}
- 新建WebSocket拦截器
public class WebSocketInterceptor implements HandshakeInterceptor{
private final static Logger LOG= LoggerFactory.getLogger(WebSocketInterceptor.class);
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
if (request instanceof ServletServerHttpRequest) {
String ID = request.getURI().toString().split("ID=")[1];
if(LOG.isInfoEnabled()){
LOG.info("当前session的ID="+ID);
}
ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request;
HttpSession session = serverHttpRequest.getServletRequest().getSession();
map.put(Constant.WEBSOCKET_USERID,ID);
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
if(LOG.isInfoEnabled()){
LOG.info("webSocket拦截器的afterHandshake方法执行了");
}
}
}
- 新建WebSocket的Hanlder服务类
@Component
public class WebSocketServer implements WebSocketHandler{
private final static Logger LOG= LoggerFactory.getLogger(WebSocketServer.class);
/**
* WebSocket不能注入(@Autowired),将要注入的service改成static
* 原因:Spring管理的都是单例的,webSocket多对象冲突
*/
private static WebSocketService webSocketService;
@Autowired
private void setWebSocketService(WebSocketService webSocketService){
WebSocketServer.webSocketService=webSocketService;
}
/**
* 成功建立连接,处理当前建立连接用户信息
* @param session
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String ID=session.getUri().toString().split("ID=")[1];
if(ID!=null){
WebSocketUtils.electricSocketMap.put(ID,session);
}
if(LOG.isInfoEnabled()){
LOG.info("[WebSocket成功建立连接] 当前在线人员"+WebSocketUtils.electricSocketMap.size()+" 当前建立连接人员id="+ID);
}
//建立连接后具体业务实现
webSocketService.connDataProc(session.getUri().toString());
}
/**
* 接收socket信息
* @param session
* @param webSocketMessage
* @throws Exception
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> webSocketMessage) throws Exception {
JSONObject jsonObject=JSONObject.fromObject(webSocketMessage.getPayload());
//收到消息后具体业务实现
webSocketService.receiveMessageProc(jsonObject);
}
/**
* 连接出错
* @param session
* @param throwable
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable throwable) throws Exception {
if(LOG.isErrorEnabled()){
LOG.error("[webSocket连接发生错误]"+session.getId()+"出错信息"+throwable);
}
if(session.isOpen()){
session.close();
}
WebSocketUtils.electricSocketMap.remove(WebSocketUtils.getClientId(session));
}
/**
* 连接关闭之后的动作
* @param webSocketSession
* @param closeStatus
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
if(LOG.isDebugEnabled()){
LOG.info("[WebSocket连接关闭]"+closeStatus);
}
WebSocketUtils.electricSocketMap.remove(WebSocketUtils.getClientId(webSocketSession));
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
- 新建WebSocket的工具类
public class WebSocketUtils {
private final static Logger LOG= LoggerFactory.getLogger(WebSocketUtils.class);
/**
*concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
*/
public static Map<String, WebSocketSession> electricSocketMap;
static {
electricSocketMap=new ConcurrentHashMap<String, WebSocketSession>();
}
/**
* 获取用户标识
* @param session
* @return
*/
public static String getClientId(WebSocketSession session){
String clientId = null;
try{
clientId= (String) session.getAttributes().get(Constant.WEBSOCKET_USERID);
}catch (Exception e){
}
return clientId;
}
/**
* 服务端主动推送:发送消息给指定的用户(一对一)
* @param clientId
* @param message
* @return
*/
public static boolean sendMessageToUser(String clientId, TextMessage message){
if(WebSocketUtils.electricSocketMap.get(clientId)==null){
return false;
}
WebSocketSession session=WebSocketUtils.electricSocketMap.get(clientId);
if(!session.isOpen()){
return false;
}
try {
if(LOG.isInfoEnabled()){
LOG.info("[服务端推送消息]:"+clientId+message);
}
synchronized (session){
session.sendMessage(message);
}
return true;
} catch (IOException e) {
if(LOG.isErrorEnabled()){
LOG.error("发送消息失败",e);
return false;
}
}
return true;
}
/**
* 服务端主动推送:广播发送消息
* @param message
* @return
*/
public static boolean sendMessageToAllUsers(TextMessage message){
boolean allSendSuccess=true;
Set<String> clientIds=WebSocketUtils.electricSocketMap.keySet();
WebSocketSession session=null;
for (String clientId : clientIds) {
try{
session=WebSocketUtils.electricSocketMap.get(clientId);
if(session.isOpen()){
session.sendMessage(message);
}
}catch (Exception e){
if(LOG.isErrorEnabled()){
LOG.error("群发消息失败"+clientId,e);
continue;
}
}
}
return allSendSuccess;
}
}
- 新建WebSocket的业务处理服务接口
public interface WebSocketService {
/**
* 建立连接数据处理
* @param uri
*/
void connDataProc(String uri);
/**
* 发送数据后数据处理
* @param jsonObject
*/
void receiveMessageProc(JSONObject jsonObject);
}
- 新建WebSocket的业务处理实现类
@Service
public class WebSocketServiceImpl implements WebSocketService{
@Override
public void connDataProc(String uri) {
String ID=uri.split("ID=")[1];
WebSocketUtils.sendMessageToUser(ID,new TextMessage(ID+"已建立连接"));
}
/**
* 接收socket发送的消息(业务处理)
* @param jsonObject
*/
@Override
public void receiveMessageProc(JSONObject jsonObject) {
//接收业务标识,根据业务标识判断所处理业务
String business= (String) jsonObject.get("business");
//具体业务处理不方便贴出,可根据自己需求定制业务。例如建立连接后每一个小时推送一次数据(使用线程处理,睡眠1个小时,关闭连接采用线程中断的方式)
WebSocketUtils.sendMessageToUser(ID,new TextMessage("所要发送的数据"));
}
}
- 新建前端处理js
var lockReconnect=true;//避免ws重复连接
var websocket;
var closeWebSocket;
var send;
function webSocket(userId,postValue) {
if(lockReconnect){
if('WebSocket' in window){
websocket = new WebSocket("ws://localhost:8080/sendMessage/ID="+userId);
}else{
alert("您的浏览器不支持websocket");
}
}else{
send(postValue);
}
websocket.onerror = function(){
setMessageInHtml("send error!"+userId);
};
websocket.onopen = function(){
lockReconnect=false;
//setMessageInHtml("connection success!"+userId)
send(postValue);
};
websocket.onmessage = function(event){
webSocketAfter(event.data);
};
websocket.onclose = function(){
lockReconnect=true;
//setMessageInHtml("closed websocket!"+userId)
};
window.onbeforeunload = function(){
closeWebSocket();
};
function setMessageInHtml(message){
console.log(message);
};
/**
* 建立连接后具体业务需求传参在send方法中写即可
* 例如:
* var postValue={};
* var msg = document.getElementById('text').value+"000"+"&001";
* postValue.business="1";
* postValue.message=msg;
*/
send=function send(postValue){
if(websocket.readyState===1){
websocket.send(JSON.stringify(postValue));
}
}
closeWebSocket=function closeWebSocket() {
if (websocket != null) {
if(websocket.readyState!=3){
var postValue = {};
postValue.userId=userId;
postValue.business = "0";
send(postValue);
}
}
}