从零构建跨端即时通讯:基于UniApp与RuoYi的WebSocket聊天室实战

最近在帮一个初创团队搭建内部协作工具时,他们提出了一个看似简单却颇具挑战的需求:要在现有的管理系统中集成一个轻量级的即时聊天功能,要求既能支持移动端也能在PC端使用,并且要能快速集成到他们基于RuoYi开发的后台框架中。经过几轮技术选型,我们最终确定了UniApp + RuoYi + WebSocket的技术栈组合。这个方案不仅满足了跨平台的需求,还能充分利用团队已有的技术积累。

如果你也在寻找一种既能快速上手,又具备良好扩展性的即时通讯实现方案,那么这篇文章正是为你准备的。我将带你从零开始,一步步构建一个完整的WebSocket聊天室,涵盖从后端服务配置到前端消息处理的全过程,特别聚焦于如何在实际项目中处理多用户群发、连接管理等核心问题。

1. 后端架构:构建稳健的WebSocket服务

1.1 环境准备与依赖配置

在开始编写WebSocket服务之前,我们需要确保开发环境已经准备就绪。对于使用RuoYi框架的项目来说,这通常意味着一个基于Spring Boot的Java后端环境。

首先,在项目的pom.xml文件中添加必要的依赖。虽然原始资料中只提到了spring-boot-starter-websocket,但在实际生产环境中,我们还需要考虑更多因素:

<!-- WebSocket核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- 用于JSON序列化,处理消息传输 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.25</version>
</dependency>

<!-- 可选:添加监控和健康检查 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

注意:依赖版本的选择需要根据你的Spring Boot版本进行匹配。如果使用RuoYi的最新版本,建议先查看其父pom中定义的版本号,避免版本冲突。

配置完成后,我们需要创建一个配置类来启用WebSocket支持。这里有一个关键点容易被忽略:WebSocket配置需要与Spring Security的配置协调工作,否则可能会遇到连接被拦截的问题。

1.2 WebSocket服务端核心实现

创建WebSocket服务端时,我们需要考虑几个关键问题:如何管理连接会话、如何处理并发访问、如何设计消息格式。下面是一个经过优化的实现方案:

package com.yourproject.websocket;

import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@Component
@ServerEndpoint("/ws/chat/{userId}")
public class ChatWebSocketEndpoint {
    
    // 使用线程安全的Map存储会话
    private static final ConcurrentHashMap<String, Session> SESSION_MAP = 
        new ConcurrentHashMap<>();
    
    // 使用原子类确保在线计数的线程安全
    private static final AtomicInteger ONLINE_COUNT = new AtomicInteger(0);
    
    private String currentUserId;
    private Session currentSession;
    
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.currentUserId = userId;
        this.currentSession = session;
        
        // 将新连接添加到会话映射中
        SESSION_MAP.put(userId, session);
        
        // 原子性增加在线人数
        int count = ONLINE_COUNT.incrementAndGet();
        
        // 广播用户上线通知
        broadcastSystemMessage(userId + " 已上线", userId);
        
        log.info("用户 {} 连接成功,当前在线人数: {}", userId, count);
    }
    
    @OnClose
    public void onClose() {
        // 从映射中移除会话
        SESSION_MAP.remove(currentUserId);
        
        // 原子性减少在线人数
        int count = ONLINE_COUNT.decrementAndGet();
        
        // 广播用户下线通知
        broadcastSystemMessage(currentUserId + " 已离线", currentUserId);
        
        log.info("用户 {} 断开连接,剩余在线人数: {}", currentUserId, count);
    }
    
    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            // 解析消息内容
            ChatMessage chatMessage = JSON.parseObject(message, ChatMessage.class);
            
            // 根据消息类型处理
            switch (chatMessage.getMessageType()) {
                case "TEXT":
                    handleTextMessage(chatMessage);
                    break;
                case "IMAGE":
                    handleImageMessage(chatMessage);
                    break;
                case "SYSTEM":
                    handleSystemMessage(chatMessage);
                    break;
                default:
                    log.warn("未知的消息类型: {}", chatMessage.getMessageType());
            }
        } catch (Exception e) {
            log.error("消息处理失败: {}", e.getMessage());
            sendError(session, "消息格式错误");
        }
    }
    
    private void handleTextMessage(ChatMessage message) {
        // 检查是否为群发消息
        if ("ALL".equals(message.getTargetId())) {
            // 群发给所有在线用户
            broadcastMessage(message);
        } else {
            // 私聊消息,发送给指定用户
            sendToUser(message.getTargetId(), message);
        }
    }
    
    // 广播消息给所有用户
    private void broadcastMessage(ChatMessage message) {
        SESSION_MAP.forEach((userId, session) -> {
            if (!userId.equals(message.getSenderId())) {
                sendMessage(session, message);
            }
        });
    }
    
    // 发送消息给指定用户
    private void sendToUser(String targetUserId, ChatMessage message) {
        Session targetSession = SESSION_MAP.get(targetUserId);
        if (targetSession != null && targetSession.isOpen()) {
            sendMessage(targetSession, message);
        } else {
            // 用户不在线,可以存储到数据库或缓存中
            log.info("用户 {} 不在线,消息已存储", targetUserId);
        }
    }
    
    // 发送消息的通用方法
    private void sendMessage(Session session, ChatMessage message) {
        try {
            session.getBasicRemote().sendText(JSON.toJSONString(message));
        } catch (IOException e) {
            log.error("发送消息失败: {}", e.getMessage());
        }
    }
}

这个实现相比基础版本有几个重要改进:

  1. 使用ConcurrentHashMap替代CopyOnWriteArraySet,在频繁读写场景下性能更好
  2. 使用AtomicInteger管理在线人数,避免并发问题
  3. 增加了消息类型区分,支持文本、图片、系统消息等多种格式
  4. 实现了更完善的错误处理机制

1.3 安全配置与连接管理

在RuoYi框架中,Spring Security默认会拦截所有请求,因此我们需要特别配置WebSocket端点的访问权限:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            // 允许WebSocket连接无需认证
            .antMatchers("/ws/**").permitAll()
            // 其他配置保持不变
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .csrf().disable();
    }
}

提示:在生产环境中,建议为WebSocket连接添加token验证机制,而不是完全开放。可以在连接建立时验证token,确保只有合法用户能够建立连接。

连接管理方面,我们还需要考虑几个实际问题:

心跳机制:WebSocket连接可能会因为网络问题或长时间空闲而断开。实现心跳机制可以保持连接活跃:

// 在WebSocket端点中添加心跳处理
@OnMessage
public void onMessage(String message, Session session) {
    if ("HEARTBEAT".equals(message)) {
        // 收到心跳包,更新最后活跃时间
        updateLastActiveTime(currentUserId);
        sendMessage(session, "HEARTBEAT_ACK");
        return;
    }
    // ... 其他消息处理逻辑
}

// 定时检查并清理失效连接
@Scheduled(fixedRate = 30000) // 每30秒执行一次
public void checkConnections() {
    long currentTime = System.currentTimeMillis();
    SESSION_MAP.entrySet().removeIf(entry -> {
        long lastActive = getLastActiveTime(entry.getKey());
        if (currentTime - lastActive > 120000) { // 2分钟无活动
            try {
                entry.getValue().close();
                return true;
            } catch (IOException e) {
                log.error("关闭失效连接失败: {}", e.getMessage());
                return false;
            }
        }
        return false;
    });
}

连接数限制:为了防止单个用户建立过多连接,我们可以添加连接数限制:

// 在onOpen方法中添加连接数检查
private static final int MAX_CONNECTIONS_PER_USER = 3;
private static final ConcurrentHashMap<String, AtomicInteger> USER_CONNECTION_COUNT = 
    new ConcurrentHashMap<>();

@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
    // 检查用户连接数
    int count = USER_CONNECTION_COUNT
        .computeIfAbsent(userId, k -> new AtomicInteger(0))
        .incrementAndGet();
    
    if (count > MAX_CONNECTIONS_PER_USER) {
        try {
            session.close(new CloseReason(
                CloseReason.CloseCodes.TRY_AGAIN_LATER,
                "连接数超过限制"
            ));
            USER_CONNECTION_COUNT.get(userId).decrementAndGet();
            return;
        } catch (IOException e) {
            log.error("关闭连接失败: {}", e.getMessage());
        }
    }
    
    // ... 其他初始化逻辑
}

2. 前端实现:UniApp中的WebSocket客户端

2.1 UniApp项目配置与基础结构

创建UniApp项目时,我建议使用HBuilderX的最新版本,它提供了更好的开发体验和调试支持。项目结构可以这样组织:

chat-project/
├── pages/
│   ├── index/
│   │   ├── index.vue      # 聊天主页面
│   │   └── index.scss
│   └── login/
│       └── login.vue      # 登录页面
├── static/
│   ├── icons/             # 图标资源
│   └── images/            # 图片资源
├── common/
│   ├── websocket.js       # WebSocket封装
│   └── utils.js          # 工具函数
└── manifest.json          # 应用配置

在manifest.json中,我们需要配置WebSocket权限:

{
  "name": "ChatApp",
  "appid": "__UNI__XXXXXX",
  "description": "即时通讯应用",
  "versionName": "1.0.0",
  "versionCode": "100",
  "transformPx": false,
  "networkTimeout": {
    "request": 15000,
    "connectSocket": 20000,
    "uploadFile": 60000,
    "downloadFile": 60000
  },
  "permission": {
    "scope.userLocation": {
      "desc": "你的位置信息将用于显示位置相关功能"
    }
  }
}

2.2 WebSocket连接管理与消息处理

在UniApp中处理WebSocket连接,我推荐创建一个专门的封装类,而不是在每个页面中直接调用原生API。这样做的好处是代码更清晰,也更容易维护。

首先创建common/websocket.js:

class WebSocketManager {
  constructor() {
    this.socketTask = null
    this.isConnected = false
    this.reconnectCount = 0
    this.maxReconnectCount = 5
    this.reconnectTimer = null
    this.messageQueue = []
    this.listeners = new Map()
    this.heartbeatInterval = null
  }

  // 连接WebSocket
  connect(options) {
    return new Promise((resolve, reject) => {
      const {
        url,
        userId,
        token,
        onOpen,
        onMessage,
        onError,
        onClose
      } = options

      // 构建完整的WebSocket URL
      const wsUrl = `${url}/${userId}?token=${token}&platform=uniapp`

      // 关闭现有连接
      if (this.socketTask) {
        this.disconnect()
      }

      // 创建WebSocket连接
      this.socketTask = uni.connectSocket({
        url: wsUrl,
        success: () => {
          console.log('WebSocket连接创建成功')
        },
        fail: (err) => {
          reject(new Error(`连接创建失败: ${err.errMsg}`))
        }
      })

      // 监听连接打开事件
      uni.onSocketOpen((res) => {
        console.log('WebSocket连接已打开')
        this.isConnected = true
        this.reconnectCount = 0
        
        // 发送积压的消息
        this.flushMessageQueue()
        
        // 开始心跳检测
        this.startHeartbeat()
        
        // 调用用户定义的回调
        if (typeof onOpen === 'function') {
          onOpen(res)
        }
        
        resolve(res)
      })

      // 监听消息事件
      uni.onSocketMessage((res) => {
        const data = this.parseMessage(res.data)
        
        // 处理心跳响应
        if (data.type === 'HEARTBEAT_ACK') {
          this.lastHeartbeatAck = Date.now()
          return
        }
        
        // 调用用户定义的回调
        if (typeof onMessage === 'function') {
          onMessage(data)
        }
        
        // 触发特定类型的监听器
        this.triggerListeners(data.type, data)
      })

      // 监听错误事件
      uni.onSocketError((err) => {
        console.error('WebSocket连接错误:', err)
        this.isConnected = false
        this.stopHeartbeat()
        
        if (typeof onError === 'function') {
          onError(err)
        }
        
        // 尝试重连
        this.scheduleReconnect()
      })

      // 监听关闭事件
      uni.onSocketClose((res) => {
        console.log('WebSocket连接已关闭:', res)
        this.isConnected = false
        this.stopHeartbeat()
        
        if (typeof onClose === 'function') {
          onClose(res)
        }
        
        // 如果不是主动断开,尝试重连
        if (res.code !== 1000) {
          this.scheduleReconnect()
        }
      })
    })
  }

  // 发送消息
  send(message) {
    const messageData = {
      type: 'TEXT',
      content: message,
      timestamp: Date.now(),
      platform: 'uniapp'
    }

    if (this.isConnected && this.socketTask) {
      uni.sendSocketMessage({
        data: JSON.stringify(messageData),
        success: () => {
          console.log('消息发送成功:', message)
        },
        fail: (err) => {
          console.error('消息发送失败:', err)
          // 加入重试队列
          this.messageQueue.push(messageData)
        }
      })
    } else {
      // 连接未建立,加入队列
      this.messageQueue.push(messageData)
      console.log('连接未就绪,消息已加入队列')
    }
  }

  // 发送特定类型的消息
  sendMessage(type, data, target = 'ALL') {
    const messageData = {
      type,
      data,
      target,
      timestamp: Date.now(),
      platform: 'uniapp'
    }

    if (this.isConnected) {
      uni.sendSocketMessage({
        data: JSON.stringify(messageData)
      })
    } else {
      this.messageQueue.push(messageData)
    }
  }

  // 断开连接
  disconnect() {
    this.stopHeartbeat()
    
    if (this.socketTask) {
      uni.closeSocket({
        code: 1000,
        reason: '用户主动断开'
      })
      this.socketTask = null
    }
    
    this.isConnected = false
    this.reconnectCount = 0
    
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer)
      this.reconnectTimer = null
    }
  }

  // 开始心跳检测
  startHeartbeat() {
    this.stopHeartbeat()
    
    this.heartbeatInterval = setInterval(() => {
      if (this.isConnected) {
        this.sendMessage('HEARTBEAT', {})
        
        // 检查上次心跳响应时间
        if (this.lastHeartbeatAck) {
          const timeSinceLastAck = Date.now() - this.lastHeartbeatAck
          if (timeSinceLastAck > 30000) { // 30秒未收到响应
            console.warn('心跳超时,尝试重连')
            this.disconnect()
            this.scheduleReconnect()
          }
        }
      }
    }, 15000) // 每15秒发送一次心跳
  }

  // 停止心跳检测
  stopHeartbeat() {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval)
      this.heartbeatInterval = null
    }
  }

  // 安排重连
  scheduleReconnect() {
    if (this.reconnectCount >= this.maxReconnectCount) {
      console.error('达到最大重连次数,停止重连')
      return
    }

    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer)
    }

    this.reconnectCount++
    const delay = Math.min(1000 * Math.pow(2, this.reconnectCount), 30000)

    this.reconnectTimer = setTimeout(() => {
      console.log(`第${this.reconnectCount}次尝试重连...`)
      this.connect(this.lastOptions).catch(err => {
        console.error('重连失败:', err)
        this.scheduleReconnect()
      })
    }, delay)
  }

  // 清空消息队列
  flushMessageQueue() {
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift()
      this.sendMessage(message.type, message.data, message.target)
    }
  }

  // 解析消息
  parseMessage(rawData) {
    try {
      return JSON.parse(rawData)
    } catch (error) {
      console.error('消息解析失败:', error)
      return {
        type: 'RAW',
        content: rawData,
        timestamp: Date.now()
      }
    }
  }

  // 添加消息监听器
  addListener(type, callback) {
    if (!this.listeners.has(type)) {
      this.listeners.set(type, [])
    }
    this.listeners.get(type).push(callback)
  }

  // 移除消息监听器
  removeListener(type, callback) {
    if (this.listeners.has(type)) {
      const callbacks = this.listeners.get(type)
      const index = callbacks.indexOf(callback)
      if (index > -1) {
        callbacks.splice(index, 1)
      }
    }
  }

  // 触发监听器
  triggerListeners(type, data) {
    if (this.listeners.has(type)) {
      this.listeners.get(type).forEach(callback => {
        try {
          callback(data)
        } catch (error) {
          console.error('监听器执行错误:', error)
        }
      })
    }
  }
}

