WebSocket实时通信:HarmonyOS开发中长连接方案

WebSocket实时通信:HarmonyOS开发中长连接方案

告别轮询的石器时代,WebSocket让你的应用拥有实时超能力

一、背景与动机:为什么需要WebSocket?

还记得早期的聊天应用是怎么实现的吗?客户端每隔几秒就问服务器:"有新消息吗?有新消息吗?"这就是HTTP轮询,效率极低,延迟又高。

WebSocket的出现彻底改变了这个局面。它在客户端和服务器之间建立一条持久化的双向通道,服务器有消息可以主动推送给客户端,无需客户端反复询问。这就像从"写信等回信"变成了"打电话"------实时、高效、自然。

WebSocket的核心优势:

全双工通信:客户端和服务器可以同时收发消息,不像HTTP那样一问一答。

低延迟:建立连接后,消息直达,没有HTTP头开销,延迟通常在毫秒级。

节省资源:不需要频繁建立/断开连接,服务器资源占用更低。

实时性强:服务器可以立即推送,用户无需等待下次轮询。

鸿蒙系统提供了@ohos.net.webSocket模块,完整支持WebSocket客户端功能。

二、核心原理:WebSocket工作流程

2.1 连接建立过程

服务器 客户端 服务器 客户端 #mermaid-svg-g9vChivBkaxULcHa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-g9vChivBkaxULcHa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-g9vChivBkaxULcHa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-g9vChivBkaxULcHa .error-icon{fill:#552222;}#mermaid-svg-g9vChivBkaxULcHa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-g9vChivBkaxULcHa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-g9vChivBkaxULcHa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-g9vChivBkaxULcHa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-g9vChivBkaxULcHa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-g9vChivBkaxULcHa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-g9vChivBkaxULcHa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-g9vChivBkaxULcHa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-g9vChivBkaxULcHa .marker.cross{stroke:#333333;}#mermaid-svg-g9vChivBkaxULcHa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-g9vChivBkaxULcHa p{margin:0;}#mermaid-svg-g9vChivBkaxULcHa .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-g9vChivBkaxULcHa text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-g9vChivBkaxULcHa .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-g9vChivBkaxULcHa .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-g9vChivBkaxULcHa .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-g9vChivBkaxULcHa .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-g9vChivBkaxULcHa #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-g9vChivBkaxULcHa .sequenceNumber{fill:white;}#mermaid-svg-g9vChivBkaxULcHa #sequencenumber{fill:#333;}#mermaid-svg-g9vChivBkaxULcHa #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-g9vChivBkaxULcHa .messageText{fill:#333;stroke:none;}#mermaid-svg-g9vChivBkaxULcHa .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-g9vChivBkaxULcHa .labelText,#mermaid-svg-g9vChivBkaxULcHa .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-g9vChivBkaxULcHa .loopText,#mermaid-svg-g9vChivBkaxULcHa .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-g9vChivBkaxULcHa .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-g9vChivBkaxULcHa .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-g9vChivBkaxULcHa .noteText,#mermaid-svg-g9vChivBkaxULcHa .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-g9vChivBkaxULcHa .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-g9vChivBkaxULcHa .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-g9vChivBkaxULcHa .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-g9vChivBkaxULcHa .actorPopupMenu{position:absolute;}#mermaid-svg-g9vChivBkaxULcHa .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-g9vChivBkaxULcHa .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-g9vChivBkaxULcHa .actor-man circle,#mermaid-svg-g9vChivBkaxULcHa line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-g9vChivBkaxULcHa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1. HTTP握手升级 2. WebSocket数据帧传输 3. Ping/Pong保活 4. 关闭连接 GET /chat HTTP/1.1\nUpgrade: websocket\nConnection: Upgrade\nSec-WebSocket-Key: xxxHTTP/1.1 101 Switching Protocols\nUpgrade: websocket\nConnection: Upgrade\nSec-WebSocket-Accept: yyy文本帧: "Hello"文本帧: "Hi there"二进制帧: 0x01, 0x02, 0x03Ping帧Pong帧Close帧 (code=1000)Close帧 (code=1000)

2.2 数据帧格式

WebSocket消息被封装在帧(Frame)中传输,帧格式设计精巧:

字段 大小 说明
FIN 1 bit 是否为最后一帧
RSV1-3 3 bits 保留位
Opcode 4 bits 操作码(文本/二进制/关闭等)
Mask 1 bit 是否掩码(客户端必须掩码)
Payload len 7/7+16/7+64 bits 负载长度
Masking key 0/32 bits 掩码密钥
Payload data 变长 实际数据

2.3 鸿蒙WebSocket API

typescript 复制代码
import webSocket from '@ohos.net.webSocket';

// 创建WebSocket连接
let ws = webSocket.createWebSocket();

// 连接服务器
await ws.connect('wss://api.example.com/ws');

// 发送消息
ws.send('Hello Server');

// 接收消息
ws.on('message', (err, value) => {
  console.info('收到消息:', value);
});

// 关闭连接
ws.close();

三、代码实战:三种典型场景

场景一:实时聊天应用

实现一个完整的聊天功能,包括连接、消息收发、重连机制。

typescript 复制代码
import webSocket from '@ohos.net.webSocket';
import { BusinessError } from '@ohos.base';

// 聊天消息类型
interface ChatMessage {
  type: 'text' | 'image' | 'file';
  content: string;
  sender: string;
  timestamp: number;
  messageId: string;
}

// 用户信息
interface UserInfo {
  userId: string;
  nickname: string;
  avatar: string;
}

// WebSocket聊天客户端
class ChatWebSocketClient {
  private ws: webSocket.WebSocket | null = null;
  private url: string;
  private token: string;
  private isConnected: boolean = false;
  private reconnectAttempts: number = 0;
  private maxReconnectAttempts: number = 5;
  private reconnectDelay: number = 1000;
  
  // 消息回调
  private onMessageCallback: ((msg: ChatMessage) => void) | null = null;
  private onConnectionChange: ((connected: boolean) => void) | null = null;
  
  constructor(url: string, token: string) {
    this.url = url;
    this.token = token;
  }
  
  // 连接服务器
  async connect(): Promise<boolean> {
    if (this.isConnected) {
      console.warn('WebSocket已连接');
      return true;
    }
  
    this.ws = webSocket.createWebSocket();
  
    try {
      // 构建带token的URL
      let wsUrl = `${this.url}?token=${this.token}`;
    
      // 连接服务器
      let result = await this.ws.connect(wsUrl, {
        header: {
          'Authorization': `Bearer ${this.token}`
        }
      });
    
      console.info('WebSocket连接成功');
      this.isConnected = true;
      this.reconnectAttempts = 0;
    
      // 设置事件监听
      this.setupEventListeners();
    
      // 通知连接状态变化
      this.onConnectionChange?.(true);
    
      return true;
    
    } catch (error) {
      let e = error as BusinessError;
      console.error('WebSocket连接失败:', e.message);
    
      // 尝试重连
      this.attemptReconnect();
    
      return false;
    }
  }
  
  // 设置事件监听
  private setupEventListeners(): void {
    if (!this.ws) return;
  
    // 接收消息
    this.ws.on('message', (err: BusinessError, value: string | ArrayBuffer) => {
      if (err) {
        console.error('消息接收错误:', err.message);
        return;
      }
    
      try {
        // 解析消息
        let message: ChatMessage;
      
        if (typeof value === 'string') {
          message = JSON.parse(value);
        } else {
          // 二进制消息,需要特殊处理
          let decoder = new TextDecoder();
          message = JSON.parse(decoder.decode(value));
        }
      
        console.info('收到消息:', message);
        this.onMessageCallback?.(message);
      
      } catch (error) {
        console.error('消息解析失败:', error);
      }
    });
  
    // 连接关闭
    this.ws.on('close', (err: BusinessError, value: webSocket.CloseResult) => {
      console.info(`WebSocket关闭: code=${value.code}, reason=${value.reason}`);
      this.isConnected = false;
      this.onConnectionChange?.(false);
    
      // 非正常关闭,尝试重连
      if (value.code !== 1000) {
        this.attemptReconnect();
      }
    });
  
    // 连接错误
    this.ws.on('error', (err: BusinessError) => {
      console.error('WebSocket错误:', err.message);
      this.isConnected = false;
      this.onConnectionChange?.(false);
    });
  }
  
