HarmonyOS PC开发中Socket UDP编程:无连接数据传输

当速度比可靠更重要时,UDP是你的不二之选

一、为什么需要UDP?

TCP虽然可靠,但可靠性是有代价的。三次握手建立连接、确认重传机制、拥塞控制算法……这些机制保证了数据可靠到达,但也带来了延迟和开销。

某些场景下,我们更看重速度而非可靠性:

实时音视频:视频会议、直播推流。丢几帧画面影响不大,但延迟会破坏实时体验。UDP可以让数据尽快到达,即使偶尔丢失也优于等待重传。

在线游戏:FPS、MOBA类游戏。玩家的操作需要立即同步,延迟100毫秒和200毫秒的体验天差地别。丢包可以插值补偿,但延迟无法忍受。

DNS查询:域名解析。请求响应都很小,一次UDP往返足够。失败就重试,不需要建立连接的开销。

物联网数据上报:传感器定期上报数据。偶尔丢失一两个数据点影响不大,但设备电量有限,不能承受TCP的心跳开销。

广播/多播:局域网设备发现、视频会议多播。UDP支持一对多传输,TCP做不到。

鸿蒙系统提供了@ohos.net.socket模块中的UDP Socket支持,本文将深入探讨。

二、核心原理:UDP特性与工作流程

2.1 UDP vs TCP 对比

UDP特性

无连接

直接发送

不保证可靠

可能乱序

无流量控制

无拥塞控制

TCP特性

面向连接

三次握手

可靠传输

有序到达

流量控制

拥塞控制

2.2 UDP数据报格式

UDP数据报格式非常简单,只有8字节头部:

字段 大小 说明
源端口 2字节 发送方端口
目的端口 2字节 接收方端口
长度 2字节 数据报总长度(含头部)
校验和 2字节 可选的校验和

数据部分最大65507字节(65535 - 8字节头 - 20字节IP头)。

2.3 UDP应用场景与注意事项

场景 为什么选UDP 需要注意
实时音视频 低延迟优先 应用层FEC前向纠错
在线游戏 快速同步 状态同步、插值补偿
DNS查询 简单高效 超时重试机制
局域网发现 广播支持 TTL设置
大文件传输 可靠性自控 应用层分片、确认

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

场景一:UDP客户端实现

实现UDP数据发送和接收,无需建立连接。

import socket from '@ohos.net.socket';
import { BusinessError } from '@ohos.base';

// UDP客户端配置
interface UDPClientConfig {
  localPort?: number;  // 本地端口(可选,自动分配)
}

// UDP客户端
class UDPClient {
  private socket: socket.UDPSocket | null = null;
  private config: UDPClientConfig;
  private isBound: boolean = false;
  
  // 回调
  private onMessage: ((data: ArrayBuffer, remoteInfo: socket.SocketRemoteInfo) => void) | null = null;
  private onError: ((error: BusinessError) => void) | null = null;
  
  constructor(config: UDPClientConfig = {}) {
    this.config = config;
  }
  
  // 绑定本地端口
  async bind(): Promise<boolean> {
    if (this.isBound) {
      return true;
    }
  
    // 创建UDP Socket
    this.socket = socket.constructUDPSocketInstance();
  
    try {
      // 绑定本地地址
      await this.socket.bind({
        address: '0.0.0.0',
        port: this.config.localPort || 0,  // 0表示自动分配
        family: 1  // IPv4
      });
    
      this.isBound = true;
    
      // 设置监听
      this.setupListeners();
    
      // 获取绑定的端口
      let state = await this.socket.getState();
      console.info(`UDP绑定成功,本地端口: ${state.localPort}`);
    
      return true;
    
    } catch (error) {
      console.error('UDP绑定失败:', error);
      return false;
    }
  }
  
  // 设置监听
  private setupListeners(): void {
    if (!this.socket) return;
  
    // 接收数据
    this.socket.on('message', (value: socket.SocketMessageInfo) => {
      let data = value.message;
      let remoteInfo = value.remoteInfo;
    
      console.debug(`收到UDP数据: ${data.byteLength} 字节,来自 ${remoteInfo.address}:${remoteInfo.port}`);
    
      this.onMessage?.(data, remoteInfo);
    });
  
    // 错误
    this.socket.on('error', (err: BusinessError) => {
      console.error('UDP错误:', err.message);
      this.onError?.(err);
    });
  }
  
