SSE线上通信504超时

# nginx反向代理配置    

# sse连接配置
    location /admin-api/sse {
        proxy_pass http://host.docker.internal:48081/admin-api/sse;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
        proxy_cache off;
        proxy_set_header Connection ''; # 确保 Nginx 不会断开 SSE 连接
        chunked_transfer_encoding on;  # 确保传输支持 chunked encoding
    }
# 如果使用nginx可以这样,但是如果是阿里云的alb等等中间件就不可以了



# 解决方案
思路:将SSE连接时间调整为网关超时时间的最大值,断开后自动重连即可

实际就是之前是永不超时,现在是超时自动重连

# 如果下面方案不适用,请参考如下连接,http2降级http1.1
https://blog.csdn.net/AinUser/article/details/154446622



# 上代码(支持分布式)
# 前端代码


import request from '@/config/axios'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { getAccessToken } from '@/utils/auth'
import { config } from '@/config/axios/config'

export const SSEApi = {

  // 创建SSE连接(支持超时和错误自动重连)
  createSseConnection: async (atId: number, userId: number, onMessage, onError?, onOpen?) => {
    const token = getAccessToken()
    const domain = `${config.base_url}`

    // 重连配置
    const maxRetries = 3 // 最大重连次数
    const initialRetryDelay = 1000 // 初始重连延迟 (ms)
    const maxRetryDelay = 30000 // 最大重连延迟 (ms)
    const connectionTimeout = 60 * 1000 // 连接超时时间 (ms)

    let retryCount = 0
    let retryDelay = initialRetryDelay
    let isClosed = false

    const connect = () => {
      return new Promise((resolve, reject) => {
        if (isClosed) {
          reject(new Error('连接已关闭'))
          return
        }

        const ctrl = new AbortController()
        let connectionEstablished = false
        let hasError = false

        // 设置连接超时
        const timeoutId = setTimeout(() => {
          if (!connectionEstablished) {
            hasError = true
            ctrl.abort()
            handleReconnect(userId)
          }
        }, connectionTimeout)

        fetchEventSource(domain + '/sse/connect/' + atId + '/' + userId, {
          method: 'get',
          openWhenHidden: true,
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${token}`
          },
          onopen: (response) => {
            clearTimeout(timeoutId)
            connectionEstablished = true
            retryCount = 0 // 重置重试计数
            retryDelay = initialRetryDelay // 重置重试延迟

            if (onOpen) {
              onOpen(response)
            }

            // 检查响应状态
            if (response.status >= 400) {
              hasError = true
              handleReconnect(userId)
            }
          },
          onmessage: (event) => {
            if (!hasError) {
              onMessage(event)
            }
          },
          onerror: (error) => {
            clearTimeout(timeoutId)
            hasError = true

            if (onError) {
              onError(error)
            }

            handleReconnect(userId)
          },
          signal: ctrl.signal
        }).catch((err) => {
          clearTimeout(timeoutId)
          if (!connectionEstablished) {
            handleReconnect(userId)
          }
        })
      })
    }

    // 处理重连逻辑
    const handleReconnect = (userId: number) => {
      if (isClosed || retryCount >= maxRetries) {
        console.error(`SSE连接失败,已达到最大重试次数 ${maxRetries}`)
        return
      }

      retryCount++
      console.log(`${userId} - SSE连接断开,${retryDelay / 1000}秒后第 ${retryCount} 次重连...`)

      setTimeout(() => {
        if (!isClosed) {
          // 指数退避策略,但不超过最大延迟
          retryDelay = Math.min(retryDelay * 2, maxRetryDelay)
          connect()
        }
      }, retryDelay)
    }

    // 开始连接
    connect()

    // 返回关闭函数
    return {
      close: () => {
        isClosed = true
      }
    }
  },

  // 向指定用户发送消息
  sendSseMessage : (fromUserId: number, toUserId: number, message: string) => {
    return request.post({ url: '/sse/send/' + fromUserId + '/' + toUserId, data: message })
  },

  // 关闭SSE连接
  closeSseConnection : (userId: number) => {
    return request.get({ url: '/sse/close/' + userId })
  }

}

# 后端代码(支持分布式)


@CrossOrigin
@RestController
@RequestMapping("/sse")
public class SSEController {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    // 添加Redis消息监听容器
    @Resource
    private RedisMessageListenerContainer redisMessageListenerContainer;
    
    private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();

    // 添加Redis消息频道常量
    private static final String CHANNEL_PREFIX = "sse:channel:";
    private static String USER_CONNECTION_KEY = "sse:connections";

    // Redis消息监听方法
    @PostConstruct
    public void initRedisListener() {
        // 创建消息监听器适配器
        MessageListenerAdapter listenerAdapter = new MessageListenerAdapter(new MessageListener() {
            @Override
            public void onMessage(Message message, byte[] pattern) {
                String channel = new String(message.getChannel());
                String userIds = channel.substring(CHANNEL_PREFIX.length());
                String fromUserId = userIds.split("-")[0];
                String toUserId = userIds.split("-")[1];
                String messageContent = new String(message.getBody());
                // 这里需要实现本地SSE连接的管理和消息发送
                System.out.println("用户[" + toUserId + "]收到Redis消息: " + messageContent);

                // 关键:从本地缓存获取 emitter 并发送消息
                SseEmitter emitter = emitters.get(toUserId);
                if (emitter != null) {
                    try {
                        emitter.send(SseEmitter.event()
                                .id(String.valueOf(System.currentTimeMillis()))
                                .data(fromUserId + ":" + messageContent) // 前端需要fromUserId开关麦
                                .reconnectTime(5000L));
                        System.out.println("向用户[" + toUserId + "]本地发送消息成功");
                    } catch (IOException e) {
                        System.out.println("向用户[" + toUserId + "]发送消息失败: " + e.getMessage());
                        // 处理发送异常(例如连接已断开)
                        emitters.remove(toUserId);
                        stringRedisTemplate.opsForSet().remove(USER_CONNECTION_KEY, toUserId);
                    }
                } else {
                    System.out.println("用户[" + toUserId + "]本地连接不存在,无法发送消息");
                }
            }
        });

        // 订阅所有用户的消息频道
        redisMessageListenerContainer.addMessageListener(listenerAdapter,
                PatternTopic.of(CHANNEL_PREFIX + "*"));
    }

    /**
     * 创建SSE连接
     */
    @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
    @GetMapping(value = "/connect/{atId}/{userId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter createConnection(@PathVariable String atId, @PathVariable String userId) {
        // 设置12小时后超时(单位:毫秒) 0L表示永不超时
        long timeout = 0L;
        SseEmitter emitter = new SseEmitter(timeout);
        if (!USER_CONNECTION_KEY.contains("-")) {
            USER_CONNECTION_KEY += "-" + atId;
        }

        // 注册回调函数
        emitter.onCompletion(() -> {
            System.out.println("[" + userId + "]连接完成");
            // 从Redis和本地缓存移除连接信息
            stringRedisTemplate.opsForSet().remove(USER_CONNECTION_KEY, userId);
            emitters.remove(userId);
        });

        emitter.onTimeout(() -> {
            System.out.println("[" + userId + "]连接超时");
            stringRedisTemplate.opsForSet().remove(USER_CONNECTION_KEY, userId);
            emitters.remove(userId);
        });

        emitter.onError(throwable -> {
            System.out.println("[" + userId + "]连接异常: " + throwable.getMessage());
            stringRedisTemplate.opsForSet().remove(USER_CONNECTION_KEY, userId);
            emitters.remove(userId);
        });

        // 将用户连接信息存储到Redis和本地缓存
        stringRedisTemplate.opsForSet().add(USER_CONNECTION_KEY, userId);
        emitters.put(userId, emitter);  // 添加此行,将emitter存入本地缓存
        System.out.println("用户[" + userId + "]SSE连接创建成功");

        return emitter;
    }
    
    /**
     * 向指定用户发送消息
     */
    @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
    @PostMapping("/send/{fromUserId}/{toUserId}")
    public boolean sendMessage(@PathVariable String fromUserId, @PathVariable String toUserId, @RequestBody String message) {
        // 检查用户是否在线
        Boolean isOnline = stringRedisTemplate.opsForSet().isMember(USER_CONNECTION_KEY, toUserId);
        if (Boolean.FALSE.equals(isOnline)) {
            System.out.println("用户[" + toUserId + "]未建立连接");
            return false;
        }
        try {
            // 通过Redis发布消息,所有节点都会收到
            String channel = CHANNEL_PREFIX + fromUserId + "-" + toUserId;
            stringRedisTemplate.convertAndSend(channel, message);
            System.out.println("向用户[" + toUserId + "]发送消息成功: " + message);
            return true;
        } catch (Exception e) {
            System.out.println("向用户[" + toUserId + "]发送消息失败: " + e.getMessage());
            return false;
        }
    }
    
    /**
     * 主动关闭连接
     */
    @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
    @GetMapping("/close/{userId}")
    public void closeConnection(@PathVariable String userId) {
        SseEmitter emitter = emitters.get(userId);
        if (emitter != null) {
            emitter.complete();
            emitters.remove(userId);
            stringRedisTemplate.opsForSet().remove(USER_CONNECTION_KEY, userId);
            System.out.println("用户[" + userId + "]连接已关闭");
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值