// 创建单例实例
const websocketManager = new WebSocketManager()

export default websocketManager

这个WebSocket管理器提供了以下关键功能:

  1. 自动重连机制:在网络不稳定时自动尝试重新连接
  2. 消息队列:在连接断开时缓存消息,连接恢复后自动发送
  3. 心跳检测:保持连接活跃,及时检测连接状态
  4. 事件监听系统:支持按消息类型注册监听器
  5. 完整的错误处理:提供详细的错误信息和恢复机制

2.3 聊天界面设计与实现

现在我们来创建聊天主界面。这个界面需要处理消息的发送、接收和显示,同时提供良好的用户体验。

创建pages/index/index.vue:

<template>
  <view class="chat-container">
    <!-- 顶部导航栏 -->
    <view class="chat-header">
      <view class="header-left">
        <text class="online-count">在线: {{ onlineCount }}人</text>
      </view>
      <view class="header-center">
        <text class="chat-title">群聊({{ currentRoom }})</text>
      </view>
      <view class="header-right">
        <button class="menu-btn" @tap="showMenu">···</button>
      </view>
    </view>

    <!-- 消息列表区域 -->
    <scroll-view 
      class="message-list" 
      scroll-y 
      :scroll-top="scrollTop"
      scroll-with-animation
      @scrolltoupper="loadHistory"
    >
      <!-- 加载更多提示 -->
      <view v-if="loadingHistory" class="load-more">
        <text>加载中...</text>
      </view>

      <!-- 历史消息分割线 -->
      <view v-if="showHistoryDivider" class="history-divider">
        <text>以上是历史消息</text>
      </view>

      <!-- 消息列表 -->
      <view 
        v-for="(message, index) in messages" 
        :key="message.id || index"
        :class="['message-item', message.senderId === userId ? 'message-right' : 'message-left']"
      >
        <!-- 系统消息 -->
        <view v-if="message.type === 'SYSTEM'" class="system-message">
          <text class="system-text">{{ message.content }}</text>
          <text class="system-time">{{ formatTime(message.timestamp) }}</text>
        </view>

        <!-- 文本消息 -->
        <view v-else-if="message.type === 'TEXT'" class="text-message">
          <!-- 对方消息 -->
          <view v-if="message.senderId !== userId" class="other-message">
            <image 
              v-if="showAvatar" 
              class="avatar" 
              :src="getAvatar(message.senderId)" 
              mode="aspectFill"
            />
            <view class="message-content">
              <text class="sender-name">{{ getSenderName(message.senderId) }}</text>
              <view class="bubble bubble-left">
                <text class="message-text">{{ message.content }}</text>
                <text class="message-time">{{ formatTime(message.timestamp) }}</text>
              </view>
            </view>
          </view>

          <!-- 自己发送的消息 -->
          <view v-else class="my-message">
            <view class="message-content">
              <view class="bubble bubble-right">
                <text class="message-text">{{ message.content }}</text>
                <text class="message-time">{{ formatTime(message.timestamp) }}</text>
              </view>
            </view>
            <image 
              v-if="showAvatar" 
              class="avatar" 
              :src="getAvatar(userId)" 
              mode="aspectFill"
            />
          </view>
        </view>

        <!-- 图片消息 -->
        <view v-else-if="message.type === 'IMAGE'" class="image-message">
          <view :class="['image-container', message.senderId === userId ? 'image-right' : 'image-left']">
            <image 
              class="chat-image" 
              :src="message.content" 
              mode="widthFix"
              @tap="previewImage(message.content)"
              :lazy-load="true"
            />
            <text class="image-time">{{ formatTime(message.timestamp) }}</text>
          </view>
        </view>
      </view>

      <!-- 新消息提示 -->
      <view v-if="hasNewMessage" class="new-message-tip" @tap="scrollToBottom">
        <text>有新消息 ↓</text>
      </view>
    </scroll-view>

    <!-- 输入区域 -->
    <view class="input-area">
      <!-- 功能按钮 -->
      <view class="function-buttons">
        <button class="func-btn" @tap="toggleEmoji">
          <text>😊</text>
        </button>
        <button class="func-btn" @tap="chooseImage">
          <text>📷</text>
        </button>
        <button class="func-btn" @tap="showMoreFunctions">
          <text>+</text>
        </button>
      </view>

      <!-- 文本输入 -->
      <view class="text-input-container">
        <textarea 
          v-model="inputText"
          class="text-input"
          placeholder="输入消息..."
          :focus="isInputFocus"
          @focus="onInputFocus"
          @blur="onInputBlur"
          @confirm="sendTextMessage"
          :adjust-position="true"
          maxlength="500"
          auto-height
        />
        <text class="text-count">{{ inputText.length }}/500</text>
      </view>

      <!-- 发送按钮 -->
      <button 
        class="send-btn" 
        :disabled="!inputText.trim()" 
        @tap="sendTextMessage"
        :class="{ 'send-btn-active': inputText.trim() }"
      >
        <text>发送</text>
      </button>
    </view>

    <!-- 表情面板 -->
    <view v-if="showEmojiPanel" class="emoji-panel">
      <scroll-view class="emoji-list" scroll-y>
        <view class="emoji-row" v-for="(row, rowIndex) in emojiList" :key="rowIndex">
          <text 
            v-for="emoji in row" 
            :key="emoji"
            class="emoji-item"
            @tap="insertEmoji(emoji)"
          >
            {{ emoji }}
          </text>
        </view>
      </scroll-view>
      <button class="emoji-close" @tap="toggleEmoji">
        <text>关闭</text>
      </button>
    </view>

    <!-- 更多功能面板 -->
    <view v-if="showMorePanel" class="more-panel">
      <view class="more-grid">
        <view class="more-item" @tap="takePhoto">
          <view class="more-icon">📸</view>
          <text class="more-text">拍照</text>
        </view>
        <view class="more-item" @tap="chooseFile">
          <view class="more-icon">📎</view>
          <text class="more-text">文件</text>
        </view>
        <view class="more-item" @tap="sendLocation">
          <view class="more-icon">📍</view>
          <text class="more-text">位置</text>
        </view>
        <view class="more-item" @tap="startVoiceCall">
          <view class="more-icon">📞</view>
          <text class="more-text">语音</text>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