  // 发送数据到指定地址
  async send(data: string | ArrayBuffer, address: string, port: number): Promise<boolean> {
    if (!this.socket || !this.isBound) {
      // 自动绑定
      let bound = await this.bind();
      if (!bound) {
        return false;
      }
    }
  
    try {
      let sendData: socket.UDPSendOptions;
    
      if (typeof data === 'string') {
        // 字符串转ArrayBuffer
        let encoder = new TextEncoder();
        sendData = {
          data: encoder.encode(data).buffer,
          address: {
            address: address,
            port: port,
            family: 1
          }
        };
      } else {
        sendData = {
          data: data,
          address: {
            address: address,
            port: port,
            family: 1
          }
        };
      }
    
      await this.socket.send(sendData);
    
      console.debug(`发送UDP数据: ${sendData.data.byteLength} 字节,到 ${address}:${port}`);
      return true;
    
    } catch (error) {
      console.error('UDP发送失败:', error);
      return false;
    }
  }
  
  // 发送广播数据
  async sendBroadcast(data: string | ArrayBuffer, port: number): Promise<boolean> {
    // 发送到广播地址
    return await this.send(data, '255.255.255.255', port);
  }
  
  // 获取本地端口
  async getLocalPort(): Promise<number> {
    if (!this.socket) return 0;
  
    try {
      let state = await this.socket.getState();
      return state.localPort;
    } catch (error) {
      return 0;
    }
  }
  
  // 关闭Socket
  async close(): Promise<void> {
    if (this.socket) {
      try {
        await this.socket.close();
        console.info('UDP Socket已关闭');
      } catch (error) {
        console.error('关闭UDP Socket失败:', error);
      }
    
      this.socket = null;
      this.isBound = false;
    }
  }
  
  // 设置回调
  setOnMessage(callback: (data: ArrayBuffer, remoteInfo: socket.SocketRemoteInfo) => void): void {
    this.onMessage = callback;
  }
  
  setOnError(callback: (error: BusinessError) => void): void {
    this.onError = callback;
  }
}

// UDP客户端页面
@Entry
@Component
struct UDPClientPage {
  @State localPort: number = 0;
  @State receivedMessages: string[] = [];
  @State sentCount: number = 0;
  @State receivedCount: number = 0;
  
  @State targetAddress: string = '192.168.1.100';
  @State targetPort: number = 8888;
  @State messageInput: string = '';
  
  private udpClient: UDPClient | null = null;
  
  async aboutToAppear() {
    this.udpClient = new UDPClient();
  
    // 设置回调
    this.udpClient.setOnMessage((data, remoteInfo) => {
      let decoder = new TextDecoder();
      let text = decoder.decode(data);
    
      let message = `收到 [${remoteInfo.address}:${remoteInfo.port}]: ${text}`;
      this.receivedMessages.unshift(message);
    
      if (this.receivedMessages.length > 20) {
        this.receivedMessages.pop();
      }
    
      this.receivedCount += data.byteLength;
    });
  
    // 绑定
    await this.udpClient.bind();
    this.localPort = await this.udpClient.getLocalPort();
  }
  
  async aboutToDisappear() {
    await this.udpClient?.close();
  }
  
  // 发送消息
  async sendMessage() {
    if (!this.messageInput) return;
  
    let success = await this.udpClient?.send(
      this.messageInput,
      this.targetAddress,
      this.targetPort
    );
  
    if (success) {
      this.sentCount += this.messageInput.length;
      this.messageInput = '';
    }
  }
  
  // 发送广播
  async sendBroadcast() {
    let message = `Broadcast at ${Date.now()}`;
    let success = await this.udpClient?.sendBroadcast(message, this.targetPort);
  
    if (success) {
      this.sentCount += message.length;
    }
  }
  
