SpringBoot引入WebSocket
目录
前言
本文主要介绍使用SpringBoot + Vue项目引入WebSocket进行服务器端与客户端进行双向通信的具体流程。
一、背景介绍
平时我们开发项目时,前后端采用HTTP的方式进行数据交互,此方式只能从客户端发起请求给服务器端,服务器端接收到请求之后进行相应的业务逻辑处理,再将结果响应给客户端,此时一次HTTP请求结束。
如果我们想要通过服务器端主动给客户端推送数据,此时我们可以使用WebSocket或者SSE来实现。本文主要来介绍使用WebSocket实现服务器端主动给客户端推送数据的过程。
二、后端集成步骤
1.项目依赖
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.6.2</version>
</dependency>
<!--以下依赖可根据自己的需要自行调整-->
<!--spring boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<!--common utils-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
本文后端SpringBoot版本采用2.3.12.RELEASE,如使用其他版本也可进行参考。
本文使用RocketMQ消息队列,用于处理websocket的session共享:使用websocket建立连接后,此连接数据无法进行序列化进行共享到其他服务器,当项目采用分布式部署时,只能由最开始建立连接的那台服务器给对应的客户端发送消息,其他服务器是发送不了消息给对应的客户端的。所以本文使用消息队列广播的方式,将要发送的消息广播给所有的服务器,未建立连接的服务器跳过处理,建立连接的服务器发送消息给客户端。
2.WebSocket配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.WebSocket服务端
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.io.Serializable;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* websocket服务端
*/
@Slf4j
@Component
@ServerEndpoint("/wsServer/{userId}")
@JsonIgnoreProperties(ignoreUnknown = true)
public class WebSocketServer implements Serializable {
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送消息
*/
private Session session;
/**
* 接收userId
*/
private String userId;
private static final ConcurrentHashMap<String, Session> WS_MAP = new ConcurrentHashMap<>();
/**
* 连接建立成功调用的方法
*
* @param session 连接会话
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId) {
this.userId = userId;
if (StringUtils.isBlank(this.userId)) {
throw new RuntimeException("用户ID不能为空");
}
this.session = session;
if (WS_MAP.containsKey(this.userId)) {
WS_MAP.remove(this.userId);
WS_MAP.put(this.userId, this.session);
} else {
WS_MAP.put(this.userId, this.session);
}
try {
sendMessage();
} catch (IOException e) {
log.error("用户".concat(this.userId).concat("网络异常"));
}
log.info("Websocket用户连接======当前在线人数: " + WS_MAP.size() + "人");
}
@OnClose
public void onClose() {
WS_MAP.remove(this.userId);
log.info("Websocket用户退出======当前在线人数: " + WS_MAP.size() + "人");
}
/**
* 收到客户端消息后调用
*
* @param message 收到的消息
* @param session websocket会话
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("用户消息:" + userId + ", 报文:" + message);
}
/**
* 发生异常时调用
*
* @param session websocket会话
* @param throwable 异常信息
*/
@OnError
public void onError(Session session, Throwable throwable) {
log.error("用户" + userId + "连接异常: ", throwable);
}
/**
* 建立连接成功后向客户端发送消息
*
* @throws IOException 发送异常
*/
private void sendMessage() throws IOException {
this.session.getBasicRemote().sendText("已连接");
}
/**
* 发送消息
*
* @param userId 目标用户ID
* @param message 消息
*/
public static void sendInfo(String userId, String message) {
log.info("发送消息给" + userId + ", 报文: " + message);
if (StringUtils.isBlank(userId) || !WS_MAP.containsKey(userId)) {
log.error("当前用户不在线");
return;
}
try {
WS_MAP.get(userId).getBasicRemote().sendText(message);
} catch (IOException e) {
throw new RuntimeException("推送消息失败", e);
}
}
/**
* 获取当前在线的用户
*
* @return 在线的用户
*/
public static Set<String> getOnlineUser() {
if (WS_MAP.isEmpty()) {
return null;
}
return WS_MAP.keySet();
}
}
4.RocketMq队列监听
import lombok.Data;
/**
* 消息实体类
*/
@Data
public class WebSocketMessageDTO {
/**
* 目标用户ID
*/
private String userId;
/**
* 要发送的消息
*/
private String message;
}
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
/**
* RocketMq队列监听
*/
@Component
@RocketMQMessageListener(topic = "${rocketmq.consumer.topic}", consumerGroup = "${rocketmq.consumer.group}", selectorExpression = "websocket", messageModel = MessageModel.BROADCASTING)
public class WebsocketConsumerListener implements RocketMQListener<WebSocketMessageDTO> {
@Override
public void onMessage(WebSocketMessageDTO messageDTO) {
WebSocketServer.sendInfo(messageDTO.getUserId(), messageDTO.getMessage());
}
}
5.推送消息实现类
/**
* 给客户端推送消息
*/
public interface WebSocketService {
/**
* 给客户端发送消息
*
* @param messageDTO 要发送的信息
*/
void sendMessage(WebSocketMessageDTO messageDTO);
}
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 给客户端推送消息
*/
@Slf4j
@Service
public class WebSocketServiceImpl implements WebSocketService {
@Value("${rocketmq.consumer.topic}")
private String topic;
@Resource
private RocketMQTemplate rocketMqTemplate;
/**
* 给客户端发送消息
*
* @param messageDTO 要发送的信息
*/
@Override
public void sendMessage(WebSocketMessageDTO messageDTO) {
// 发送到队列中
String destination = topic + ":websocket";
SendResult sendResult = rocketMqTemplate.syncSend(destination, messageDTO);
log.info("已推送至消息队列:MsgId={}, MessageQueue={}", sendResult.getMsgId(), sendResult.getMessageQueue());
}
}
三、前端集成步骤
1.项目依赖
npm install reconnecting-websocket
本文前端项目采用NodeJs + Vue2进行构建。
本文使用的reconnecting-websocket组件,在连接建立后,当连接中断时会自动进行尝试重连。因为默认使用的websocket组件并没有自动重连机制,故需自行实现自动重连。当然如果不使用reconnecting-websocket组件也可以,使用window.setInterval()建立定时器轮询检查websocket连接状态的方式也可以。
2.创建websocket
initWebSocket: function () {
// websocket的服务端地址,userId可根据需要自行替换为当前登录的用户ID等
let url = "http://127.0.0.1:8080/wsServer/userId";
// 实例化socket
this.socket = new ReconnectingWebSocket(url);
// 如果使用websocket组件, 则为: this.socket = new WebSocket(url);
// 监听socket连接
this.socket.onopen = this.openWebSocket;
// 监听socket错误信息
this.socket.onerror = this.errorWebSocket;
// 监听socket消息
this.socket.onmessage = this.getMessageWebSocket;
// 监听socket断开连接的消息
this.socket.close=this.closeWebSocket;
},
openWebSocket: function () {
console.log("socket连接成功");
},
errorWebSocket: function () {
console.log("连接错误");
},
getMessageWebSocket: function (message) {
let data = message.data;
console.log("接收到的消息: " + data);
},
closeWebSocket: function () {
console.log("连接关闭");
}
四、其他配置
1.Nginx转发ws请求
如果我们在部署websocket项目时会采用分布式部署,此时会有多个websocket服务节点,所以我们可以采用nginx作为负载均衡,统一提供websocket服务入口,供客户端调用。nginx上的配置如下:
http {
map $http_upgrade $connection_upgrade {
default keep-alive; # 默认 keep-alive,表示HTTP协议。
'websocket' upgrade; # 若是 websocket 请求,则升级协议 upgrade。
}
upstream websocket_servers {
server websocket服务ip1:port
server websocket服务ip2:port
}
server {
listen 8080;
location /system/wsServer {
proxy_pass http://websocket_servers; # 转发到websocket后端接口
proxy_read_timeout 1800s; # 设置超时时间,默认是60s
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
}
总结
以上就是今天要讲的内容,本文仅仅简单介绍了Java项目中集成WebSocket的方式,具体在使用时,可根据项目需要填充具体的业务逻辑进行使用。
本文中介绍的WebSocket是一种全双工的通信技术,既可以从服务器端给客户端推送消息,客户端也可以主动给服务器端推送消息,如果要实现实时聊天的业务场景,可以使用WebSocket进行实现。
此外,如果只是要实现从服务器端给客户端推送消息的单向通信,也可使用SSE(Server Sent Event)进行实现,具体集成方式将在后续的文章中总结。