  // 发送文本消息
  async sendTextMessage(content: string, sender: UserInfo): Promise<boolean> {
    if (!this.isConnected || !this.ws) {
      console.error('WebSocket未连接');
      return false;
    }
  
    let message: ChatMessage = {
      type: 'text',
      content: content,
      sender: sender.userId,
      timestamp: Date.now(),
      messageId: this.generateMessageId()
    };
  
    try {
      await this.ws.send(JSON.stringify(message));
      console.info('消息发送成功');
      return true;
    
    } catch (error) {
      console.error('消息发送失败:', error);
      return false;
    }
  }
  
  // 发送二进制数据(如图片)
  async sendBinaryMessage(data: ArrayBuffer, sender: UserInfo): Promise<boolean> {
    if (!this.isConnected || !this.ws) {
      return false;
    }
  
    try {
      await this.ws.send(data);
      return true;
    
    } catch (error) {
      console.error('二进制消息发送失败:', error);
      return false;
    }
  }
  
  // 尝试重连
  private attemptReconnect(): void {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('重连次数已达上限');
      return;
    }
  
    this.reconnectAttempts++;
    let delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
  
    console.info(`将在 ${delay}ms 后尝试第 ${this.reconnectAttempts} 次重连`);
  
    setTimeout(() => {
      this.connect();
    }, delay);
  }
  
  // 关闭连接
  async close(): Promise<void> {
    if (this.ws) {
      try {
        await this.ws.close(1000, 'Client closing');
        console.info('WebSocket已关闭');
      } catch (error) {
        console.error('关闭WebSocket失败:', error);
      }
    
      this.ws = null;
      this.isConnected = false;
    }
  }
  
  // 设置消息回调
  setOnMessage(callback: (msg: ChatMessage) => void): void {
    this.onMessageCallback = callback;
  }
  
  // 设置连接状态回调
  setOnConnectionChange(callback: (connected: boolean) => void): void {
    this.onConnectionChange = callback;
  }
  
  // 生成消息ID
  private generateMessageId(): string {
    return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
  
  // 获取连接状态
  getConnectionStatus(): boolean {
    return this.isConnected;
  }
}

// 聊天页面UI组件
@Entry
@Component
struct ChatPage {
  @State messages: ChatMessage[] = [];
  @State inputText: string = '';
  @State isConnected: boolean = false;
  @State currentUser: UserInfo = {
    userId: 'user_001',
    nickname: '张三',
    avatar: 'https://example.com/avatar.jpg'
  };
  
  private wsClient: ChatWebSocketClient | null = null;
  private scroller: Scroller = new Scroller();
  
  async aboutToAppear() {
    // 初始化WebSocket客户端
    this.wsClient = new ChatWebSocketClient(
      'wss://api.example.com/chat',
      'your_auth_token'
    );
  
    // 设置消息回调
    this.wsClient.setOnMessage((msg) => {
      this.messages.push(msg);
      // 滚动到底部
      this.scroller.scrollEdge(Edge.Bottom);
    });
  
    // 设置连接状态回调
    this.wsClient.setOnConnectionChange((connected) => {
      this.isConnected = connected;
    });
  
    // 连接服务器
    await this.wsClient.connect();
  }
  
  aboutToDisappear() {
    // 页面销毁时关闭连接
    this.wsClient?.close();
  }
  
  // 发送消息
  async sendMessage() {
    if (!this.inputText.trim()) return;
  
    // 添加本地消息(乐观更新)
    let localMsg: ChatMessage = {
      type: 'text',
      content: this.inputText,
      sender: this.currentUser.userId,
      timestamp: Date.now(),
      messageId: `local_${Date.now()}`
    };
  
    this.messages.push(localMsg);
    this.scroller.scrollEdge(Edge.Bottom);
  
    // 发送到服务器
    let success = await this.wsClient?.sendTextMessage(this.inputText, this.currentUser);
  
    if (!success) {
      // 发送失败,标记消息状态
      console.error('消息发送失败');
    }
  
    this.inputText = '';
  }
  