  build() {
    Column() {
      // 本地端口显示
      Row() {
        Text(`本地端口: ${this.localPort}`)
          .fontSize(16)
      
        Blank().layoutWeight(1)
      
        Text(`发送: ${this.sentCount} | 接收: ${this.receivedCount}`)
          .fontSize(14)
          .fontColor('#666')
      }
      .width('100%')
      .padding(15)
    
      // 目标配置
      Column() {
        Row() {
          Text('目标地址:')
            .fontSize(14)
            .width(80)
        
          TextInput({ text: this.targetAddress })
            .layoutWeight(1)
            .onChange((value) => this.targetAddress = value)
        }
        .width('100%')
      
        Row() {
          Text('目标端口:')
            .fontSize(14)
            .width(80)
        
          TextInput({ text: this.targetPort.toString() })
            .layoutWeight(1)
            .type(InputType.Number)
            .onChange((value) => this.targetPort = parseInt(value) || 8888)
        }
        .width('100%')
        .margin({ top: 10 })
      }
      .width('100%')
      .padding(10)
    
      // 消息输入
      Row() {
        TextInput({ text: this.messageInput, placeholder: '输入消息' })
          .layoutWeight(1)
          .onChange((value) => this.messageInput = value)
          .onSubmit(() => this.sendMessage())
      
        Button('发送')
          .onClick(() => this.sendMessage())
          .margin({ left: 10 })
      
        Button('广播')
          .onClick(() => this.sendBroadcast())
          .margin({ left: 10 })
      }
      .width('100%')
      .padding(10)
    
      // 接收消息列表
      Column() {
        Text('接收消息:')
          .fontSize(14)
          .width('100%')
      
        List() {
          ForEach(this.receivedMessages, (msg: string, index: number) => {
            ListItem() {
              Text(msg)
                .fontSize(12)
                .fontFamily('monospace')
            }
          })
        }
        .width('100%')
        .layoutWeight(1)
        .backgroundColor('#F5F5F5')
        .padding(10)
      }
      .width('100%')
      .padding(10)
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
  }
}

场景二:UDP服务端实现

实现UDP服务器,监听端口并响应客户端请求。

import socket from '@ohos.net.socket';
import { BusinessError } from '@ohos.base';

// UDP服务端配置
interface UDPServerConfig {
  port: number;
  host?: string;
}

// UDP服务端
class UDPServer {
  private socket: socket.UDPSocket | null = null;
  private config: UDPServerConfig;
  private isListening: boolean = false;
  
  // 回调
  private onMessage: ((data: ArrayBuffer, remoteInfo: socket.SocketRemoteInfo) => void) | null = null;
  private onError: ((error: BusinessError) => void) | null = null;
  
  constructor(config: UDPServerConfig) {
    this.config = {
      host: '0.0.0.0',
      ...config
    };
  }
  
  // 启动服务器
  async start(): Promise<boolean> {
    if (this.isListening) {
      return true;
    }
  
    // 创建UDP Socket
    this.socket = socket.constructUDPSocketInstance();
  
    try {
      // 绑定地址和端口
      await this.socket.bind({
        address: this.config.host,
        port: this.config.port,
        family: 1
      });
    
      this.isListening = true;
    
      // 设置监听
      this.setupListeners();
    
      console.info(`UDP服务器启动: ${this.config.host}:${this.config.port}`);
      return true;
    
    } catch (error) {
      console.error('UDP服务器启动失败:', error);
      return false;
    }
  }
  
  // 设置监听
  private setupListeners(): void {
    if (!this.socket) return;
  
    // 接收数据
    this.socket.on('message', (value: socket.SocketMessageInfo) => {
      let data = value.message;
      let remoteInfo = value.remoteInfo;
    
      console.info(`收到UDP请求: ${data.byteLength} 字节,来自 ${remoteInfo.address}:${remoteInfo.port}`);
    
      // 通知上层
      this.onMessage?.(data, remoteInfo);
    });
  
    // 错误
    this.socket.on('error', (err: BusinessError) => {
      console.error('UDP服务器错误:', err.message);
      this.onError?.(err);
    });
  }
  
