/** * @Author: Gongxh * @Date: 2025-03-28 * @Description: 网络socket */ import { Platform } from "../../global/Platform"; import { debug, warn } from "../../tool/log"; type BinaryType = "blob" | "arraybuffer"; interface SocketOptions { /** * 给原生平台 和 web 用 * 一个协议字符串或者一个包含协议字符串的数组。 * 这些字符串用于指定子协议,这样单个服务器可以实现多个 WebSocket 子协议( * 例如,你可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互。 * 如果不指定协议字符串,则假定为空字符串。 */ protocols?: string[]; /** * 使用 Blob 对象处理二进制数据。这是默认值 * 使用 ArrayBuffer 对象处理二进制数据 * @url https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType */ binaryType?: BinaryType; /** 超时时间 默认3000毫秒 */ timeout?: number; } export class Socket { /** * socket对象 * @internal */ private _socket: WebSocket | WechatMiniprogram.SocketTask | AliyMiniprogram.SocketTask | BytedanceMiniprogram.SocketTask; /** * @param {string} url 要连接的 URL;这应该是 WebSocket 服务器将响应的 URL * @param {SocketOptions} options 可选参数 针对不同平台的一些特殊参数 详细信息见定义 */ constructor(url: string, options?: SocketOptions) { if (Platform.isWX) { this._socket = this.createWechatSocket(url, options?.timeout || 3000, options?.protocols); } else if (Platform.isAlipay) { this._socket = this.createAliSocket(url, options?.timeout || 3000, options?.protocols); } else if (Platform.isBytedance) { this._socket = this.createBytedanceSocket(url, options?.timeout || 3000, options?.protocols); } else { this._socket = this.createOtherSocket(url, options?.binaryType, options?.timeout || 3000, options?.protocols); } } /** * 微信小游戏创建socket * @internal */ private createWechatSocket(url: string, timeout?: number, protocols?: string[]): WechatMiniprogram.SocketTask { let socket = wx.connectSocket({ url, protocols: protocols, timeout: timeout, success: () => { debug("socket success") }, fail: () => { warn("socket fail") } }); socket.onOpen(() => { this.onopen && this.onopen(); }); socket.onMessage((res: { data: string | ArrayBuffer }) => { this.onmessage && this.onmessage(res.data); }); socket.onError((res: { errMsg: string }) => { // 微信上socket和原生平台以及浏览器不一致 所以这里特殊处理 给他一个默认的错误码 this.onclose?.(1000, res?.errMsg); }); socket.onClose((res: { code: number, reason: string }) => { this.onclose?.(res.code, res.reason); }); return socket; } /** * 支付宝小游戏创建socket * @internal */ private createAliSocket(url: string, timeout?: number, protocols?: string[]): AliyMiniprogram.SocketTask { let socket = my.connectSocket({ url, protocols: protocols, multiple: true, timeout: timeout, success: () => { debug("socket success") }, fail: () => { warn("socket fail") } }); socket.onOpen((info: AliyMiniprogram.OnOpenData) => { this.onopen && this.onopen(); }); socket.onMessage((info: AliyMiniprogram.OnMessageData) => { if (!this.onmessage) { return; } if (info.isBuffer) { if (my.base64ToArrayBuffer) { this.onmessage(my.base64ToArrayBuffer(info.data.data)); } else if (atob) { this.onmessage(this.base64ToArrayBuffer(info.data.data)); } else { this.onmessage(info.data.data); } this.onmessage(info.data.data); } else { this.onmessage(info.data.data); } }); socket.onError((info: { data: AliyMiniprogram.CallBack.Fail }) => { this.onclose && this.onclose(info.data.error, info.data.errorMessage); }); socket.onClose((info: { code: number, reason: string, data: { code: number, reason: string } }) => { this.onclose && this.onclose(info.code, info.reason); }); return socket; } /** * 抖音小游戏创建socket * @internal */ private createBytedanceSocket(url: string, timeout?: number, protocols?: string[]): BytedanceMiniprogram.SocketTask { let socket: BytedanceMiniprogram.SocketTask = tt.connectSocket({ url, protocols: protocols, success: () => { debug("socket success") }, fail: () => { warn("socket fail") } }); let timer = setTimeout(() => { socket.close({}); }, timeout); socket.onOpen(() => { timer && clearTimeout(timer); timer = null; this.onopen && this.onopen(); }); socket.onMessage((info: { data: string | ArrayBuffer }) => { this.onmessage && this.onmessage(info.data); }); socket.onError((res: { errMsg: string }) => { timer && clearTimeout(timer); timer = null; // 微信上socket和原生平台以及浏览器不一致 所以这里特殊处理 给他一个默认的错误码 this.onclose?.(1000, res.errMsg); }); socket.onClose((res: { code: string, reason: string }) => { timer && clearTimeout(timer); timer = null; this.onclose?.(Number(res.code), res.reason); }); return socket; } /** * 除微信小游戏、支付宝小游戏、抖音小游戏之外的平台创建socket * @internal */ private createOtherSocket(url: string, binaryType: BinaryType, timeout?: number, protocols?: string[]): WebSocket { let socket = new WebSocket(url, protocols); if (binaryType) { socket.binaryType = binaryType; } let timer = setTimeout(() => { socket.close(); }, timeout); socket.onopen = () => { timer && clearTimeout(timer); timer = null; this.onopen?.(); } socket.onmessage = (event: MessageEvent) => { this.onmessage?.(event.data); } socket.onerror = () => { timer && clearTimeout(timer); timer = null; this.onerror?.(); } socket.onclose = (event: CloseEvent) => { timer && clearTimeout(timer); timer = null; this.onclose?.(event?.code, event?.reason); } return socket; } /** * 发送文本数据 * @param data - 文本数据 */ public send(data: string): void { if (Platform.isWX) { (this._socket as WechatMiniprogram.SocketTask).send({ data: data }); } else if (Platform.isAlipay) { (this._socket as AliyMiniprogram.SocketTask).send({ data: data }); } else if (Platform.isBytedance) { (this._socket as BytedanceMiniprogram.SocketTask).send({ data: data }); } else { (this._socket as WebSocket).send(data); } } /** * 发送二进制数据 * @param data - 二进制数据 */ public sendBuffer(data: ArrayBuffer): void { if (Platform.isWX) { (this._socket as WechatMiniprogram.SocketTask).send({ data: data }); } else if (Platform.isAlipay) { if (my.arrayBufferToBase64) { (this._socket as AliyMiniprogram.SocketTask).send({ data: my.arrayBufferToBase64(data), isBuffer: true }); } else if (btoa) { (this._socket as AliyMiniprogram.SocketTask).send({ data: this.uint8ArrayToBase64(new Uint8Array(data)), isBuffer: true }); } else { (this._socket as AliyMiniprogram.SocketTask).send({ data: data }); } } else if (Platform.isBytedance) { (this._socket as BytedanceMiniprogram.SocketTask).send({ data: data }); } else { (this._socket as WebSocket).send(data); } } /** * 客户端主动断开 * @param code - 关闭代码: 如果没有传这个参数,默认使用1000, 客户端可使用的数字范围: [3001-3999] * @param reason - 关闭原因: 一个人类可读的字符串,它解释了连接关闭的原因。这个 UTF-8 编码的字符串不能超过 123 个字节 */ public close(code?: number, reason?: string): void { if (Platform.isWX) { (this._socket as WechatMiniprogram.SocketTask).close({ code: code, reason: reason }); } else if (Platform.isAlipay) { (this._socket as AliyMiniprogram.SocketTask).close({ code: code, reason: reason }); } else if (Platform.isBytedance) { (this._socket as BytedanceMiniprogram.SocketTask).close({ code: code, reason: reason }); } else { (this._socket as WebSocket).close(code, reason); } } /** * 获取socket示例 * 在微信小游戏、支付宝小游戏、抖音小游戏 返回的是他们平台的socket实例类型 */ public socket(): T { return this._socket as T; } /** * socket已准备好 open成功 * 当前连接已经准备好发送和接受数据 */ public onopen: () => void; /** * 接收到服务端发送的消息 * @param data - 消息数据 */ public onmessage: (data: string | ArrayBuffer) => void; /** * 监听可能发生的错误,一般用不到 */ public onerror: () => void; /** * 关闭连接 * @param code - 关闭代码 * @param reason - 关闭原因 */ public onclose: (code: number, reason: string) => void; /** * 二进制数据转成Base64 (支付宝用) * @internal */ private uint8ArrayToBase64(u8Array: Uint8Array): string { let CHUNK_SIZE = 0x8000; // 32768 let index = 0; let length = u8Array.length; let result = ''; let slice: Uint8Array;; // 分段处理,避免`btoa`输入字符串过长 for (; index < length; index += CHUNK_SIZE) { slice = u8Array.subarray(index, Math.min(index + CHUNK_SIZE, length)); // 将Uint8Array转换为字符串并使用btoa进行Base64编码 result += btoa(String.fromCharCode(...slice)); } return result; } /** * base64转成ArrayBuffer (支付宝用) * @internal */ private base64ToArrayBuffer(base64: string): ArrayBuffer { let binary_string = atob(base64); let len = binary_string.length; let bytes = new Uint8Array(len); for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } return bytes.buffer; } }