  build() {
    Column() {
      // 顶部状态栏
      Row() {
        Text('实时聊天')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
      
        // 连接状态指示
        Row() {
          Circle()
            .width(8)
            .height(8)
            .fill(this.isConnected ? '#7ED321' : '#D0021B')
        
          Text(this.isConnected ? '已连接' : '未连接')
            .fontSize(12)
            .fontColor(this.isConnected ? '#7ED321' : '#D0021B')
            .margin({ left: 4 })
        }
      }
      .width('100%')
      .padding(15)
      .backgroundColor('#F5F5F5')
    
      // 消息列表
      List({ scroller: this.scroller }) {
        ForEach(this.messages, (msg: ChatMessage) => {
          ListItem() {
            this.MessageItem(msg)
          }
        }, (msg: ChatMessage) => msg.messageId)
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 10, right: 10 })
    
      // 输入区域
      Row() {
        TextInput({ text: this.inputText, placeholder: '输入消息...' })
          .layoutWeight(1)
          .height(40)
          .onChange((value) => this.inputText = value)
          .onSubmit(() => this.sendMessage())
      
        Button('发送')
          .width(70)
          .height(40)
          .margin({ left: 10 })
          .enabled(this.isConnected)
          .onClick(() => this.sendMessage())
      }
      .width('100%')
      .padding(10)
      .backgroundColor('#FFFFFF')
    }
    .width('100%')
    .height('100%')
  }
  
  @Builder
  MessageItem(msg: ChatMessage) {
    // 判断是否为自己发送的消息
    let isSelf = msg.sender === this.currentUser.userId;
  
    Row() {
      if (isSelf) {
        Blank().layoutWeight(1)
      }
    
      Column() {
        if (!isSelf) {
          Text(msg.sender)
            .fontSize(12)
            .fontColor('#666')
            .margin({ bottom: 4 })
        }
      
        Text(msg.content)
          .fontSize(16)
          .padding(10)
          .borderRadius(8)
          .backgroundColor(isSelf ? '#4A90E2' : '#E8E8E8')
          .fontColor(isSelf ? '#FFFFFF' : '#000000')
      
        Text(this.formatTime(msg.timestamp))
          .fontSize(10)
          .fontColor('#999')
          .margin({ top: 4 })
      }
      .alignItems(isSelf ? HorizontalAlign.End : HorizontalAlign.Start)
    
      if (!isSelf) {
        Blank().layoutWeight(1)
      }
    }
    .width('100%')
    .margin({ top: 10 })
  }
  
  // 格式化时间
  private formatTime(timestamp: number): string {
    let date = new Date(timestamp);
    return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
  }
}

场景二:实时数据监控

监控服务器状态、股票行情等实时数据流。

typescript 复制代码
import webSocket from '@ohos.net.webSocket';

// 服务器状态数据
interface ServerStatus {
  cpu: number;          // CPU使用率
  memory: number;       // 内存使用率
  disk: number;         // 磁盘使用率
  network: {
    in: number;         // 入流量 KB/s
    out: number;        // 出流量 KB/s
  };
  timestamp: number;
}

// 实时监控客户端
class RealtimeMonitorClient {
  private ws: webSocket.WebSocket | null = null;
  private statusCallback: ((status: ServerStatus) => void) | null = null;
  private alertCallback: ((alert: string) => void) | null = null;
  
  // 连接监控服务
  async connect(serverUrl: string): Promise<boolean> {
    this.ws = webSocket.createWebSocket();
  
    try {
      await this.ws.connect(serverUrl);
    
      // 监听消息
      this.ws.on('message', (err, value) => {
        if (err || typeof value !== 'string') return;
      
        let data = JSON.parse(value);
      
        switch (data.type) {
          case 'status':
            this.statusCallback?.(data.payload as ServerStatus);
            break;
          case 'alert':
            this.alertCallback?.(data.payload as string);
            break;
        }
      });
    
      return true;
    
    } catch (error) {
      console.error('监控连接失败:', error);
      return false;
    }
  }
  
  // 订阅特定服务器
  async subscribeServer(serverId: string): Promise<void> {
    if (!this.ws) return;
  
    await this.ws.send(JSON.stringify({
      action: 'subscribe',
      serverId: serverId
    }));
  }
  
  // 设置状态回调
  onStatus(callback: (status: ServerStatus) => void): void {
    this.statusCallback = callback;
  }
  
  // 设置告警回调
  onAlert(callback: (alert: string) => void): void {
    this.alertCallback = callback;
  }
  