  // 发送响应
  async send(data: string | ArrayBuffer, address: string, port: number): Promise<boolean> {
    if (!this.socket || !this.isListening) {
      return false;
    }
  
    try {
      let sendData: socket.UDPSendOptions;
    
      if (typeof data === 'string') {
        let encoder = new TextEncoder();
        sendData = {
          data: encoder.encode(data).buffer,
          address: {
            address: address,
            port: port,
            family: 1
          }
        };
      } else {
        sendData = {
          data: data,
          address: {
            address: address,
            port: port,
            family: 1
          }
        };
      }
    
      await this.socket.send(sendData);
      return true;
    
    } catch (error) {
      console.error('UDP响应发送失败:', error);
      return false;
    }
  }
  
  // 停止服务器
  async stop(): Promise<void> {
    if (this.socket) {
      try {
        await this.socket.close();
      } catch (error) {
        // 忽略
      }
    
      this.socket = null;
      this.isListening = false;
    }
  
    console.info('UDP服务器已停止');
  }
  
  // 设置回调
  setOnMessage(callback: (data: ArrayBuffer, remoteInfo: socket.SocketRemoteInfo) => void): void {
    this.onMessage = callback;
  }
  
  setOnError(callback: (error: BusinessError) => void): void {
    this.onError = callback;
  }
  
  // 获取监听状态
  isListening_(): boolean {
    return this.isListening;
  }
}

// UDP服务端页面
@Entry
@Component
struct UDPServerPage {
  @State serverStatus: string = '未启动';
  @State requestCount: number = 0;
  @State logs: string[] = [];
  
  private udpServer: UDPServer | null = null;
  
  async aboutToAppear() {
    this.udpServer = new UDPServer({
      port: 8888
    });
  
    // 设置回调
    this.udpServer.setOnMessage(async (data, remoteInfo) => {
      this.requestCount++;
    
      // 解析请求
      let decoder = new TextDecoder();
      let text = decoder.decode(data);
    
      this.addLog(`请求 [${remoteInfo.address}:${remoteInfo.port}]: ${text}`);
    
      // 发送响应
      let response = `Echo: ${text}`;
      await this.udpServer?.send(response, remoteInfo.address, remoteInfo.port);
    
      this.addLog(`响应 -> [${remoteInfo.address}:${remoteInfo.port}]: ${response}`);
    });
  }
  
  async aboutToDisappear() {
    await this.udpServer?.stop();
  }
  
  // 启动服务器
  async startServer() {
    let success = await this.udpServer?.start();
    this.serverStatus = success ? '运行中' : '启动失败';
    this.addLog(`服务器${success ? '启动成功' : '启动失败'}`);
  }
  
  // 停止服务器
  async stopServer() {
    await this.udpServer?.stop();
    this.serverStatus = '已停止';
    this.addLog('服务器已停止');
  }
  
  // 添加日志
  private addLog(log: string) {
    let time = new Date().toLocaleTimeString();
    this.logs.unshift(`[${time}] ${log}`);
    if (this.logs.length > 50) {
      this.logs.pop();
    }
  }
  
  build() {
    Column() {
      // 服务器状态
      Row() {
        Text('服务器状态:')
          .fontSize(16)
        Text(this.serverStatus)
          .fontSize(16)
          .fontColor(this.serverStatus === '运行中' ? '#7ED321' : '#666')
          .margin({ left: 10 })
      
        Blank().layoutWeight(1)
      
        Text(`请求数: ${this.requestCount}`)
          .fontSize(16)
      }
      .width('100%')
      .padding(15)
    
      // 控制按钮
      Row() {
        Button('启动服务器')
          .onClick(() => this.startServer())
          .enabled(this.serverStatus !== '运行中')
          .layoutWeight(1)
      
        Button('停止服务器')
          .onClick(() => this.stopServer())
          .enabled(this.serverStatus === '运行中')
          .layoutWeight(1)
          .margin({ left: 10 })
      }
      .width('100%')
      .padding(10)
    
      // 日志区域
      Column() {
        Text('服务器日志:')
          .fontSize(14)
          .width('100%')
      
        List() {
          ForEach(this.logs, (log: string, index: number) => {
            ListItem() {
              Text(log)
                .fontSize(12)
                .fontFamily('monospace')
            }
          })
        }
        .width('100%')
        .layoutWeight(1)
        .backgroundColor('#F5F5F5')
        .padding(10)
      }
      .width('100%')
      .padding(10)
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
  }
}

场景三:局域网设备发现

使用UDP广播实现局域网设备发现功能。

import socket from '@ohos.net.socket';

// 设备发现协议
interface DiscoveryMessage {
  type: 'discover' | 'announce';
  deviceId: string;
  deviceName: string;
  deviceType: string;
  ipAddress: string;
  port: number;
  timestamp: number;
}

// 发现的设备
interface DiscoveredDevice {
  deviceId: string;
  deviceName: string;
  deviceType: string;
  ipAddress: string;
  port: number;
  lastSeen: number;
}

// 设备发现服务
class DeviceDiscoveryService {
  private udpSocket: socket.UDPSocket | null = null;
  private discoveryPort: number = 38888;
  private isRunning: boolean = false;
  