import websocketManager from '@/common/websocket.js'
import { formatTime, debounce } from '@/common/utils.js'

export default {
  data() {
    return {
      // 用户信息
      userId: '',
      userName: '',
      userAvatar: '',
      
      // 聊天状态
      messages: [],
      inputText: '',
      isInputFocus: false,
      showEmojiPanel: false,
      showMorePanel: false,
      
      // 滚动控制
      scrollTop: 0,
      lastScrollTop: 0,
      hasNewMessage: false,
      
      // 在线状态
      onlineCount: 0,
      onlineUsers: [],
      currentRoom: '默认群聊',
      
      // 加载状态
      loadingHistory: false,
      showHistoryDivider: false,
      pageSize: 20,
      currentPage: 1,
      hasMoreHistory: true,
      
      // 配置
      showAvatar: true,
      emojiList: this.generateEmojiList()
    }
  },
  
  onLoad(options) {
    // 获取用户信息
    this.userId = options.userId || 'user_' + Date.now()
    this.userName = options.userName || '用户' + this.userId.slice(-4)
    
    // 初始化WebSocket连接
    this.initWebSocket()
    
    // 加载历史消息
    this.loadHistoryMessages()
    
    // 监听键盘高度变化
    this.listenKeyboard()
  },
  
  onUnload() {
    // 清理资源
    this.removeKeyboardListener()
    websocketManager.removeListener('USER_LIST', this.handleUserList)
    websocketManager.removeListener('SYSTEM', this.handleSystemMessage)
    websocketManager.disconnect()
  },
  
  methods: {
    // 初始化WebSocket连接
    initWebSocket() {
      const wsUrl = 'ws://your-server.com/ws/chat'
      const token = uni.getStorageSync('auth_token')
      
      websocketManager.connect({
        url: wsUrl,
        userId: this.userId,
        token: token,
        onOpen: (res) => {
          console.log('连接成功')
          this.showToast('连接成功')
          
          // 请求在线用户列表
          this.requestUserList()
        },
        onMessage: (data) => {
          this.handleIncomingMessage(data)
        },
        onError: (err) => {
          console.error('连接错误:', err)
          this.showToast('连接出错,正在重连...')
        },
        onClose: (res) => {
          console.log('连接关闭:', res)
          if (res.code !== 1000) {
            this.showToast('连接断开,正在重连...')
          }
        }
      })
      
      // 注册消息监听器
      websocketManager.addListener('USER_LIST', this.handleUserList)
      websocketManager.addListener('SYSTEM', this.handleSystemMessage)
      websocketManager.addListener('TEXT', this.handleTextMessage)
      websocketManager.addListener('IMAGE', this.handleImageMessage)
    },
    
    // 处理收到的消息
    handleIncomingMessage(data) {
      // 根据消息类型分发处理
      switch (data.type) {
        case 'TEXT':
          this.handleTextMessage(data)
          break
        case 'IMAGE':
          this.handleImageMessage(data)
          break
        case 'SYSTEM':
          this.handleSystemMessage(data)
          break
        case 'USER_LIST':
          this.handleUserList(data)
          break
        default:
          console.warn('未知消息类型:', data.type)
      }
    },
    
    // 处理文本消息
    handleTextMessage(data) {
      const message = {
        id: 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2),
        type: 'TEXT',
        senderId: data.senderId,
        senderName: data.senderName || '用户' + data.senderId.slice(-4),
        content: data.content,
        timestamp: data.timestamp || Date.now(),
        status: 'received'
      }
      
      this.addMessage(message)
      
      // 如果用户正在查看历史消息,不自动滚动到底部
      if (this.isAtBottom()) {
        this.scrollToBottom()
      } else {
        this.hasNewMessage = true
      }
    },
    
    // 处理系统消息
    handleSystemMessage(data) {
      const message = {
        id: 'sys_' + Date.now(),
        type: 'SYSTEM',
        content: data.content,
        timestamp: data.timestamp || Date.now()
      }
      
      this.addMessage(message)
      this.showToast(data.content)
    },
    
    // 处理用户列表更新
    handleUserList(data) {
      this.onlineUsers = data.users || []
      this.onlineCount = this.onlineUsers.length
    },
    
    // 发送文本消息
    sendTextMessage() {
      if (!this.inputText.trim()) return
      
      const message = {
        type: 'TEXT',
        content: this.inputText.trim(),
        timestamp: Date.now()
      }
      
      // 先添加到本地消息列表
      const localMessage = {
        id: 'local_' + Date.now(),
        type: 'TEXT',
        senderId: this.userId,
        senderName: this.userName,
        content: message.content,
        timestamp: message.timestamp,
        status: 'sending'
      }
      
      this.addMessage(localMessage)
      this.scrollToBottom()
      
      // 发送到服务器
      websocketManager.sendMessage('TEXT', message.content, 'ALL')
      
      // 清空输入框
      this.inputText = ''
      
      // 更新消息状态(在实际项目中,应该等待服务器确认)
      setTimeout(() => {
        const index = this.messages.findIndex(m => m.id === localMessage.id)
        if (index !== -1) {
          this.$set(this.messages, index, {
            ...this.messages[index],
            status: 'sent'
          })
        }
      }, 1000)
    },
    
    // 添加消息到列表
    addMessage(message) {
      this.messages.push(message)
      
      // 限制消息数量,避免内存问题
      if (this.messages.length > 1000) {
        this.messages = this.messages.slice(-500)
      }
    },
    
    // 滚动到底部
    scrollToBottom() {
      this.$nextTick(() => {
        // 使用一个较大的值确保滚动到底部
        this.scrollTop = 999999
        this.hasNewMessage = false
      })
    },
    
    // 检查是否在底部
    isAtBottom() {
      // 这里需要根据实际滚动位置判断
      // 简化实现:如果最近没有手动滚动,认为在底部
      return Date.now() - this.lastScrollTime > 1000
    },
    
    // 加载历史消息
    async loadHistoryMessages() {
      if (this.loadingHistory || !this.hasMoreHistory) return
      
      this.loadingHistory = true
      
      try {
        // 模拟API调用
        const mockHistory = this.generateMockHistory()
        
        if (mockHistory.length > 0) {
          // 将历史消息插入到开头
          this.messages = [...mockHistory, ...this.messages]
          this.currentPage++
          
          // 显示历史消息分割线
          if (this.currentPage > 1) {
            this.showHistoryDivider = true
          }
        } else {
          this.hasMoreHistory = false
        }
      } catch (error) {
        console.error('加载历史消息失败:', error)
        this.showToast('加载失败')
      } finally {
        this.loadingHistory = false
      }
    },
    
    // 选择图片
    async chooseImage() {
      try {
        const res = await uni.chooseImage({
          count: 1,
          sizeType: ['compressed'],
          sourceType: ['album', 'camera']
        })
        
        if (res.tempFilePaths.length > 0) {
          const imagePath = res.tempFilePaths[0]
          
          // 先显示本地图片
          const localMessage = {
            id: 'img_local_' + Date.now(),
            type: 'IMAGE',
            senderId: this.userId,
            content: imagePath,
            timestamp: Date.now(),
            status: 'uploading'
          }
          
          this.addMessage(localMessage)
          this.scrollToBottom()
          
          // 模拟上传过程
          setTimeout(() => {
            const index = this.messages.findIndex(m => m.id === localMessage.id)
            if (index !== -1) {
              this.$set(this.messages, index, {
                ...this.messages[index],
                status: 'sent'
              })
              
              // 发送到服务器(实际项目中需要上传到服务器)
              websocketManager.sendMessage('IMAGE', {
                url: imagePath,
                width: 300,
                height: 200
              }, 'ALL')
            }
          }, 1500)
        }
      } catch (error) {
        console.error('选择图片失败:', error)
        this.showToast('选择图片失败')
      }
    },
    
    // 预览图片
    previewImage(url) {
      uni.previewImage({
        urls: [url],
        current: 0
      })
    },
    
    // 插入表情
    insertEmoji(emoji) {
      this.inputText += emoji
      this.isInputFocus = true
    },
    
    // 生成表情列表
    generateEmojiList() {
      const emojis = ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇',
                     '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚',
                     '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🤩']
      
      const rows = []
      for (let i = 0; i < emojis.length; i += 8) {
        rows.push(emojis.slice(i, i + 8))
      }
      return rows
    },
    
    // 生成模拟历史消息
    generateMockHistory() {
      const mockMessages = []
      const users = ['user_001', 'user_002', 'user_003', 'user_004']
      const contents = [
        '大家好!',
        '这个聊天室功能很实用',
        '有人在线吗?',
        '测试消息',
        'WebSocket连接很稳定',
        'UniApp开发体验不错',
        'RuoYi框架集成很方便',
        '继续加油!'
      ]
      
      const now = Date.now()
      for (let i = 0; i < 10; i++) {
        const userId = users[Math.floor(Math.random() * users.length)]
        mockMessages.unshift({
          id: 'hist_' + (now - i * 60000),
          type: 'TEXT',
          senderId: userId,
          senderName: '用户' + userId.slice(-3),
          content: contents[Math.floor(Math.random() * contents.length)],
          timestamp: now - i * 60000,
          status: 'sent'
        })
      }
      
      return mockMessages
    },
    
    // 工具方法
    formatTime,
    
    getAvatar(userId) {
      // 实际项目中应该从用户信息中获取
      return `/static/avatars/${userId}.jpg`
    },
    
    getSenderName(userId) {
      const user = this.onlineUsers.find(u => u.id === userId)
      return user ? user.name : '用户' + userId.slice(-4)
    },
    
    showToast(title) {
      uni.showToast({
        title,
        icon: 'none',
        duration: 2000
      })
    },
    
    // 请求用户列表
    requestUserList() {
      websocketManager.sendMessage('GET_USERS', {}, 'SERVER')
    },
    
    // 监听键盘
    listenKeyboard() {
      // UniApp中键盘高度变化监听
      uni.onKeyboardHeightChange(res => {
        if (res.height > 0) {
          // 键盘弹出时滚动到底部
          setTimeout(() => {
            this.scrollToBottom()
          }, 300)
        }
      })
    },
    
    removeKeyboardListener() {
      // 清理键盘监听
    },
    
    // 其他功能方法
    toggleEmoji() {
      this.showEmojiPanel = !this.showEmojiPanel
      this.showMorePanel = false
      if (this.showEmojiPanel) {
        this.isInputFocus = false
      }
    },
    
    showMoreFunctions() {
      this.showMorePanel = !this.showMorePanel
      this.showEmojiPanel = false
    },
    
    onInputFocus() {
      this.showEmojiPanel = false
      this.showMorePanel = false
    },
    
    onInputBlur() {
      // 输入框失去焦点时的处理
    },
    
    takePhoto() {
      // 拍照功能
      uni.chooseImage({
        sourceType: ['camera'],
        success: (res) => {
          this.chooseImageSuccess(res)
        }
      })
    },
    
    chooseFile() {
      // 选择文件
      uni.chooseMessageFile({
        count: 1,
        type: 'all',
        success: (res) => {
          console.log('选择的文件:', res)
        }
      })
    },
    
    sendLocation() {
      // 发送位置
      uni.chooseLocation({
        success: (res) => {
          console.log('选择的位置:', res)
        }
      })
    },
    
    startVoiceCall() {
      // 开始语音通话
      this.showToast('语音通话功能开发中...')
    }
  }
}
</script>