  // 关闭连接
  async close(): Promise<void> {
    if (this.ws) {
      await this.ws.close();
      this.ws = null;
    }
  }
}

// 服务器监控UI
@Entry
@Component
struct ServerMonitorPage {
  @State serverStatus: ServerStatus | null = null;
  @State alerts: string[] = [];
  @State isMonitoring: boolean = false;
  
  private monitorClient: RealtimeMonitorClient = new RealtimeMonitorClient();
  
  async aboutToAppear() {
    // 设置回调
    this.monitorClient.onStatus((status) => {
      this.serverStatus = status;
    });
  
    this.monitorClient.onAlert((alert) => {
      this.alerts.unshift(alert);
      if (this.alerts.length > 10) {
        this.alerts.pop();
      }
    });
  
    // 连接并订阅
    this.isMonitoring = await this.monitorClient.connect('wss://monitor.example.com/ws');
  
    if (this.isMonitoring) {
      await this.monitorClient.subscribeServer('server_001');
    }
  }
  
  aboutToDisappear() {
    this.monitorClient.close();
  }
  
  build() {
    Column() {
      Text('服务器实时监控')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })
    
      if (this.serverStatus) {
        // CPU使用率
        this.StatusCard('CPU', this.serverStatus.cpu, '%')
      
        // 内存使用率
        this.StatusCard('内存', this.serverStatus.memory, '%')
      
        // 磁盘使用率
        this.StatusCard('磁盘', this.serverStatus.disk, '%')
      
        // 网络流量
        Row() {
          Column() {
            Text('入流量')
              .fontSize(14)
            Text(`${this.serverStatus.network.in} KB/s`)
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor('#4A90E2')
          }
          .layoutWeight(1)
        
          Column() {
            Text('出流量')
              .fontSize(14)
            Text(`${this.serverStatus.network.out} KB/s`)
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor('#F5A623')
          }
          .layoutWeight(1)
        }
        .width('100%')
        .padding(15)
        .backgroundColor('#F5F5F5')
        .borderRadius(8)
        .margin({ top: 10 })
      
        // 告警列表
        if (this.alerts.length > 0) {
          Column() {
            Text('告警信息')
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
              .margin({ bottom: 10 })
          
            ForEach(this.alerts, (alert: string, index: number) => {
              Text(alert)
                .fontSize(14)
                .fontColor('#D0021B')
                .width('100%')
                .padding(8)
                .backgroundColor('#FFE5E5')
                .borderRadius(4)
                .margin({ bottom: 5 })
            })
          }
          .width('100%')
          .margin({ top: 20 })
        }
      } else {
        Text('等待数据...')
          .fontSize(16)
          .fontColor('#999')
      }
    }
    .width('100%')
    .padding(20)
  }
  
  @Builder
  StatusCard(title: string, value: number, unit: string) {
    Row() {
      Text(title)
        .fontSize(16)
        .layoutWeight(1)
    
      Stack() {
        // 背景条
        Row()
          .width(150)
          .height(20)
          .backgroundColor('#E8E8E8')
          .borderRadius(10)
      
        // 进度条
        Row()
          .width(`${value}%`)
          .height(20)
          .backgroundColor(this.getStatusColor(value))
          .borderRadius(10)
      }
      .width(150)
      .height(20)
    
      Text(`${value.toFixed(1)}${unit}`)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .width(70)
        .textAlign(TextAlign.End)
    }
    .width('100%')
    .padding(15)
    .backgroundColor('#F5F5F5')
    .borderRadius(8)
    .margin({ bottom: 10 })
  }
  
  private getStatusColor(value: number): string {
    if (value < 50) return '#7ED321';
    if (value < 80) return '#F5A623';
    return '#D0021B';
  }
}

场景三:多人协作编辑

实现类似Google Docs的实时协作编辑功能。

typescript 复制代码
import webSocket from '@ohos.net.webSocket';

// 编辑操作类型
interface EditOperation {
  type: 'insert' | 'delete' | 'replace';
  position: number;
  content?: string;
  length?: number;
  userId: string;
  timestamp: number;
  version: number;
}

// 协作文档状态
interface DocumentState {
  content: string;
  version: number;
  cursors: Map<string, number>;  // 用户ID -> 光标位置
}