  // 本设备信息
  private localDevice: {
    deviceId: string;
    deviceName: string;
    deviceType: string;
    port: number;
  };
  
  // 发现的设备列表
  private discoveredDevices: Map<string, DiscoveredDevice> = new Map();
  
  // 回调
  private onDeviceFound: ((device: DiscoveredDevice) => void) | null = null;
  private onDeviceLost: ((deviceId: string) => void) | null = null;
  
  // 设备过期时间(毫秒)
  private deviceTimeout: number = 30000;
  private cleanupTimer: number = -1;
  
  constructor(deviceInfo: {
    deviceId: string;
    deviceName: string;
    deviceType: string;
    port: number;
  }) {
    this.localDevice = deviceInfo;
  }
  
  // 启动发现服务
  async start(): Promise<boolean> {
    if (this.isRunning) {
      return true;
    }
  
    // 创建UDP Socket
    this.udpSocket = socket.constructUDPSocketInstance();
  
    try {
      // 绑定到发现端口
      await this.udpSocket.bind({
        address: '0.0.0.0',
        port: this.discoveryPort,
        family: 1
      });
    
      this.isRunning = true;
    
      // 设置监听
      this.setupListeners();
    
      // 启动设备清理定时器
      this.startCleanupTimer();
    
      // 发送发现请求
      await this.sendDiscoveryRequest();
    
      console.info('设备发现服务已启动');
      return true;
    
    } catch (error) {
      console.error('启动设备发现服务失败:', error);
      return false;
    }
  }
  
  // 设置监听
  private setupListeners(): void {
    if (!this.udpSocket) return;
  
    this.udpSocket.on('message', async (value: socket.SocketMessageInfo) => {
      try {
        // 解析消息
        let decoder = new TextDecoder();
        let text = decoder.decode(value.message);
        let message: DiscoveryMessage = JSON.parse(text);
      
        // 忽略自己的消息
        if (message.deviceId === this.localDevice.deviceId) {
          return;
        }
      
        switch (message.type) {
          case 'discover':
            // 收到发现请求,回复设备信息
            await this.sendAnnounce(value.remoteInfo.address, value.remoteInfo.port);
            break;
          
          case 'announce':
            // 收到设备公告,更新设备列表
            this.handleDeviceAnnounce(message);
            break;
        }
      
      } catch (error) {
        // 忽略解析错误
      }
    });
  }
  
  // 发送发现请求(广播)
  private async sendDiscoveryRequest(): Promise<void> {
    if (!this.udpSocket) return;
  
    let message: DiscoveryMessage = {
      type: 'discover',
      deviceId: this.localDevice.deviceId,
      deviceName: this.localDevice.deviceName,
      deviceType: this.localDevice.deviceType,
      ipAddress: '',  // 广播消息不需要
      port: this.localDevice.port,
      timestamp: Date.now()
    };
  
    let encoder = new TextEncoder();
    let data = encoder.encode(JSON.stringify(message)).buffer;
  
    // 发送广播
    try {
      await this.udpSocket.send({
        data: data,
        address: {
          address: '255.255.255.255',
          port: this.discoveryPort,
          family: 1
        }
      });
    
      console.info('已发送发现请求');
    } catch (error) {
      console.error('发送发现请求失败:', error);
    }
  }
  
