本实例介绍了Spring Cloud Gateway整合基于STOMP协议的WebSocket的实现。开发了聊天功能,和用户在线状态。解决了协议gateway整合websocket出现的问题
技术点
- Spring Cloud Gateway
- Nacos
- WebSocket
- STOMP
WebSocket与STOMP协议详解
1. WebSocket
WebSocket 是一种通信协议,提供了在客户端和服务器之间建立全双工通信的功能。这意味着客户端和服务器可以在任意时间相互发送数据,而不必遵循请求-响应的传统模式(如HTTP)。
特点:
-
双向通信:与传统的HTTP不同,WebSocket允许服务器主动向客户端推送数据,而不必等待客户端请求。
-
持久连接:WebSocket连接一旦建立,可以一直保持连接,直到任意一方关闭它。这减少了频繁建立连接的开销。
-
低延迟:由于WebSocket消除了HTTP请求的头部开销和连接延迟,因此它适合于对延迟敏感的应用,如在线游戏、股票交易和聊天应用。
工作流程:
-
连接建立:客户端通过HTTP的升级机制(Upgrade header)请求建立WebSocket连接。
-
数据传输:一旦连接建立,客户端和服务器可以通过这个连接双向传输数据,使用一种轻量级的帧格式。
-
连接关闭:任意一方可以随时关闭连接,通知对方连接已关闭。
使用场景:
-
实时聊天应用
-
实时数据流(如股票行情、体育比赛更新)
-
在线多人游戏
-
实时协作工具(如Google Docs)
2. STOMP
STOMP(Simple Text Oriented Messaging Protocol) 是一种简单的文本协议,用于在客户端和消息代理(例如,RabbitMQ、ActiveMQ)之间交换消息。它是应用层的协议,常用于消息传递系统,提供了一种基于消息的通信方式。
特点:
-
简单易用:STOMP使用类似于HTTP的文本格式,非常容易理解和实现。
-
基于订阅/发布模型:STOMP支持基于主题(topic)的订阅/发布模型,这使得消息可以广播给多个客户端。
-
支持消息队列:STOMP支持将消息发送到队列,多个消费者可以从同一个队列中读取消息,确保负载均衡。
常用命令:
-
CONNECT:客户端请求连接到STOMP服务器。
-
SEND:客户端发送一条消息到指定的目的地(如某个队列或主题)。
-
SUBSCRIBE:客户端订阅某个目的地,以接收该目的地的所有消息。
-
UNSUBSCRIBE:取消订阅。
-
DISCONNECT:断开与STOMP服务器的连接。
STOMP和WebSocket的结合:
虽然WebSocket本身是一个很好的低延迟双向通信工具,但它只提供了一种基础的传输方式,没有定义消息的格式或通信模式。STOMP可以在WebSocket之上运行,提供了消息格式、路由、确认等高级功能。
例如,在一个实时聊天应用中,WebSocket用于底层的双向通信,而STOMP则负责处理消息的路由和格式化:
-
客户端通过WebSocket连接到服务器,并通过STOMP发送或订阅消息。
-
服务器使用STOMP协议将消息广播给所有订阅了相应主题的客户端。
WebSocket服务端
依赖
<!-- WebSocket依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- nacos依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
枚举类
package com.inspur.message.constant;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum MessageTypeEnum {
TEXT("文本"),
IMAGE("图片"),
VOICE("语音"),
FILE("文件"),
EMOJI("表情"),
SYSTEM_NOTIFICATION("系统通知"),
LOCATION("位置"),
LINK("链接"),
RECALL("撤回"),
MENTION("@提及"),
SYSTEM_MESSAGE("系统提示"),
VIDEO("视频"),
RED_PACKET("红包"),
VOTE("投票"),
FRIEND_SHARE("好友分享");
// 其他可能的消息类型...
private String description;
public String getDescription() {
return description;
}
public static boolean isFileMessageType(MessageTypeEnum messageType) {
return messageType == IMAGE || messageType == VOICE || messageType == FILE || messageType == VIDEO;
}
}
WebSocket配置类
package com.inspur.message.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker//注解开启STOMP协议来传输基于代理的信息,实现实时双向通信和消息传递
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 客户端在订阅或发布消息到目的地路径前,要连接到该端点
*
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS();// 启用 SockJS (浏览器不支持WebSocket,SockJS 将会提供兼容性支持)
}
/**
* 配置消息代理
*
* @param register
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry register) {
/**
* 放开的前缀路由,客户端才能接收对应路由开头的信息
*/
register.enableSimpleBroker("/topic", "/user");
/**
* 客户端发送消息到服务端,有用@MessageMapping注解的方法路径上,需要添加的前缀
*/
register.setApplicationDestinationPrefixes("/app");
}
}
聊天接口
package com.inspur.message.domain.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.inspur.common.io.file.constant.FileStorageStrategyEnum;
import com.inspur.message.constant.MessageTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("cas_chat_message")
public class ChatMessage implements Serializable {
private static final long serialVersionUID = -82451738820080491L;
@TableId(value = "id")
private String id;
/**
* 发送者
*/
@TableField(value = "sender")
private String sender;
@TableField(value = "sender_name")
private String senderName;
/**
* 接收者
*/
@TableField(value = "recipient")
private String recipient;
@TableField(value = "recipient_name")
private String recipientName;
/**
* 消息类型
*/
@TableField(value = "message_type")
private MessageTypeEnum messageTypeEnum;
/**
* 消息内容
*/
@TableField(value = "content")
private String content;
/**
* 文件路径
*/
@TableField(value = "file_path")
private String filePath;
/**
* 文件名称
*/
@TableField(value = "file_name")
private String fileName;
/**
* 文件存储策略
*/
@TableField(value = "file_storage_strategy")
private FileStorageStrategyEnum fileStorageStrategyEnum;
/**
* 发送时间
*/
@TableField(value = "send_date_time")
private Date sendDateTime;
}
package com.inspur.message.domain.dto;
import com.inspur.message.constant.MessageTypeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(description = "聊天消息数据传输对象")
public class ChatMessageDTO implements Serializable {
private static final long serialVersionUID = 155949851013052603L;
/**
* 发送者
*/
@ApiModelProperty("发送者ID")
private String sender;
/**
* 发送者名称
*/
@ApiModelProperty("发送者名称")
private String senderName;
/**
* 接收者
*/
@ApiModelProperty("接收者ID:存在则是单播,不存在,则是广播")
private String recipient;
/**
* 接收者名称
*/
@ApiModelProperty("接收者名称:存在则是单播,不存在,则是广播")
private String recipientName;
/**
* 消息类型
*/
@ApiModelProperty("消息类型")
private MessageTypeEnum messageTypeEnum;
/**
* 消息内容
*/
@ApiModelProperty("消息内容")
private String content;
/**
* 文件名称
*/
@ApiModelProperty("文件名:上传文件时,传入")
private String fileName;
/**
* 文件路径
*/
@ApiModelProperty("文件路径:上传文件时,传入")
private String filePath;
}
package com.inspur.message.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("聊天消息视图对象")
public class ChatMessageVO implements Serializable {
private static final long serialVersionUID = 802322252498374013L;
@ApiModelProperty("id")
private String id;
@ApiModelProperty("发送者ID")
private String sender;
@ApiModelProperty("发送者名称")
private String senderName;
@ApiModelProperty("接收者ID")
private String recipient;
@ApiModelProperty("接收者名称")
private String recipientName;
@ApiModelProperty("消息类型")
private String messageTypeEnum;
@ApiModelProperty("消息内容")
private String content;
@ApiModelProperty("文件路径")
private String filePath;
@ApiModelProperty("文件名称")
private String fileName;
@ApiModelProperty("文件存储策略")
private String fileStorageStrategyEnum;
@ApiModelProperty("发送时间")
private Date sendDateTime;
}
package com.inspur.message.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.inspur.common.core.exception.IcmException;
import com.inspur.common.core.response.R;
import com.inspur.message.domain.dto.ChatMessageDTO;
import com.inspur.message.domain.query.ChatMessageQuery;
import com.inspur.message.domain.vo.ChatMessageVO;
import com.inspur.message.service.ChatService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@Api(tags = "聊天")
public class ChatController {
@Resource
private ChatService chatService;
@ApiOperation("发送消息")
@MessageMapping("/chat/send")
public R sendMessage(@Payload ChatMessageDTO chatMessageDTO) {
chatService.saveMessage(chatMessageDTO);
return R.ok();
}
@ApiOperation("查询历史消息")
@PostMapping("/chat/history")
public R<IPage<ChatMessageVO>> getChatHistory(@Valid @RequestBody ChatMessageQuery chatMessageQuery) {
return R.ok(chatService.getChatHistory(chatMessageQuery));
}
}
package com.inspur.message.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.inspur.message.domain.dto.ChatMessageDTO;
import com.inspur.message.domain.query.ChatMessageQuery;
import com.inspur.message.domain.vo.ChatMessageVO;
public interface ChatService {
void saveMessage(ChatMessageDTO chatMessageDTO);
IPage<ChatMessageVO> getChatHistory(ChatMessageQuery chatMessageQuery);
}
package com.inspur.message.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.inspur.common.core.constants.UcConstant;
import com.inspur.common.core.domain.AccountInfo;
import com.inspur.common.io.file.constant.FileStorageStrategyEnum;
import com.inspur.message.constant.MessageTypeEnum;
import com.inspur.message.domain.convert.ChatMessageConvert;
import com.inspur.message.domain.dto.ChatMessageDTO;
import com.inspur.message.domain.po.ChatMessage;
import com.inspur.message.domain.query.ChatMessageQuery;
import com.inspur.message.domain.vo.ChatMessageVO;
import com.inspur.message.service.ChatMessageService;
import com.inspur.message.service.ChatService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Objects;
@Service
public class ChatServiceImpl implements ChatService {
@Resource
private SimpMessagingTemplate messagingTemplate;
@Resource
private ChatMessageService chatMessageService;
@Resource
private ChatMessageConvert chatMessageConvert;
@Value("${file-manager.strategy}")
private String fileStorageStrategyStrategy;
/**
* 单播、广播发送消息和文件
*
* @param chatMessageDTO
*/
@Override
public void saveMessage(ChatMessageDTO chatMessageDTO) {
ChatMessage chatMessage = chatMessageConvert.dtoToPo(chatMessageDTO);
MessageTypeEnum messageTypeEnum = chatMessage.getMessageTypeEnum();
boolean fileMessageType = MessageTypeEnum.isFileMessageType(messageTypeEnum);
if (fileMessageType)
chatMessage.setFileStorageStrategyEnum(FileStorageStrategyEnum.valueOf(fileStorageStrategyStrategy));
chatMessage.setSendDateTime(new Date());
chatMessageService.save(chatMessage);
boolean blankRecipient = StringUtils.isBlank(chatMessage.getRecipient());
if (fileMessageType && !blankRecipient) {
messagingTemplate.convertAndSendToUser(chatMessage.getRecipient(), "/queue/file", chatMessage);
} else if (fileMessageType && blankRecipient) {
messagingTemplate.convertAndSend("/topic/file");
} else if (MessageTypeEnum.TEXT.equals(messageTypeEnum) && !blankRecipient) {
//发送指定用户消息拼接后的路径/user/{chatMessage.getRecipient()}/queue/private
messagingTemplate.convertAndSendToUser(chatMessage.getRecipient(), "/queue/greeting", chatMessage);
} else if (MessageTypeEnum.TEXT.equals(messageTypeEnum) && blankRecipient) {
messagingTemplate.convertAndSend("/topic/public", chatMessage);
}
}
@Override
public IPage<ChatMessageVO> getChatHistory(ChatMessageQuery chatMessageQuery) {
AccountInfo accountInfo = (AccountInfo) StpUtil.getSession().get(UcConstant.SESSION_ACCOUNT_INFO);
String loginId = accountInfo.getAccountId();
LambdaQueryWrapper<ChatMessage> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.isBlank(chatMessageQuery.getFriendId())) {//广播消息
//我发送和收到的广播
wrapper.and(item -> item.eq(ChatMessage::getSender, loginId).or().eq(ChatMessage::getRecipient, loginId));
} else {//单播消息
//我发送和收到的单播
wrapper.and(item -> item
.eq(ChatMessage::getSender, loginId)
.eq(ChatMessage::getRecipient, chatMessageQuery.getFriendId())
.or()
.eq(ChatMessage::getSender, chatMessageQuery.getFriendId())
.eq(ChatMessage::getRecipient, loginId)
);
}
if (chatMessageQuery.getMessageTypeEnum() != null) {//查询具体文件类型
wrapper.eq(ChatMessage::getMessageTypeEnum, chatMessageQuery.getMessageTypeEnum());
}
if (!Objects.isNull(chatMessageQuery.getSendDateTime())) {
Date sendDateTime = chatMessageQuery.getSendDateTime();
Date date1 = DateUtil.parse(DateUtil.formatDate(sendDateTime) + " 00:00:00");
Date date2 = DateUtil.parse(DateUtil.formatDate(sendDateTime) + " 23:59:59");
wrapper.between(ChatMessage::getSendDateTime, date1, date2);
}
wrapper.orderByDesc(ChatMessage::getSendDateTime);
Page<ChatMessage> chatMessagePage = new Page<>(chatMessageQuery.getPageIndex(), chatMessageQuery.getPageSize());
Page<ChatMessage> page = chatMessageService.page(chatMessagePage, wrapper);
Collections.sort(page.getRecords(), Comparator.comparing(ChatMessage::getSendDateTime));
Page<ChatMessageVO> voPage = chatMessageConvert.poToVoPage(page);
return voPage;
}
}
用户在线状态接口
package com.inspur.message.controller;
import com.inspur.common.core.response.R;
import com.inspur.message.domain.po.UserOnlineStatus;
import com.inspur.message.service.OnlineStatusService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@Api(tags = "用户在线状态")
@RestController
@RequestMapping("/online-status")
public class OnlineStatusController {
@Resource
private OnlineStatusService onlineStatusService;
@ApiOperation("上线")
@GetMapping("/loggedIn")
public R userLoggedIn(@RequestParam("loginName") String loginName) {
onlineStatusService.userLoggedIn(loginName);
return R.ok();
}
@ApiOperation("下线")
@GetMapping("/loggedOut")
public R userLoggedOut(@RequestParam("loginName") String loginName) {
onlineStatusService.userLoggedOut(loginName);
return R.ok();
}
@ApiOperation("获取用户在线状态")
@GetMapping("/history")
public R<List<UserOnlineStatus>> history() {
List<UserOnlineStatus> onlineUsers = onlineStatusService.getOnlineUsers();
return R.ok(onlineUsers);
}
}
package com.inspur.message.service;
import com.inspur.message.domain.po.UserOnlineStatus;
import java.util.List;
public interface OnlineStatusService {
void userLoggedIn(String username);
void userLoggedOut(String username);
List<UserOnlineStatus> getOnlineUsers();
}
package com.inspur.message.service.impl;
import com.inspur.message.domain.po.UserOnlineStatus;
import com.inspur.message.service.OnlineStatusService;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class OnlineStatusServiceImpl implements OnlineStatusService {
private static final String ONLINE_STATUS_KEY = "onlineStatus";
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private SimpMessagingTemplate messagingTemplate;
@Override
public void userLoggedIn(String username) {
updateOnlineStatus(username, true);
notifyOnlineStatusChange(username, true);
}
@Override
public void userLoggedOut(String username) {
updateOnlineStatus(username, false);
notifyOnlineStatusChange(username, false);
}
@Override
public List<UserOnlineStatus> getOnlineUsers() {
HashOperations<String, String, Boolean> hashOperations = redisTemplate.opsForHash();
Map<String, Boolean> entries = hashOperations.entries(ONLINE_STATUS_KEY);
return entries.entrySet().stream()
.map(entry -> new UserOnlineStatus(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
private void updateOnlineStatus(String username, boolean online) {
redisTemplate.opsForHash().put(ONLINE_STATUS_KEY, username, online);
}
private void notifyOnlineStatusChange(String username, boolean online) {
messagingTemplate.convertAndSend("/topic/onlineStatus", new UserOnlineStatus(username, online));
}
}
Gateway配置
package com.inspur.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
@Component
public class WebsocketFilter implements GlobalFilter, Ordered {
public final static String DEFAULT_FILTER_PATH = "/ws/info";
public final static String DEFAULT_FILTER_WEBSOCKET = "websocket";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getRequest().getHeaders().getUpgrade();
URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
String scheme = requestUrl.getScheme();
//如果不是ws的请求直接通过
if (!"ws".equals(scheme) && !"wss".equals(scheme)) {
return chain.filter(exchange);
//如果是/ws/info的请求,把它还原成http请求。
} else if (DEFAULT_FILTER_PATH.equals(requestUrl.getPath())) {
String wsScheme = convertWsToHttp(scheme);
URI wsRequestUrl = UriComponentsBuilder.fromUri(requestUrl).scheme(wsScheme).build().toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, wsRequestUrl);
//如果是sockJS降级后的http请求,把它还原成http请求,也就是地址{transport}不为websocket的所有请求
} else if (!requestUrl.getPath().contains(DEFAULT_FILTER_WEBSOCKET)) {
String wsScheme = convertWsToHttp(scheme);
URI wsRequestUrl = UriComponentsBuilder.fromUri(requestUrl).scheme(wsScheme).build().toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, wsRequestUrl);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 2;
}
static String convertWsToHttp(String scheme) {
scheme = scheme.toLowerCase();
return "ws".equals(scheme) ? "http" : "wss".equals(scheme) ? "https" : scheme;
}
}
Nacos配置
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
# 配置消息模块的接口
- id: icm-message-service
uri: lb://icm-message-service
predicates:
- Path=/api/msg/**
filters:
- StripPrefix=2
# 配置websocket连接
- id: icm-message-service
uri: lb:ws://icm-message-service
predicates:
- Path=/ws/**
Nginx配置转发
如何部署到服务器上后,使用nginx进行端口转发,还需要对nginx进行如下配置,否则会跨域错误
upstream cas_server{
server 172.31.4.167:31001;
}
server {
listen 45000;
server_name localhost;
location /ws {
proxy_pass http://cas_server/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
WebSocket前端代码
<html>
<body>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> -->
<!-- 引用本地js库适合没有网络的环境 -->
<script src="lib/sockjs1.6.1.min.js"></script>
<script src="lib/stomp.min.js"></script>
<script>
var authToken = '7LZobHFmKjKbgMqZO9IKskyta69NFj6SB3Fw0jBK401ISUZimeFz7tGdKjwImdE2';
// WebSocket connection setup
// 请求参数中加入token,网关鉴权
var socket = new SockJS('http://127.0.0.1:31001/ws?token='+authToken,null,{
timeout: 10000
});
var stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log('连接成功!!!!!')
// Subscription to the topic
stompClient.subscribe('/topic/public', function (response) {
var message = JSON.parse(response.body);
// Handle received message, e.g., update UI
console.log(message);
});
stompClient.subscribe('/topic/file', function (response) {
var message = JSON.parse(response.body);
console.log(message);
});
stompClient.subscribe('/user/1727512379732152322/queue/greeting', function (response) {
var message = JSON.parse(response.body);
console.log(message)
});
stompClient.subscribe('/user/1727512379732152322/queue/file', function (response) {
var message = JSON.parse(response.body);
console.log(message)
});
//测试发送
sendMessage();
});
// 发送消息
function sendMessage() {
var message = { sender: "1730428514659909634", recipient: "1727512379732152322", messageTypeEnum: "TEXT", content: "Hello, World!", fileId: "" };
stompClient.send("/app/chat/send", {}, JSON.stringify(message));
}
</script>
<!-- UI elements and input for sending messages -->
</body>
</html>
bug问题
本机测试没事,部署到服务器上,连接很慢
修改服务器主机,etc/host文件添加即可。
使用websocket后端Access-Control-Allow-Origin不能配置为*
后端配置了Access-Control-Allow-Origin为*,js脚本会出问题,js脚本传过去此值为null