// 协作编辑客户端
class CollaborativeEditClient {
  private ws: webSocket.WebSocket | null = null;
  private document: DocumentState = {
    content: '',
    version: 0,
    cursors: new Map()
  };
  private userId: string;
  private pendingOps: EditOperation[] = [];
  
  // 回调
  private onDocumentChange: ((doc: DocumentState) => void) | null = null;
  private onUserJoin: ((userId: string) => void) | null = null;
  private onUserLeave: ((userId: string) => void) | null = null;
  
  constructor(userId: string) {
    this.userId = userId;
  }
  
  // 连接协作服务
  async connect(docId: string, serverUrl: string): Promise<boolean> {
    this.ws = webSocket.createWebSocket();
  
    try {
      await this.ws.connect(`${serverUrl}?docId=${docId}&userId=${this.userId}`);
    
      this.ws.on('message', (err, value) => {
        if (err || typeof value !== 'string') return;
      
        let data = JSON.parse(value);
        this.handleServerMessage(data);
      });
    
      return true;
    
    } catch (error) {
      console.error('协作连接失败:', error);
      return false;
    }
  }
  
  // 处理服务器消息
  private handleServerMessage(data: any): void {
    switch (data.type) {
      case 'init':
        // 初始化文档
        this.document = {
          content: data.content,
          version: data.version,
          cursors: new Map(Object.entries(data.cursors || {}))
        };
        this.onDocumentChange?.(this.document);
        break;
      
      case 'operation':
        // 收到其他用户的操作
        this.applyOperation(data.operation);
        break;
      
      case 'cursor':
        // 光标位置更新
        this.document.cursors.set(data.userId, data.position);
        this.onDocumentChange?.(this.document);
        break;
      
      case 'join':
        this.onUserJoin?.(data.userId);
        break;
      
      case 'leave':
        this.document.cursors.delete(data.userId);
        this.onUserLeave?.(data.userId);
        this.onDocumentChange?.(this.document);
        break;
    }
  }
  
  // 本地编辑操作
  async localEdit(operation: Omit<EditOperation, 'userId' | 'timestamp' | 'version'>): Promise<void> {
    let fullOp: EditOperation = {
      ...operation,
      userId: this.userId,
      timestamp: Date.now(),
      version: this.document.version
    };
  
    // 立即应用到本地(乐观更新)
    this.applyLocalOperation(fullOp);
  
    // 发送到服务器
    if (this.ws) {
      await this.ws.send(JSON.stringify({
        type: 'operation',
        operation: fullOp
      }));
    }
  }
  
  // 应用本地操作
  private applyLocalOperation(op: EditOperation): void {
    let content = this.document.content;
  
    switch (op.type) {
      case 'insert':
        if (op.content) {
          content = content.slice(0, op.position) + op.content + content.slice(op.position);
        }
        break;
      
      case 'delete':
        if (op.length) {
          content = content.slice(0, op.position) + content.slice(op.position + op.length);
        }
        break;
      
      case 'replace':
        if (op.content && op.length) {
          content = content.slice(0, op.position) + op.content + content.slice(op.position + op.length);
        }
        break;
    }
  
    this.document.content = content;
    this.document.version++;
    this.onDocumentChange?.(this.document);
  }
  
  // 应用远程操作(需要处理冲突)
  private applyOperation(op: EditOperation): void {
    // 简化处理:直接应用
    // 实际需要实现OT算法处理冲突
    this.applyLocalOperation(op);
  }
  
  // 更新光标位置
  async updateCursor(position: number): Promise<void> {
    this.document.cursors.set(this.userId, position);
  
    if (this.ws) {
      await this.ws.send(JSON.stringify({
        type: 'cursor',
        position: position
      }));
    }
  }
  
  // 设置文档变化回调
  setOnDocumentChange(callback: (doc: DocumentState) => void): void {
    this.onDocumentChange = callback;
  }
  
  // 获取当前文档
  getDocument(): DocumentState {
    return this.document;
  }
  
  // 关闭连接
  async close(): Promise<void> {
    if (this.ws) {
      await this.ws.close();
      this.ws = null;
    }
  }
}

// 协作编辑UI
@Entry
@Component
struct CollaborativeEditPage {
  @State documentContent: string = '';
  @State cursors: Map<string, number> = new Map();
  @State version: number = 0;
  