  // 发送设备公告
  private async sendAnnounce(targetAddress: string, targetPort: number): Promise<void> {
    if (!this.udpSocket) return;
  
    // 获取本地IP
    let localIp = await this.getLocalIp();
  
    let message: DiscoveryMessage = {
      type: 'announce',
      deviceId: this.localDevice.deviceId,
      deviceName: this.localDevice.deviceName,
      deviceType: this.localDevice.deviceType,
      ipAddress: localIp,
      port: this.localDevice.port,
      timestamp: Date.now()
    };
  
    let encoder = new TextEncoder();
    let data = encoder.encode(JSON.stringify(message)).buffer;
  
    try {
      await this.udpSocket.send({
        data: data,
        address: {
          address: targetAddress,
          port: targetPort,
          family: 1
        }
      });
    } catch (error) {
      console.error('发送设备公告失败:', error);
    }
  }
  
  // 处理设备公告
  private handleDeviceAnnounce(message: DiscoveryMessage): void {
    let device: DiscoveredDevice = {
      deviceId: message.deviceId,
      deviceName: message.deviceName,
      deviceType: message.deviceType,
      ipAddress: message.ipAddress,
      port: message.port,
      lastSeen: Date.now()
    };
  
    let isNew = !this.discoveredDevices.has(device.deviceId);
  
    this.discoveredDevices.set(device.deviceId, device);
  
    if (isNew) {
      console.info(`发现新设备: ${device.deviceName} (${device.ipAddress}:${device.port})`);
      this.onDeviceFound?.(device);
    }
  }
  
  // 启动设备清理定时器
  private startCleanupTimer(): void {
    this.cleanupTimer = setInterval(() => {
      let now = Date.now();
    
      for (let [deviceId, device] of this.discoveredDevices) {
        if (now - device.lastSeen > this.deviceTimeout) {
          this.discoveredDevices.delete(deviceId);
          this.onDeviceLost?.(deviceId);
          console.info(`设备已离线: ${device.deviceName}`);
        }
      }
    }, 5000);
  }
  
  // 获取本地IP
  private async getLocalIp(): Promise<string> {
    // 简化处理,实际需要获取真实IP
    return '192.168.1.100';
  }
  
  // 停止服务
  async stop(): Promise<void> {
    if (this.cleanupTimer !== -1) {
      clearInterval(this.cleanupTimer);
      this.cleanupTimer = -1;
    }
  
    if (this.udpSocket) {
      try {
        await this.udpSocket.close();
      } catch (error) {
        // 忽略
      }
      this.udpSocket = null;
    }
  
    this.isRunning = false;
    this.discoveredDevices.clear();
  }
  
  // 获取发现的设备列表
  getDiscoveredDevices(): DiscoveredDevice[] {
    return Array.from(this.discoveredDevices.values());
  }
  
  // 设置回调
  setOnDeviceFound(callback: (device: DiscoveredDevice) => void): void {
    this.onDeviceFound = callback;
  }
  
  setOnDeviceLost(callback: (deviceId: string) => void): void {
    this.onDeviceLost = callback;
  }
}

// 设备发现页面
@Entry
@Component
struct DeviceDiscoveryPage {
  @State isScanning: boolean = false;
  @State devices: DiscoveredDevice[] = [];
  
  private discoveryService: DeviceDiscoveryService | null = null;
  
  async aboutToAppear() {
    // 初始化发现服务
    this.discoveryService = new DeviceDiscoveryService({
      deviceId: 'device_' + Math.random().toString(36).substr(2, 9),
      deviceName: '我的设备',
      deviceType: 'HarmonyOS',
      port: 8080
    });
  
    // 设置回调
    this.discoveryService.setOnDeviceFound((device) => {
      this.devices = this.discoveryService?.getDiscoveredDevices() || [];
    });
  
    this.discoveryService.setOnDeviceLost((deviceId) => {
      this.devices = this.discoveryService?.getDiscoveredDevices() || [];
    });
  }
  
  async aboutToDisappear() {
    await this.discoveryService?.stop();
  }
  
  // 开始扫描
  async startScan() {
    this.isScanning = true;
    await this.discoveryService?.start();
  
    // 5秒后停止扫描
    setTimeout(() => {
      this.isScanning = false;
    }, 5000);
  }
  