<style scoped>
.chat-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background-color: #f5f5f5;
}

.chat-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 16px;
  background-color: #ffffff;
  border-bottom: 1px solid #e0e0e0;
  height: 60px;
  box-sizing: border-box;
}

.header-center {
  flex: 1;
  text-align: center;
}

.chat-title {
  font-size: 18px;
  font-weight: 600;
  color: #333333;
}

.online-count {
  font-size: 12px;
  color: #666666;
}

.menu-btn {
  background: none;
  border: none;
  font-size: 20px;
  color: #333333;
  padding: 4px 12px;
}

.message-list {
  flex: 1;
  padding: 12px;
  overflow: hidden;
}

.message-item {
  margin-bottom: 16px;
}

.system-message {
  text-align: center;
  margin: 8px 0;
}

.system-text {
  font-size: 12px;
  color: #999999;
  background-color: rgba(0, 0, 0, 0.05);
  padding: 4px 12px;
  border-radius: 12px;
}

.system-time {
  display: block;
  font-size: 10px;
  color: #cccccc;
  margin-top: 2px;
}

.other-message, .my-message {
  display: flex;
  align-items: flex-start;
  max-width: 80%;
}

.other-message {
  align-self: flex-start;
}

.my-message {
  align-self: flex-end;
  flex-direction: row-reverse;
}

.avatar {
  width: 40px;
  height: 40px;
  border-radius: 20px;
  margin: 0 8px;
}

.message-content {
  display: flex;
  flex-direction: column;
  max-width: calc(100% - 56px);
}

.sender-name {
  font-size: 12px;
  color: #666666;
  margin-bottom: 4px;
  margin-left: 4px;
}

.bubble {
  padding: 10px 14px;
  border-radius: 18px;
  position: relative;
  word-break: break-word;
}

.bubble-left {
  background-color: #ffffff;
  border-top-left-radius: 4px;
}

.bubble-right {
  background-color: #95ec69;
  border-top-right-radius: 4px;
}

.message-text {
  font-size: 16px;
  line-height: 1.4;
}

.message-time {
  display: block;
  font-size: 10px;
  color: #999999;
  margin-top: 4px;
  text-align: right;
}

.input-area {
  display: flex;
  align-items: flex-end;
  padding: 8px 12px;
  background-color: #ffffff;
  border-top: 1px solid #e0e0e0;
  min-height: 60px;
}

.function-buttons {
  display: flex;
  margin-right: 8px;
}

.func-btn {
  background: none;
  border: none;
  font-size: 24px;
  padding: 8px;
  margin-right: 4px;
}

.text-input-container {
  flex: 1;
  position: relative;
  background-color: #f8f8f8;
  border-radius: 20px;
  padding: 8px 12px;
  min-height: 40px;
  max-height: 120px;
}

.text-input {
  width: 100%;
  font-size: 16px;
  line-height: 1.4;
  max-height: 100px;
}

.text-count {
  position: absolute;
  right: 12px;
  bottom: 4px;
  font-size: 10px;
  color: #cccccc;
}

.send-btn {
  background-color: #e0e0e0;
  border: none;
  border-radius: 20px;
  padding: 8px 20px;
  margin-left: 8px;
  font-size: 16px;
  color: #999999;
  transition: all 0.3s;
}

.send-btn-active {
  background-color: #07c160;
  color: #ffffff;
}

.emoji-panel {
  position: absolute;
  bottom: 60px;
  left: 0;
  right: 0;
  background-color: #ffffff;
  border-top: 1px solid #e0e0e0;
  height: 200px;
}

.emoji-list {
  height: 160px;
  padding: 12px;
}