  private editClient: CollaborativeEditClient | null = null;
  private textAreaController: TextAreaController = new TextAreaController();
  
  async aboutToAppear() {
    this.editClient = new CollaborativeEditClient('user_001');
  
    this.editClient.setOnDocumentChange((doc) => {
      this.documentContent = doc.content;
      this.cursors = doc.cursors;
      this.version = doc.version;
    });
  
    await this.editClient.connect('doc_001', 'wss://collab.example.com/ws');
  }
  
  aboutToDisappear() {
    this.editClient?.close();
  }
  
  build() {
    Column() {
      // 顶部信息栏
      Row() {
        Text(`版本: ${this.version}`)
          .fontSize(14)
      
        Blank().layoutWeight(1)
      
        Text(`协作者: ${this.cursors.size}人`)
          .fontSize(14)
      }
      .width('100%')
      .padding(10)
      .backgroundColor('#F5F5F5')
    
      // 编辑区域
      TextArea({ text: this.documentContent, controller: this.textAreaController })
        .width('100%')
        .layoutWeight(1)
        .onChange((value) => {
          // 计算操作并发送
          // 简化处理:整体替换
          this.editClient?.localEdit({
            type: 'replace',
            position: 0,
            content: value,
            length: this.documentContent.length
          });
        })
    
      // 协作者光标指示
      Row() {
        ForEach(Array.from(this.cursors.entries()), (entry: [string, number]) => {
          Row() {
            Circle()
              .width(8)
              .height(8)
              .fill(this.getUserColor(entry[0]))
          
            Text(`${entry[0]}: 位置${entry[1]}`)
              .fontSize(12)
              .margin({ left: 4 })
          }
          .margin({ right: 15 })
        }, (entry: [string, number]) => entry[0])
      }
      .width('100%')
      .padding(10)
    }
    .width('100%')
    .height('100%')
  }
  
  private getUserColor(userId: string): string {
    let colors = ['#4A90E2', '#F5A623', '#7ED321', '#D0021B'];
    let index = userId.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % colors.length;
    return colors[index];
  }
}

四、踩坑与注意事项

坑点一:连接未正常关闭

页面销毁时忘记关闭WebSocket,导致资源泄漏。

typescript 复制代码
// ❌ 错误:忘记关闭
@Entry
@Component
struct BadExample {
  private ws: webSocket.WebSocket | null = null;
  
  async aboutToAppear() {
    this.ws = webSocket.createWebSocket();
    await this.ws.connect('wss://example.com/ws');
    // 没有在aboutToDisappear中关闭!
  }
}

// ✅ 正确:生命周期管理
@Entry
@Component
struct GoodExample {
  private ws: webSocket.WebSocket | null = null;
  
  async aboutToAppear() {
    this.ws = webSocket.createWebSocket();
    await this.ws.connect('wss://example.com/ws');
  }
  
  async aboutToDisappear() {
    if (this.ws) {
      await this.ws.close();
      this.ws = null;
    }
  }
}

坑点二:消息解析异常

未处理异常消息格式,导致应用崩溃。

typescript 复制代码
// ❌ 错误:直接解析
this.ws.on('message', (err, value) => {
  let data = JSON.parse(value as string);  // 可能抛出异常
  // 处理data...
});

// ✅ 正确:异常处理
this.ws.on('message', (err, value) => {
  if (err) {
    console.error('消息错误:', err);
    return;
  }
  
  try {
    if (typeof value === 'string') {
      let data = JSON.parse(value);
      // 处理data...
    } else if (value instanceof ArrayBuffer) {
      // 处理二进制数据...
    }
  } catch (error) {
    console.error('消息解析失败:', error);
  }
});

坑点三:重连风暴

多个客户端同时重连,导致服务器压力过大。

typescript 复制代码
// ❌ 错误:立即重连
private reconnect() {
  setTimeout(() => this.connect(), 1000);  // 固定1秒
}

// ✅ 正确:指数退避 + 随机抖动
private reconnectAttempts: number = 0;

private reconnect() {
  this.reconnectAttempts++;
  
  // 指数退避
  let baseDelay = 1000 * Math.pow(2, this.reconnectAttempts - 1);
  
  // 随机抖动(0.5 ~ 1.5倍)
  let jitter = 0.5 + Math.random();
  let delay = baseDelay * jitter;
  
  // 上限30秒
  delay = Math.min(delay, 30000);
  
  console.info(`将在 ${delay}ms 后重连`);
  setTimeout(() => this.connect(), delay);
}