  build() {
    Column() {
      // 标题
      Row() {
        Text('局域网设备发现')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
      
        Blank().layoutWeight(1)
      
        Button(this.isScanning ? '扫描中...' : '扫描设备')
          .onClick(() => this.startScan())
          .enabled(!this.isScanning)
      }
      .width('100%')
      .padding(15)
    
      // 设备列表
      if (this.devices.length > 0) {
        List() {
          ForEach(this.devices, (device: DiscoveredDevice) => {
            ListItem() {
              this.DeviceCard(device)
            }
          }, (device: DiscoveredDevice) => device.deviceId)
        }
        .width('100%')
        .layoutWeight(1)
        .padding(10)
      } else {
        Column() {
          Text('暂无发现设备')
            .fontSize(16)
            .fontColor('#999')
        
          Text('点击"扫描设备"开始搜索')
            .fontSize(14)
            .fontColor('#999')
            .margin({ top: 10 })
        }
        .width('100%')
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
      }
    }
    .width('100%')
    .height('100%')
  }
  
  @Builder
  DeviceCard(device: DiscoveredDevice) {
    Column() {
      Row() {
        // 设备图标
        Column() {
          if (device.deviceType === 'HarmonyOS') {
            Text('📱')
              .fontSize(30)
          } else if (device.deviceType === 'IoT') {
            Text('🔌')
              .fontSize(30)
          } else {
            Text('💻')
              .fontSize(30)
          }
        }
        .width(50)
        .height(50)
        .justifyContent(FlexAlign.Center)
        .backgroundColor('#E8E8E8')
        .borderRadius(25)
      
        // 设备信息
        Column() {
          Text(device.deviceName)
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
        
          Text(device.deviceType)
            .fontSize(12)
            .fontColor('#666')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
        .margin({ left: 15 })
        .alignItems(HorizontalAlign.Start)
      
        // 连接按钮
        Button('连接')
          .height(36)
          .onClick(() => {
            console.info(`连接设备: ${device.ipAddress}:${device.port}`);
          })
      }
      .width('100%')
    
      // 地址信息
      Row() {
        Text(`IP: ${device.ipAddress}`)
          .fontSize(12)
          .fontColor('#999')
      
        Text(`端口: ${device.port}`)
          .fontSize(12)
          .fontColor('#999')
          .margin({ left: 20 })
      }
      .width('100%')
      .margin({ top: 10 })
    }
    .width('100%')
    .padding(15)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .shadow({ radius: 2, color: '#00000010', offsetX: 0, offsetY: 2 })
  }
}

四、踩坑与注意事项

坑点一:数据包大小限制

UDP数据报有大小限制,超过MTU会被IP层分片,增加丢包风险。

// ❌ 错误:发送过大的UDP包
let largeData = new ArrayBuffer(100000);  // 100KB
await udpClient.send(largeData, '192.168.1.100', 8888);
// 可能被IP分片,丢包风险高

// ✅ 正确:控制包大小
const MAX_UDP_SIZE = 1400;  // 留出IP和UDP头的空间

async function sendLargeData(data: ArrayBuffer, address: string, port: number) {
  let offset = 0;
  let totalSize = data.byteLength;
  
  while (offset < totalSize) {
    let chunkSize = Math.min(MAX_UDP_SIZE, totalSize - offset);
    let chunk = data.slice(offset, offset + chunkSize);
  
    await udpClient.send(chunk, address, port);
  
    offset += chunkSize;
  
    // 可选:添加序号用于重组
  }
}

坑点二:丢包未处理

UDP不保证可靠传输,需要应用层处理丢包。

// 带确认机制的UDP发送
class ReliableUDP {
  private sequence: number = 0;
  private pendingAcks: Map<number, { data: ArrayBuffer; timer: number; retries: number }> = new Map();
  private maxRetries: number = 3;
  private ackTimeout: number = 1000;
  
  // 发送数据并等待确认
  async sendReliable(data: ArrayBuffer, address: string, port: number): Promise<boolean> {
    let seq = ++this.sequence;
  
    // 添加序号
    let packet = this.addSequence(data, seq);
  
    return new Promise(async (resolve) => {
      let attempt = () => {
        let pending = this.pendingAcks.get(seq);
      
        if (!pending || pending.retries >= this.maxRetries) {
          this.pendingAcks.delete(seq);
          resolve(false);
          return;
        }
      
        pending.retries++;
      
        // 发送数据
        udpClient.send(packet, address, port);
      
        // 设置超时
        pending.timer = setTimeout(attempt, this.ackTimeout);
      };
    
      // 初始化
      this.pendingAcks.set(seq, {
        data: packet,
        timer: -1,
        retries: 0
      });
    
      attempt();
    });
  }
  
