Spring Cloud Gateway整合基于STOMP协议的WebSocket实战及遇到问题解决

本实例介绍了Spring Cloud Gateway整合基于STOMP协议的WebSocket的实现。开发了聊天功能,和用户在线状态。解决了协议gateway整合websocket出现的问题

技术点

  • Spring Cloud Gateway
  • Nacos
  • WebSocket
  • STOMP

WebSocket与STOMP协议详解

1. WebSocket

WebSocket 是一种通信协议,提供了在客户端和服务器之间建立全双工通信的功能。这意味着客户端和服务器可以在任意时间相互发送数据,而不必遵循请求-响应的传统模式(如HTTP)。

特点:
  • 双向通信:与传统的HTTP不同,WebSocket允许服务器主动向客户端推送数据,而不必等待客户端请求。

  • 持久连接:WebSocket连接一旦建立,可以一直保持连接,直到任意一方关闭它。这减少了频繁建立连接的开销。

  • 低延迟:由于WebSocket消除了HTTP请求的头部开销和连接延迟,因此它适合于对延迟敏感的应用,如在线游戏、股票交易和聊天应用。

工作流程:
  1. 连接建立:客户端通过HTTP的升级机制(Upgrade header)请求建立WebSocket连接。

  2. 数据传输:一旦连接建立,客户端和服务器可以通过这个连接双向传输数据,使用一种轻量级的帧格式。

  3. 连接关闭:任意一方可以随时关闭连接,通知对方连接已关闭。

使用场景:
  • 实时聊天应用

  • 实时数据流(如股票行情、体育比赛更新)

  • 在线多人游戏

  • 实时协作工具(如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

### 更换 Ubuntu 默认软件源为国内镜像 对于希望提升 `apt` 命令执行效率和稳定性的情况,更改至国内镜像是一个有效的方法。针对 Ubuntu 系统,默认情况下会使用官方的国外服务器作为软件源。然而,在网络条件不佳的情况下,这可能会导致下载速度缓慢甚至失败。 为了将 Ubuntu 的默认软件源更改为国内镜像源,具体操作如下: #### 获取当前系统的代号名称 由于不同版本的 Ubuntu 使用不同的代号(Codename),因此首先需要确认正在使用的 Ubuntu 版本对应的代号是什么。可以通过运行以下命令来获取该信息[^3]: ```bash lsb_release -c ``` 假设输出显示的是 `focal` 或其他任何特定于所用 Ubuntu 发行版的名字,则后续配置文件中的路径应与此相匹配。 #### 编辑 APT 源列表文件 接着打开 `/etc/apt/sources.list` 文件进行编辑。可以采用文本编辑器如 nano 来完成此任务: ```bash sudo nano /etc/apt/sources.list ``` 删除原有条目并替换成目标国内镜像站点的内容。以下是几个常用的国内镜像站地址模板之一——清华大学开源软件镜像库为例: 对于 Ubuntu 20.04 (Focal Fossa),可参照下面格式修改: ``` deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse ... ``` 注意:上述 URL 中的 "focal" 应当被替换为你自己系统实际的 codename。 保存更改后的文件后退出编辑模式。 #### 更新本地包索引 最后一步是刷新本地缓存以使新的设置生效。通过终端输入下列指令实现这一点: ```bash sudo apt update ``` 此时已经成功完成了从默认国际源切换到更快捷稳定的国内镜像的过程[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愤怒的代码

如果您有受益,欢迎打赏博主😊

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值