坑点四:内存泄漏

未清理事件监听器,导致回调累积。

typescript 复制代码
// ❌ 错误:重复注册监听
async connect() {
  this.ws = webSocket.createWebSocket();
  await this.ws.connect(url);
  
  // 每次连接都注册新的监听器
  this.ws.on('message', (err, value) => {
    // 处理消息
  });
}

// ✅ 正确:单次注册或清理旧监听
private messageHandler: Function | null = null;

async connect() {
  this.ws = webSocket.createWebSocket();
  await this.ws.connect(url);
  
  // 使用成员变量保存监听器
  this.messageHandler = (err, value) => {
    // 处理消息
  };
  
  this.ws.on('message', this.messageHandler);
}

五、HarmonyOS 6适配指南

5.1 新增特性

HarmonyOS 6对WebSocket模块进行了增强。

typescript 复制代码
import webSocket from '@ohos.net.webSocket';

// HarmonyOS 6: 新增配置选项
let ws = webSocket.createWebSocket();

await ws.connect('wss://api.example.com/ws', {
  // 连接超时
  connectTimeout: 10000,
  
  // 新增:子协议协商
  protocols: ['json', 'binary'],
  
  // 新增:自定义请求头
  header: {
    'Authorization': 'Bearer token',
    'X-Client-Version': '1.0.0'
  },
  
  // 新增:Ping/Pong配置
  pingInterval: 30000,  // 每30秒发送Ping
  pongTimeout: 5000     // 5秒未收到Pong则断开
});

// 新增:获取连接信息
let info = await ws.getConnectionInfo();
console.info('连接状态:', info.readyState);
console.info('实际子协议:', info.protocol);

5.2 二进制数据优化

HarmonyOS 6优化了二进制数据的处理性能。

typescript 复制代码
// 发送二进制数据
let buffer = new ArrayBuffer(1024);
let view = new Uint8Array(buffer);

// 填充数据
for (let i = 0; i < 1024; i++) {
  view[i] = i % 256;
}

// HarmonyOS 6: 直接发送ArrayBuffer
await ws.send(buffer);

// 接收二进制数据
ws.on('message', (err, value) => {
  if (value instanceof ArrayBuffer) {
    // 高效处理二进制数据
    let view = new DataView(value);
    let firstByte = view.getUint8(0);
    console.info('首字节:', firstByte);
  }
});

5.3 连接状态管理

HarmonyOS 6提供了更详细的连接状态。

typescript 复制代码
// 连接状态枚举
enum ReadyState {
  CONNECTING = 0,  // 连接中
  OPEN = 1,        // 已连接
  CLOSING = 2,     // 关闭中
  CLOSED = 3       // 已关闭
}

// 监听状态变化
ws.on('stateChange', (state: ReadyState) => {
  switch (state) {
    case ReadyState.CONNECTING:
      console.info('正在连接...');
      break;
    case ReadyState.OPEN:
      console.info('连接已建立');
      break;
    case ReadyState.CLOSING:
      console.info('正在关闭...');
      break;
    case ReadyState.CLOSED:
      console.info('连接已关闭');
      break;
  }
});

六、总结一下下

WebSocket是实时通信的基石,掌握它对现代应用开发至关重要。本文从三个实战场景展开:

实时聊天:最经典的WebSocket应用。需要处理连接管理、消息收发、重连机制、UI状态同步等。核心是建立可靠的双向通信通道。

实时监控:数据流式推送场景。服务器主动推送状态数据,客户端实时渲染。注意数据量控制和渲染性能。

协作编辑:多人实时协作场景。需要处理操作同步、冲突解决(OT/CRDT算法)、光标同步等。技术难度较高,但用户体验极佳。

四个常见坑点:连接未关闭、消息解析异常、重连风暴、内存泄漏。遇到WebSocket问题时,先排查这四个方面。

HarmonyOS 6带来了子协议协商、Ping/Pong配置、状态管理等新特性,让WebSocket开发更加便捷可靠。

下一篇文章,我们将深入WebSocket心跳与重连机制,确保长连接的稳定性!