.emoji-row {
  display: flex;
  justify-content: space-around;
  margin-bottom: 12px;
}

.emoji-item {
  font-size: 24px;
  padding: 8px;
}

.more-panel {
  position: absolute;
  bottom: 60px;
  left: 0;
  right: 0;
  background-color: #ffffff;
  border-top: 1px solid #e0e0e0;
  padding: 20px;
}

.more-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 20px;
}

.more-item {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.more-icon {
  font-size: 32px;
  margin-bottom: 8px;
}

.more-text {
  font-size: 12px;
  color: #666666;
}

.new-message-tip {
  position: absolute;
  bottom: 80px;
  left: 50%;
  transform: translateX(-50%);
  background-color: rgba(0, 0, 0, 0.7);
  color: white;
  padding: 8px 16px;
  border-radius: 20px;
  font-size: 12px;
  z-index: 100;
}

.load-more {
  text-align: center;
  padding: 12px;
  color: #999999;
  font-size: 14px;
}

.history-divider {
  text-align: center;
  padding: 12px;
  color: #999999;
  font-size: 12px;
  position: relative;
}

.history-divider::before,
.history-divider::after {
  content: '';
  position: absolute;
  top: 50%;
  width: 30%;
  height: 1px;
  background-color: #e0e0e0;
}

.history-divider::before {
  left: 10%;
}

.history-divider::after {
  right: 10%;
}
</style>

这个聊天界面实现了以下功能:

  1. 完整的消息展示:支持文本、图片、系统消息等多种类型
  2. 智能滚动:新消息自动滚动到底部,查看历史消息时显示新消息提示
  3. 丰富的交互:支持表情选择、图片发送、文件传输等功能
  4. 响应式设计:适配不同屏幕尺寸和设备
  5. 良好的用户体验:包括消息状态显示、发送反馈、错误处理等

3. 高级功能与优化策略

3.1 消息持久化与历史记录

在实际项目中,消息的持久化存储是必不可少的。我们需要将聊天记录保存到数据库中,以便用户查看历史消息。

后端消息存储实现:

@Service
public class MessageService {
    
    @Autowired
    private MessageMapper messageMapper;
    
    // 保存消息到数据库
    public void saveMessage(ChatMessage message) {
        MessageRecord record = new MessageRecord();
        record.setMessageId(generateMessageId());
        record.setSenderId(message.getSenderId());
        record.setReceiverId(message.getReceiverId());
        record.setMessageType(message.getMessageType());
        record.setContent(message.getContent());
        record.setTimestamp(message.getTimestamp());
        record.setStatus(MessageStatus.SENT);
        
        messageMapper.insert(record);
    }
    
    // 获取历史消息
    public List<ChatMessage> getHistoryMessages(String userId, String targetId, 
                                               Long startTime, int limit) {
        List<MessageRecord> records = messageMapper.selectHistory(
            userId, targetId, startTime, limit
        );
        
        return records.stream()
            .map(this::convertToChatMessage)
            .collect(Collectors.toList());
    }
    
    // 消息分页查询
    public PageResult<ChatMessage> getMessagesByPage(String userId, String targetId, 
                                                    int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        List<MessageRecord> records = messageMapper.selectByUserAndTarget(userId, targetId);
        
        PageInfo<MessageRecord> pageInfo = new PageInfo<>(records);
        List<ChatMessage> messages = records.stream()
            .map(this::convertToChatMessage)
            .collect(Collectors.toList());
        
        return new PageResult<>(
            messages, 
            pageInfo.getTotal(), 
            pageInfo.getPageNum(), 
            pageInfo.getPageSize()
        );
    }
    
    // 消息状态更新(已读、撤回等)
    public void updateMessageStatus(String messageId, MessageStatus status) {
        MessageRecord record = new MessageRecord();
        record.setMessageId(messageId);
        record.setStatus(status);
        record.setUpdateTime(new Date());
        
        messageMapper.updateStatus(record);
    }
    
    // 生成唯一消息ID
    private String generateMessageId() {
        return "msg_" + System.currentTimeMillis() + "_" + 
               UUID.randomUUID().toString().substring(0, 8);
    }
    
    private ChatMessage convertToChatMessage(MessageRecord record) {
        ChatMessage message = new ChatMessage();
        message.setMessageId(record.getMessageId());
        message.setSenderId(record.getSenderId());
        message.setReceiverId(record.getReceiverId());
        message.setMessageType(record.getMessageType());
        message.setContent(record.getContent());
        message.setTimestamp(record.getTimestamp().getTime());
        message.setStatus(record.getStatus());
        
        return message;
    }
}

前端历史消息加载优化:

// 在WebSocket管理器中添加历史消息加载功能
class WebSocketManager {
  // ... 其他代码 ...
  
  // 加载历史消息
  async loadHistoryMessages(params) {
    const { userId, targetId, beforeTime, limit = 20 } = params
    
    try {
      // 先检查本地缓存
      const cachedMessages = this.getCachedMessages(userId, targetId, beforeTime, limit)
      if (cachedMessages.length >= limit) {
        return cachedMessages
      }
      
      // 从服务器加载
      const response = await uni.request({
        url: `${this.baseUrl}/api/messages/history`,
        method: 'GET',
        data: {
          userId,
          targetId,
          beforeTime,
          limit
        },
        header: {
          'Authorization': `Bearer ${this.token}`
        }
      })
      
      if (response.statusCode === 200) {
        const messages = response.data.data || []
        
        // 缓存消息
        this.cacheMessages(userId, targetId, messages)
        
        return messages
      } else {
        throw new Error('加载历史消息失败')
      }
    } catch (error) {
      console.error('加载历史消息失败:', error)
      throw error
    }
  }
  
  // 本地消息缓存
  cacheMessages(userId, targetId, messages) {
    const cacheKey = `messages_${userId}_${targetId}`
    const cached = uni.getStorageSync(cacheKey) || []
    
    // 合并并去重
    const allMessages = [...messages, ...cached]
      .filter((msg, index, self) => 
        index === self.findIndex(m => m.messageId === msg.messageId)
      )
      .sort((a, b) => b.timestamp - a.timestamp) // 按时间倒序
      .slice(0, 1000) // 最多缓存1000条
    
    uni.setStorageSync(cacheKey, allMessages)
  }
  
  getCachedMessages(userId, targetId, beforeTime, limit) {
    const cacheKey = `messages_${userId}_${targetId}`
    const cached = uni.getStorageSync(cacheKey) || []
    
    return cached
      .filter(msg => beforeTime ? msg.timestamp < beforeTime : true)
      .slice(0, limit)
  }
}

3.2 消息状态同步与确认机制

在即时通讯中,确保消息的可靠传递至关重要。我们需要实现消息状态同步机制:

// 消息状态枚举
public enum MessageStatus {
    SENDING,     // 发送中
    SENT,        // 已发送
    DELIVERED,   // 已送达
    READ,        // 已读
    FAILED       // 发送失败
}

// 消息确认服务
@Service
public class MessageAckService {
    
    private static final ConcurrentHashMap<String, MessageDelivery> DELIVERY_MAP = 
        new ConcurrentHashMap<>();
    
    // 发送消息并等待确认
    public void sendWithAck(ChatMessage message, Session session) {
        String ackId = generateAckId();
        message.setAckId(ackId);
        
        // 保存到待确认队列
        DELIVERY_MAP.put(ackId, new MessageDelivery(message, session));
        
        // 发送消息
        sendMessage(session, message);
        
        // 启动超时检查
        scheduleAckTimeout(ackId);
    }
    
    // 处理消息确认
    public void handleAck(String ackId) {
        MessageDelivery delivery = DELIVERY_MAP.remove(ackId);
        if (delivery != null) {
            // 更新消息状态为已送达
            updateMessageStatus(delivery.getMessage().getMessageId(), MessageStatus.DELIVERED);
            
            // 发送送达回执给发送者
            sendDeliveryReceipt(delivery.getSession(), ackId);
        }
    }
    
    // 处理已读回执
    public void handleReadReceipt(String messageId, String readerId) {
        // 更新消息状态为已读
        updateMessageStatus(messageId, MessageStatus.READ);
        
        // 发送已读回执给发送者
        MessageRecord message = getMessageById(messageId);
        if (message != null) {
            sendReadReceipt(message.getSenderId(), messageId, readerId);
        }
    }
    
    private void scheduleAckTimeout(String ackId) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                MessageDelivery delivery = DELIVERY_MAP.get(ackId);
                if (delivery != null) {
                    // 确认超时,标记为发送失败
                    updateMessageStatus(delivery.getMessage().getMessageId(), MessageStatus.FAILED);
                    DELIVERY_MAP.remove(ackId);
                    
                    // 通知发送者
                    sendDeliveryFailed(delivery.getSession(), ackId);
                }
            }
        }, 30000); // 30秒超时
    }
}

