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心跳与重连机制,确保长连接的稳定性!