  // 收到确认
  handleAck(seq: number): void {
    let pending = this.pendingAcks.get(seq);
  
    if (pending) {
      clearTimeout(pending.timer);
      this.pendingAcks.delete(seq);
    }
  }
  
  // 添加序号到数据包
  private addSequence(data: ArrayBuffer, seq: number): ArrayBuffer {
    let packet = new ArrayBuffer(4 + data.byteLength);
    let view = new DataView(packet);
    view.setUint32(0, seq, false);
  
    let dataView = new Uint8Array(packet, 4);
    let sourceView = new Uint8Array(data);
    for (let i = 0; i < sourceView.length; i++) {
      dataView[i] = sourceView[i];
    }
  
    return packet;
  }
}

坑点三:端口被占用

绑定端口时可能被其他程序占用。

// ❌ 错误:端口被占用时直接失败
await udpSocket.bind({
  address: '0.0.0.0',
  port: 8888  // 可能被占用
});

// ✅ 正确:尝试多个端口
async function bindWithFallback(socket: socket.UDPSocket, preferredPort: number): Promise<number> {
  let ports = [preferredPort, 8889, 8890, 8891, 0];  // 0表示自动分配
  
  for (let port of ports) {
    try {
      await socket.bind({
        address: '0.0.0.0',
        port: port,
        family: 1
      });
    
      let state = await socket.getState();
      return state.localPort;
    } catch (error) {
      console.warn(`端口 ${port} 绑定失败,尝试下一个`);
    }
  }
  
  throw new Error('无法绑定任何端口');
}

五、HarmonyOS 6适配指南

5.1 多播支持

HarmonyOS 6支持UDP多播(Multicast)。

import socket from '@ohos.net.socket';

let udpSocket = socket.constructUDPSocketInstance();

await udpSocket.bind({
  address: '0.0.0.0',
  port: 8888,
  family: 1
});

// HarmonyOS 6: 加入多播组
await udpSocket.addMembership({
  multicastAddress: '239.255.255.250',  // 多播组地址
  interface: '192.168.1.100'  // 本地接口
});

// 发送多播数据
await udpSocket.send({
  data: encoder.encode('Hello Multicast').buffer,
  address: {
    address: '239.255.255.250',
    port: 8888,
    family: 1
  }
});

// 离开多播组
await udpSocket.dropMembership({
  multicastAddress: '239.255.255.250',
  interface: '192.168.1.100'
});

5.2 设置Socket选项

HarmonyOS 6提供了更多Socket选项设置。

// HarmonyOS 6: 设置UDP选项
await udpSocket.setOption({
  // 设置广播
  broadcast: true,
  
  // 设置接收缓冲区大小
  receiveBufferSize: 65536,
  
  // 设置发送缓冲区大小
  sendBufferSize: 65536,
  
  // 设置TTL(Time To Live)
  ttl: 64,
  
  // 设置多播TTL
  multicastTtl: 1,
  
  // 设置多播回环
  multicastLoopback: false
});

六、总结一下下

UDP是追求速度场景的首选方案。本文从三个场景展开:

UDP客户端:无需建立连接,直接发送数据。注意绑定本地端口、设置接收监听、处理响应。

UDP服务端:监听端口,接收并响应请求。UDP无连接特性使得服务端实现比TCP简单得多。

设备发现:UDP广播的经典应用。发送广播发现请求,接收设备公告,维护设备列表。

三个常见坑点:数据包大小限制、丢包未处理、端口被占用。遇到UDP问题时,先排查这三个方面。

HarmonyOS 6带来了多播支持和丰富的Socket选项,让UDP编程更加强大。但切记:UDP的"不可靠"是特性而非缺陷,在合适的场景下,它比TCP更优秀。

下一篇文章,我们将探讨网络状态监听,实现在线/离线检测功能!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