前端消息状态管理:

// 扩展消息管理器,添加状态管理
class MessageStatusManager {
  constructor() {
    this.pendingMessages = new Map()
    this.messageCallbacks = new Map()
  }
  
  // 发送消息并等待确认
  sendMessageWithAck(message, options = {}) {
    return new Promise((resolve, reject) => {
      const messageId = message.id || this.generateMessageId()
      const ackId = `ack_${messageId}`
      
      // 设置确认ID
      message.ackId = ackId
      
      // 保存到待确认队列
      this.pendingMessages.set(ackId, {
        message,
        timestamp: Date.now(),
        retryCount: 0,
        maxRetries: options.maxRetries || 3,
        resolve,
        reject
      })
      
      // 发送消息
      this.sendMessageInternal(message)
      
      // 设置超时检查
      this.setupAckTimeout(ackId, options.timeout || 30000)
    })
  }
  
  // 处理服务器确认
  handleServerAck(ackId) {
    const pending = this.pendingMessages.get(ackId)
    if (pending) {
      this.pendingMessages.delete(ackId)
      pending.resolve({
        success: true,
        ackId,
        timestamp: Date.now()
      })
      
      // 更新本地消息状态
      this.updateMessageStatus(pending.message.id, 'delivered')
    }
  }
  
  // 处理已读回执
  handleReadReceipt(messageId, readerId) {
    this.updateMessageStatus(messageId, 'read', { readerId })
    
    // 触发已读回调
    const callbacks = this.messageCallbacks.get(messageId)
    if (callbacks && callbacks.onRead) {
      callbacks.onRead(readerId)
    }
  }
  
  // 发送消息的内部方法
  sendMessageInternal(message) {
    const pending = this.pendingMessages.get(message.ackId)
    
    websocketManager.sendMessage(message.type, message.content, message.target)
      .then(() => {
        // 发送成功,更新状态为发送中
        this.updateMessageStatus(message.id, 'sending')
      })
      .catch(error => {
        // 发送失败,重试或标记为失败
        if (pending && pending.retryCount < pending.maxRetries) {
          pending.retryCount++
          setTimeout(() => {
            this.sendMessageInternal(message)
          }, 1000 * pending.retryCount) // 指数退避
        } else {
          this.updateMessageStatus(message.id, 'failed', { error })
          if (pending) {
            pending.reject(new Error(`消息发送失败: ${error.message}`))
            this.pendingMessages.delete(message.ackId)
          }
        }
      })
  }
  
  // 设置消息状态更新回调
  onMessageStatusChange(messageId, callbacks) {
    this.messageCallbacks.set(messageId, callbacks)
  }
  
  // 更新消息状态
  updateMessageStatus(messageId, status, extra = {}) {
    // 更新本地存储
    const messages = this.getLocalMessages()
    const index = messages.findIndex(msg => msg.id === messageId)
    if (index !== -1) {
      messages[index].status = status
      messages[index].statusTime = Date.now()
      Object.assign(messages[index], extra)
      this.saveLocalMessages(messages)
    }
    
    // 触发状态变更事件
    this.triggerStatusChange(messageId, status, extra)
  }
  
  // 生成消息ID
  generateMessageId() {
    return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
  }
}

3.3 性能优化与监控

连接池管理:

@Component
public class WebSocketConnectionPool {
    
    private static final ConcurrentHashMap<String, SessionWrapper> SESSION_POOL = 
        new ConcurrentHashMap<>();
    
    // 会话包装类,添加额外信息
    private static class SessionWrapper {
        private Session session;
        private String userId;
        private String deviceId;
        private long connectTime;
        private long lastActiveTime;
        private String clientVersion;
        
        // 检查会话是否有效
        public boolean isValid() {
            return session != null && session.isOpen();
        }
        
        // 更新活跃时间
        public void updateActiveTime() {
            this.lastActiveTime = System.currentTimeMillis();
        }
    }
    
    // 添加会话
    public void addSession(String userId, String deviceId, Session session, 
                          String clientVersion) {
        SessionWrapper wrapper = new SessionWrapper();
        wrapper.session = session;
        wrapper.userId = userId;
        wrapper.deviceId = deviceId;
        wrapper.connectTime = System.currentTimeMillis();
        wrapper.lastActiveTime = System.currentTimeMillis();
        wrapper.clientVersion = clientVersion;
        
        String key = generateKey(userId, deviceId);
        SESSION_POOL.put(key, wrapper);
        
        log.info("会话添加成功: userId={}, deviceId={}, 当前连接数: {}", 
                 userId, deviceId, SESSION_POOL.size());
    }
    
    // 获取会话
    public Session getSession(String userId, String deviceId) {
        String key = generateKey(userId, deviceId);
        SessionWrapper wrapper = SESSION_POOL.get(key);
        return wrapper != null && wrapper.isValid() ? wrapper.session : null;
    }
    
    // 获取用户的所有会话
    public List<Session> getUserSessions(String userId) {
        return SESSION_POOL.entrySet().stream()
            .filter(entry -> entry.getKey().startsWith(userId + "|"))
            .map(entry -> entry.getValue())
            .filter(SessionWrapper::isValid)
            .map(wrapper -> wrapper.session)
            .collect(Collectors.toList());
    }
    
    // 移除会话
    public void removeSession(String userId, String deviceId) {
        String key = generateKey(userId, deviceId);
        SessionWrapper wrapper = SESSION_POOL.remove(key);
        if (wrapper != null) {
            log.info("会话移除成功: userId={}, deviceId={}, 剩余连接数: {}", 
                     userId, deviceId, SESSION_POOL.size());
        }
    }
    
    // 清理无效会话
    @Scheduled(fixedRate = 60000) // 每分钟执行一次
    public void cleanupInvalidSessions() {
        long now = System.currentTimeMillis();
        List<String> toRemove = new ArrayList<>();
        
        SESSION_POOL.forEach((key, wrapper) -> {
            // 检查会话是否已关闭
            if (!wrapper.isValid()) {
                toRemove.add(key);
                return;
            }
            
            // 检查是否超时(5分钟无活动)
            if (now - wrapper.lastActiveTime > 300000) {
                try {
                    wrapper.session.close(new CloseReason(
                        CloseReason.CloseCodes.GOING_AWAY,
                        "连接超时"
                    ));
                } catch (IOException e) {
                    log.error("关闭超时会话失败: {}", e.getMessage());
                }
                toRemove.add(key);
            }
        });
        
        // 移除无效会话
        toRemove.forEach(SESSION_POOL::remove);
        
        if (!toRemove.isEmpty()) {
            log.info("清理了 {} 个无效会话,剩余连接数: {}", 
                     toRemove.size(), SESSION_POOL.size());
        }
    }
    
    // 生成会话键
    private String generateKey(String userId, String deviceId) {
        return userId + "|" + deviceId;
    }
    
    // 获取连接统计信息
    public ConnectionStats getConnectionStats() {
        ConnectionStats stats = new ConnectionStats();
        stats.setTotalConnections(SESSION_POOL.size());
        
        // 按用户分组统计
        Map<String, Long> userConnections = SESSION_POOL.values().stream()
            .collect(Collectors.groupingBy(
                wrapper -> wrapper.userId,
                Collectors.counting()
            ));
        
        stats.setUserConnections(userConnections);
        stats.setAvgConnectionsPerUser(
            SESSION_POOL.size() / (double) userConnections.size()
        );
        
        return stats;
    }
}

前端性能监控:

// 性能监控工具
class PerformanceMonitor {
  constructor() {
    this.metrics = {
      connectionTime: 0,
      messageLatency: [],
      messageLoss: 0,
      reconnectCount: 0,
      errors: []
    }
    
    this.startTime = Date.now()
    this.messageSentTimes = new Map()
  }
  
  // 记录连接建立时间
  recordConnectionTime() {
    this.metrics.connectionTime = Date.now() - this.startTime
    this.reportMetric('connection_time', this.metrics.connectionTime)
  }
  
  // 记录消息发送
  recordMessageSend(messageId) {
    this.messageSentTimes.set(messageId, Date.now())
  }
  
  // 记录消息接收
  recordMessageReceive(messageId) {
    const sentTime = this.messageSentTimes.get(messageId)
    if (sentTime) {
      const latency = Date.now() - sentTime
      this.metrics.messageLatency.push(latency)
      this.messageSentTimes.delete(messageId)
      
      // 报告延迟
      this.reportMetric('message_latency', latency)
      
      // 计算平均延迟
      if (this.metrics.messageLatency.length % 10 === 0) {
        const avgLatency = this.metrics.messageLatency.reduce((a, b) => a + b, 0) / 
                         this.metrics.messageLatency.length
        this.reportMetric('avg_message_latency', avgLatency)
      }
    }
  }
  
  // 记录重连
  recordReconnect() {
    this.metrics.reconnectCount++
    this.reportMetric('reconnect_count', this.metrics.reconnectCount)
  }
  
  // 记录错误
  recordError(error) {
    this.metrics.errors.push({
      time: Date.now(),
      error: error.message,
      stack: error.stack
    })
    
    // 错误过多时报警
    if (this.metrics.errors.length > 10) {
      this.alert('错误次数过多,请检查网络连接')
    }
  }
  
  // 报告指标
  reportMetric(name, value) {
    // 在实际项目中,这里可以上报到监控系统
    console.log(`[Metric] ${name}: ${value}`)
    
    // 也可以存储到本地
    this.saveMetricToLocal(name, value)
  }
  
  // 获取性能报告
  getPerformanceReport() {
    const avgLatency = this.metrics.messageLatency.length > 0
      ? this.metrics.messageLatency.reduce((a, b) => a + b, 0) / 
        this.metrics.messageLatency.length
      : 0
    
    return {
      connectionTime: this.metrics.connectionTime,
      avgMessageLatency: avgLatency,
      totalMessages: this.metrics.messageLatency.length,
      reconnectCount: this.metrics.reconnectCount,
      errorCount: this.metrics.errors.length,
      uptime: Date.now() - this.startTime
    }
  }
  
  // 保存指标到本地
  saveMetricToLocal(name, value) {
    const metrics = uni.getStorageSync('websocket_metrics') || {}
    if (!metrics[name]) {
      metrics[name] = []
    }
    metrics[name].push({
      timestamp: Date.now(),
      value
    })
    
    // 只保留最近1000条记录
    if (metrics[name].length > 1000) {
      metrics[name] = metrics[name].slice(-1000)
    }
    
    uni.setStorageSync('websocket_metrics', metrics)
  }
  
  // 报警
  alert(message) {
    // 在实际项目中,这里可以触发UI提示或上报到监控系统
    console.warn(`[Alert] ${message}`)
    
    // 显示用户提示
    uni.showToast({
      title: message,
      icon: 'none',
      duration: 3000
    })
  }
}

// 在WebSocket管理器中集成性能监控
class WebSocketManager {
  constructor() {
    // ... 其他初始化代码 ...
    this.performanceMonitor = new PerformanceMonitor()
  }
  
  connect(options) {
    this.performanceMonitor.startTime = Date.now()
    
    return new Promise((resolve, reject) => {
      // ... 连接逻辑 ...
      
      uni.onSocketOpen((res) => {
        this.performanceMonitor.recordConnectionTime()
        // ... 其他逻辑 ...
      })
      
      uni.onSocketError((err) => {
        this.performanceMonitor.recordError(err)
        // ... 其他逻辑 ...
      })
      
      // ... 其他事件监听 ...
    })
  }
  
  sendMessage(type, data, target) {
    const messageId = this.generateMessageId()
    this.performanceMonitor.recordMessageSend(messageId)
    
    // ... 发送逻辑 ...
  }
  
  // 处理接收到的消息
  handleIncomingMessage(data) {
    if (data.messageId) {
      this.performanceMonitor.recordMessageReceive(data.messageId)
    }
    // ... 其他处理逻辑 ...
  }
}

4. 部署与运维实践

4.1 生产环境配置

在生产环境中部署WebSocket服务时,需要考虑以下几个关键方面:

Nginx反向代理配置:

http {
    # WebSocket连接需要较长的超时时间
    map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;
    }
    
    upstream websocket_backend {
        server 127.0.0.1:8080;
        server 127.0.0.1:8081;
        # 添加更多后端服务器
    }
    
    server {
        listen 80;
        server_name your-domain.com;
        
        # WebSocket连接
        location /ws/ {
            proxy_pass http://websocket_backend;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
            proxy_set_header Host $host;
            
            # 重要:设置较长的超时时间
            proxy_read_timeout 3600s;
            proxy_send_timeout 3600s;
            proxy_connect_timeout 75s;
            
            # 启用缓冲
            proxy_buffering off;
            proxy_buffer_size 4k;
            proxy_buffers 8 4k;
            
            # 启用压缩
            proxy_set_header Accept-Encoding gzip;
        }
        
        # 静态资源
        location / {
            root /path/to/static/files;
            index index.html;
            try_files $uri $uri/ /index.html;
        }
    }
}

Spring Boot应用配置:

# application-prod.yml
server:
  port: 8080
  compression:
    enabled: true
    mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
    min-response-size: 1024
  
spring:
  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/chat_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: your_username
    password: your_password
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
  
  # Redis配置(用于会话共享)
  redis:
    host: localhost
    port: 6379
    password: your_redis_password
    timeout: 10000
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
  
  # WebSocket配置
  websocket:
    max-sessions: 10000
    session-timeout: 3600000
    message-size-limit: 8192
    send-timeout: 5000
    send-buffer-size-limit: 512000

# 自定义配置
chat:
  websocket:
    # 心跳间隔(毫秒)
    heartbeat-interval: 30000

更多推荐