Laya.Socket 使用指南

简介

Laya.Socket 是 LayaAir 引擎提供的 WebSocket 网络通信类,封装了 HTML5 WebSocket,实现服务器与客户端之间的全双工实时通信。

适用场景:

  • 多人在线游戏(即时战斗、聊天)
  • 实时数据同步
  • 跨域通信
  • 长连接推送服务

工作原理:

建立连接 → 交换数据 → 心跳维持 → 连接关闭/重连

核心优势:

优势说明
全双工通信服务器和客户端可主动互相发送数据
低延迟基于 WebSocket 协议,实时性高
跨域支持支持跨域通信,无需 JSONP
二进制支持支持文本和二进制数据传输

目录


API 参考

构造函数

方法参数说明
constructor(host?: string, port?: number, protocols?: string[], isSecure?: boolean)主机, 端口, 子协议, 是否安全连接创建 Socket 对象,默认字节序为 BIG_ENDIAN

连接方法

方法参数返回值说明
connect(host: string, port: number, isSecure?: boolean, protocols?: string[])主机, 端口, 安全协议, 子协议void连接到指定主机和端口
connectByUrl(url: string, protocols?: string[])WebSocket URL, 子协议void通过 URL 连接服务器

数据发送

方法参数返回值说明
send(data: string | ArrayBuffer)字符串或二进制数据Promise<void>发送数据到服务器
flush()无void发送 output 缓冲区中的数据到服务器

连接控制

方法参数返回值说明
close()无void关闭连接
cleanSocket()无void清理 Socket:关闭连接,移除监听,重置状态

属性

属性类型只读说明
connectedboolean✅当前是否已连接
inputLaya.Byte✅从服务器接收的数据缓存
outputLaya.Byte✅需要发送到服务器的数据缓冲区
endianstring❌字节序(取值为 Laya.Byte.BIG_ENDIAN 或 Laya.Byte.LITTLE_ENDIAN)
disableInputboolean❌是否禁用输入缓存(字符串数据建议设为 true)

字节序常量(来自 Laya.Byte):

常量值说明
Laya.Byte.LITTLE_ENDIAN"littleEndian"小端字节序
Laya.Byte.BIG_ENDIAN"bigEndian"大端字节序(默认)

事件

事件触发时机事件参数
Laya.Event.OPEN连接成功建立HTML DOM Event
Laya.Event.MESSAGE接收到数据数据内容(字符串或 ArrayBuffer)
Laya.Event.CLOSE连接关闭HTML DOM Event
Laya.Event.ERROR发生错误HTML DOM Event

基础用法

1. 通过 URL 连接(推荐)

最简洁的连接方式:

class GameSocket {
    private socket: Laya.Socket;

    constructor() {
        this.socket = new Laya.Socket();

        // 监听事件
        this.socket.on(Laya.Event.OPEN, this, this.onOpen);
        this.socket.on(Laya.Event.MESSAGE, this, this.onMessage);
        this.socket.on(Laya.Event.CLOSE, this, this.onClose);
        this.socket.on(Laya.Event.ERROR, this, this.onError);

        // 连接服务器
        this.socket.connectByUrl("ws://localhost:8080");
    }

    private onOpen(): void {
        console.log("连接成功");
    }

    private onMessage(data: any): void {
        if (typeof data === "string") {
            console.log("收到文本消息:", data);
        } else if (data instanceof ArrayBuffer) {
            console.log("收到二进制消息:", data);
        }
    }

    private onClose(): void {
        console.log("连接关闭");
    }

    private onError(): void {
        console.log("连接错误");
    }
}

2. 通过主机和端口连接

// 普通 WebSocket 连接
let socket = new Laya.Socket();
socket.connect("localhost", 8080);

// 安全 WebSocket 连接 (wss)
socket.connect("localhost", 8080, true);

3. 发送字符串消息

// 连接成功后发送
private onOpen(): void {
    // 直接发送字符串
    this.socket.send("Hello Server!");
}

4. 发送二进制数据

private sendBinaryData(): void {
    // 方式1: 使用 send 直接发送 ArrayBuffer
    let buffer = new ArrayBuffer(4);
    let view = new DataView(buffer);
    view.setInt32(0, 12345);
    this.socket.send(buffer);

    // 方式2: 使用 output 缓冲区
    this.socket.output.writeByte(1);
    this.socket.output.writeInt32(12345);
    this.socket.output.writeUTFString("test");
    this.socket.flush();  // 必须调用 flush 发送
}

5. 接收并解析数据

private onMessage(data: any): void {
    if (typeof data === "string") {
        // 处理字符串消息
        this.handleTextMessage(data);
    } else if (data instanceof ArrayBuffer) {
        // 处理二进制消息
        this.handleBinaryMessage(data);
    }
}

private handleBinaryMessage(buffer: ArrayBuffer): void {
    let byte = new Laya.Byte(buffer);
    byte.endian = Laya.Byte.BIG_ENDIAN;

    let msgType = byte.getByte();
    let msgId = byte.getInt32();
    let content = byte.getUTFString();

    console.log(`消息类型: ${msgType}, ID: ${msgId}, 内容: ${content}`);
}

实用示例

示例1: 基础 WebSocket 通信

@regClass()
export class NetworkManager extends Laya.Script {
    private socket: Laya.Socket;
    private reconnectDelay: number = 3000;

    onAwake(): void {
        this.initSocket();
    }

    private initSocket(): void {
        this.socket = new Laya.Socket();
        this.socket.disableInput = true;  // 禁用输入缓存,使用直接接收

        this.socket.on(Laya.Event.OPEN, this, this.onOpen);
        this.socket.on(Laya.Event.MESSAGE, this, this.onMessage);
        this.socket.on(Laya.Event.CLOSE, this, this.onClose);
        this.socket.on(Laya.Event.ERROR, this, this.onError);

        this.connect();
    }

    private connect(): void {
        console.log("正在连接服务器...");
        this.socket.connectByUrl("ws://localhost:8080/ws");
    }

    private onOpen(): void {
        console.log("服务器连接成功");
        this.reconnectDelay = 3000;  // 重置重连延迟

        // 发送登录消息
        this.sendMessage({
            type: "login",
            userId: "player123",
            token: "xxx"
        });
    }

    private onMessage(data: any): void {
        if (typeof data === "string") {
            let msg = JSON.parse(data);
            this.handleMessage(msg);
        }
    }

    private onClose(): void {
        console.log("服务器连接关闭,尝试重连...");
        this.scheduleReconnect();
    }

    private onError(): void {
        console.log("连接错误,尝试重连...");
        this.scheduleReconnect();
    }

    private scheduleReconnect(): void {
        Laya.timer.clear(this, this.connect);  // 清除之前的重连定时器
        Laya.timer.once(this.reconnectDelay, this, this.connect);
        this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);  // 最大30秒
    }

    public sendMessage(msg: any): void {
        if (!this.socket.connected) {
            console.warn("未连接到服务器");
            return;
        }
        this.socket.send(JSON.stringify(msg));
    }

    private handleMessage(msg: any): void {
        switch (msg.type) {
            case "chat":
                this.onChatMessage(msg);
                break;
            case "move":
                this.onPlayerMove(msg);
                break;
            case "battle":
                this.onBattleUpdate(msg);
                break;
        }
    }

    private onChatMessage(msg: any): void {
        console.log(`${msg.player}: ${msg.content}`);
    }

    private onPlayerMove(msg: any): void {
        // 处理玩家移动
    }

    private onBattleUpdate(msg: any): void {
        // 处理战斗更新
    }

    onDestroy(): void {
        this.socket.offAll();
        this.socket.close();
        Laya.timer.clearAll(this);
    }
}

示例2: 二进制协议通信

// 协议定义
enum MsgType {
    LOGIN = 1,
    MOVE = 2,
    ATTACK = 3,
    SYNC = 4
}

@regClass()
export class BinarySocket extends Laya.Script {
    private socket: Laya.Socket;

    onAwake(): void {
        this.socket = new Laya.Socket();
        this.socket.endian = Laya.Byte.LITTLE_ENDIAN;

        this.socket.on(Laya.Event.OPEN, this, this.onOpen);
        this.socket.on(Laya.Event.MESSAGE, this, this.onMessage);
        this.socket.connectByUrl("ws://localhost:8080");
    }

    private onOpen(): void {
        // 发送登录消息
        this.sendLogin("player123", "token");
    }

    // 发送登录消息
    public sendLogin(userId: string, token: string): void {
        this.socket.output.writeByte(MsgType.LOGIN);
        this.socket.output.writeUTFString(userId);
        this.socket.output.writeUTFString(token);
        this.socket.flush();
    }

    // 发送移动消息
    public sendMove(x: number, y: number): void {
        this.socket.output.clear();
        this.socket.output.writeByte(MsgType.MOVE);
        this.socket.output.writeFloat32(x);
        this.socket.output.writeFloat32(y);
        this.socket.flush();
    }

    // 发送攻击消息
    public sendAttack(targetId: number, skillId: number): void {
        this.socket.output.clear();
        this.socket.output.writeByte(MsgType.ATTACK);
        this.socket.output.writeInt32(targetId);
        this.socket.output.writeInt32(skillId);
        this.socket.flush();
    }

    private onMessage(data: any): void {
        if (!(data instanceof ArrayBuffer)) return;

        let byte = new Laya.Byte(data);
        byte.endian = Laya.Byte.LITTLE_ENDIAN;

        let msgType = byte.getByte();

        switch (msgType) {
            case MsgType.LOGIN:
                this.handleLogin(byte);
                break;
            case MsgType.MOVE:
                this.handleMove(byte);
                break;
            case MsgType.ATTACK:
                this.handleAttack(byte);
                break;
            case MsgType.SYNC:
                this.handleSync(byte);
                break;
        }
    }

    private handleLogin(byte: Laya.Byte): void {
        let success = byte.getByte() === 1;
        if (success) {
            console.log("登录成功");
        } else {
            console.log("登录失败");
        }
    }

    private handleMove(byte: Laya.Byte): void {
        let playerId = byte.getInt32();
        let x = byte.getFloat32();
        let y = byte.getFloat32();
        console.log(`玩家 ${playerId} 移动到 (${x}, ${y})`);
    }

    private handleAttack(byte: Laya.Byte): void {
        let attackerId = byte.getInt32();
        let targetId = byte.getInt32();
        let damage = byte.getInt32();
        console.log(`玩家 ${attackerId} 攻击 ${targetId},造成 ${damage} 伤害`);
    }

    private handleSync(byte: Laya.Byte): void {
        let count = byte.getByte();
        for (let i = 0; i < count; i++) {
            let id = byte.getInt32();
            let x = byte.getFloat32();
            let y = byte.getFloat32();
            // 同步玩家位置...
        }
    }
}

示例3: 心跳保活机制

@regClass()
export class HeartbeatSocket extends Laya.Script {
    private socket: Laya.Socket;
    private heartbeatInterval: number = 30000;  // 30秒
    private timeoutDelay: number = 5000;  // 5秒超时
    private lastPingTime: number = 0;

    onAwake(): void {
        this.initSocket();
    }

    private initSocket(): void {
        this.socket = new Laya.Socket();
        this.socket.on(Laya.Event.OPEN, this, this.onOpen);
        this.socket.on(Laya.Event.MESSAGE, this, this.onMessage);
        this.socket.on(Laya.Event.CLOSE, this, this.onClose);
        this.socket.connectByUrl("ws://localhost:8080");
    }

    private onOpen(): void {
        console.log("连接成功");
        this.startHeartbeat();
    }

    private onMessage(data: any): void {
        if (typeof data === "string") {
            let msg = JSON.parse(data);
            if (msg.type === "pong") {
                this.onPong();
            }
        }
    }

    private startHeartbeat(): void {
        this.stopHeartbeat();
        Laya.timer.loop(
            this.heartbeatInterval,
            this,
            this.sendPing
        );
    }

    private stopHeartbeat(): void {
        Laya.timer.clear(this, this.sendPing);
        Laya.timer.clear(this, this.onTimeout);
    }

    private sendPing(): void {
        this.lastPingTime = Date.now();
        this.socket.send(JSON.stringify({ type: "ping" }));
        Laya.timer.once(this.timeoutDelay, this, this.onTimeout);
    }

    private onPong(): void {
        // 收到响应,清除超时检测
        Laya.timer.clear(this, this.onTimeout);
        let latency = Date.now() - this.lastPingTime;
        console.log(`网络延迟: ${latency}ms`);
    }

    private onTimeout(): void {
        console.warn("心跳超时,连接可能已断开");
        this.socket.close();
    }

    private onClose(): void {
        this.stopHeartbeat();
        console.log("连接关闭");
    }

    onDestroy(): void {
        this.stopHeartbeat();
        this.socket.offAll();
        this.socket.close();
    }
}

示例4: 断线重连与消息队列

@regClass()
export class ReliableSocket extends Laya.Script {
    private socket: Laya.Socket;
    private messageQueue: string[] = [];
    private isConnecting: boolean = false;
    private reconnectAttempts: number = 0;
    private maxReconnectAttempts: number = 5;

    onAwake(): void {
        this.connect();
    }

    private connect(): void {
        if (this.isConnecting) return;
        this.isConnecting = true;

        this.socket = new Laya.Socket();
        this.socket.on(Laya.Event.OPEN, this, this.onOpen);
        this.socket.on(Laya.Event.MESSAGE, this, this.onMessage);
        this.socket.on(Laya.Event.CLOSE, this, this.onClose);
        this.socket.on(Laya.Event.ERROR, this, this.onError);

        this.socket.connectByUrl("ws://localhost:8080");
    }

    private onOpen(): void {
        console.log("连接成功");
        this.isConnecting = false;
        this.reconnectAttempts = 0;

        // 发送队列中的消息
        this.flushMessageQueue();
    }

    private onClose(): void {
        console.log("连接关闭");
        this.isConnecting = false;
        this.scheduleReconnect();
    }

    private onError(): void {
        console.log("连接错误");
        this.isConnecting = false;
        this.scheduleReconnect();
    }

    private scheduleReconnect(): void {
        if (this.reconnectAttempts >= this.maxReconnectAttempts) {
            console.error("达到最大重连次数");
            return;
        }

        this.reconnectAttempts++;
        let delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 10000);

        console.log(`${delay}ms 后尝试第 ${this.reconnectAttempts} 次重连`);
        Laya.timer.once(delay, this, this.connect);
    }

    // 发送消息(自动处理断线情况)
    public send(msg: any): void {
        let data = JSON.stringify(msg);

        if (this.socket && this.socket.connected) {
            this.socket.send(data);
        } else {
            console.log("未连接,消息加入队列");
            this.messageQueue.push(data);

            // 如果没有在连接中,开始连接
            if (!this.isConnecting) {
                this.connect();
            }
        }
    }

    private flushMessageQueue(): void {
        while (this.messageQueue.length > 0 && this.socket.connected) {
            let msg = this.messageQueue.shift();
            this.socket.send(msg);
        }
    }

    private onMessage(data: any): void {
        if (typeof data === "string") {
            let msg = JSON.parse(data);
            console.log("收到消息:", msg);
        }
    }

    onDestroy(): void {
        this.socket.offAll();
        this.socket.close();
        Laya.timer.clearAll(this);
    }
}

示例5: 房间匹配与游戏同步

interface Player {
    id: string;
    name: string;
    x: number;
    y: number;
    hp: number;
}

@regClass()
export class GameNetwork extends Laya.Script {
    private socket: Laya.Socket;
    private players: Map<string, Player> = new Map();
    private localPlayer: Player = null!;

    onAwake(): void {
        this.initSocket();
    }

    private initSocket(): void {
        this.socket = new Laya.Socket();
        this.socket.on(Laya.Event.OPEN, this, this.onOpen);
        this.socket.on(Laya.Event.MESSAGE, this, this.onMessage);
        this.socket.connectByUrl("ws://localhost:8080/game");
    }

    private onOpen(): void {
        console.log("游戏服务器连接成功");
        this.matchRoom();
    }

    // 匹配房间
    public matchRoom(): void {
        this.send({
            type: "match",
            mode: "1v1"
        });
    }

    // 发送移动
    public sendMove(x: number, y: number): void {
        this.send({
            type: "move",
            x: x,
            y: y
        });
    }

    // 发送攻击
    public sendAttack(targetId: string): void {
        this.send({
            type: "attack",
            targetId: targetId
        });
    }

    private send(msg: any): void {
        if (this.socket.connected) {
            this.socket.send(JSON.stringify(msg));
        }
    }

    private onMessage(data: any): void {
        if (typeof data !== "string") return;

        let msg = JSON.parse(data);

        switch (msg.type) {
            case "matched":
                this.onMatched(msg);
                break;
            case "gameStart":
                this.onGameStart(msg);
                break;
            case "playerJoin":
                this.onPlayerJoin(msg);
                break;
            case "playerLeave":
                this.onPlayerLeave(msg);
                break;
            case "playerMove":
                this.onPlayerMove(msg);
                break;
            case "playerAttack":
                this.onPlayerAttack(msg);
                break;
            case "gameState":
                this.onGameState(msg);
                break;
            case "gameOver":
                this.onGameOver(msg);
                break;
        }
    }

    private onMatched(msg: any): void {
        console.log("匹配成功,房间 ID:", msg.roomId);
    }

    private onGameStart(msg: any): void {
        console.log("游戏开始");
        this.localPlayer = {
            id: msg.playerId,
            name: msg.playerName,
            x: msg.x,
            y: msg.y,
            hp: 100
        };
    }

    private onPlayerJoin(msg: any): void {
        let player: Player = {
            id: msg.id,
            name: msg.name,
            x: msg.x,
            y: msg.y,
            hp: 100
        };
        this.players.set(player.id, player);
        console.log(`${player.name} 加入游戏`);
    }

    private onPlayerLeave(msg: any): void {
        this.players.delete(msg.id);
        console.log(`玩家 ${msg.id} 离开游戏`);
    }

    private onPlayerMove(msg: any): void {
        let player = this.players.get(msg.id);
        if (player) {
            player.x = msg.x;
            player.y = msg.y;
        }
    }

    private onPlayerAttack(msg: any): void {
        console.log(`${msg.attackerId} 攻击 ${msg.targetId},伤害 ${msg.damage}`);
        // 播放攻击特效...
    }

    private onGameState(msg: any): void {
        // 全量状态同步
        for (let p of msg.players) {
            let player = this.players.get(p.id) || {
                id: p.id,
                name: p.name,
                x: 0,
                y: 0,
                hp: 100
            };
            player.x = p.x;
            player.y = p.y;
            player.hp = p.hp;
            this.players.set(player.id, player);
        }
    }

    private onGameOver(msg: any): void {
        console.log("游戏结束,结果:", msg.result);
    }

    onDestroy(): void {
        this.socket.offAll();
        this.socket.close();
    }
}

高级技巧

1. 协议封包与解包

class Packet {
    static write(type: number, data: any): ArrayBuffer {
        let json = JSON.stringify(data);
        let jsonBytes = new TextEncoder().encode(json);
        let buffer = new ArrayBuffer(3 + jsonBytes.length);
        let view = new DataView(buffer);

        view.setUint8(0, type);           // 消息类型 (1 byte)
        view.setUint16(1, jsonBytes.length);  // 数据长度 (2 bytes)

        for (let i = 0; i < jsonBytes.length; i++) {
            view.setUint8(3 + i, jsonBytes[i]);
        }

        return buffer;
    }

    static read(buffer: ArrayBuffer): { type: number, data: any } {
        let view = new DataView(buffer);
        let type = view.getUint8(0);
        let length = view.getUint16(1);
        let bytes = new Uint8Array(buffer, 3, length);
        let json = new TextDecoder().decode(bytes);
        let data = JSON.parse(json);

        return { type, data };
    }
}

// 使用
let packet = Packet.write(1, { action: "move", x: 100, y: 200 });
socket.send(packet);

// 接收
private onMessage(data: any): void {
    let msg = Packet.read(data);
    console.log(`类型: ${msg.type}, 数据:`, msg.data);
}

2. 消息处理器注册

type MessageHandler = (data: any) => void;

class NetworkDispatcher {
    private handlers: Map<string, MessageHandler> = new Map();
    private socket: Laya.Socket;

    constructor(socket: Laya.Socket) {
        this.socket = socket;
        this.socket.on(Laya.Event.MESSAGE, this, this.onMessage);
    }

    // 注册消息处理器
    on(type: string, handler: MessageHandler): void {
        this.handlers.set(type, handler);
    }

    // 发送消息
    send(type: string, data: any): void {
        this.socket.send(JSON.stringify({ type, ...data }));
    }

    private onMessage(data: any): void {
        if (typeof data !== "string") return;

        let msg = JSON.parse(data);
        let handler = this.handlers.get(msg.type);

        if (handler) {
            handler(msg);
        }
    }

    destroy(): void {
        this.socket.offAll();
        this.handlers.clear();
    }
}

// 使用
let dispatcher = new NetworkDispatcher(socket);

dispatcher.on("login", (data) => {
    console.log("登录响应:", data);
});

dispatcher.on("chat", (data) => {
    console.log(`${data.player}: ${data.message}`);
});

dispatcher.send("login", { userId: "123", token: "xxx" });

3. SSL/TLS 安全连接

// 使用 wss 协议
let socket = new Laya.Socket();

// 方式1: 通过 connect 的 isSecure 参数
socket.connect("secure.example.com", 443, true);

// 方式2: 通过 URL
socket.connectByUrl("wss://secure.example.com:443/ws");

4. 子协议协商

// 指定子协议
let protocols = ["chat", "superchat"];
let socket = new Laya.Socket();
socket.connectByUrl("ws://localhost:8080", protocols);

// 或者使用 connect
socket.connect("localhost", 8080, false, protocols);

5. 处理大文件传输

class FileTransfer {
    private socket: Laya.Socket;
    private chunkSize: number = 1024 * 64;  // 64KB 分片

    constructor(socket: Laya.Socket) {
        this.socket = socket;
    }

    // 发送文件
    sendFile(file: ArrayBuffer, filename: string): Promise<void> {
        return new Promise((resolve, reject) => {
            let totalChunks = Math.ceil(file.byteLength / this.chunkSize);
            let chunkIndex = 0;

            let sendChunk = () => {
                let start = chunkIndex * this.chunkSize;
                let end = Math.min(start + this.chunkSize, file.byteLength);
                let chunk = file.slice(start, end);

                this.socket.send(JSON.stringify({
                    type: "fileChunk",
                    filename: filename,
                    chunkIndex: chunkIndex,
                    totalChunks: totalChunks,
                    data: chunk
                }));

                chunkIndex++;

                if (chunkIndex < totalChunks) {
                    // 流控:等待一点时间再发下一片
                    Laya.timer.once(10, this, sendChunk);
                } else {
                    resolve();
                }
            };

            sendChunk();
        });
    }
}

最佳实践

1. 错误处理与重连

class RobustSocket {
    private socket: Laya.Socket;
    private reconnectDelay: number = 1000;
    private maxReconnectDelay: number = 30000;

    connect(): void {
        this.socket = new Laya.Socket();

        this.socket.on(Laya.Event.ERROR, this, () => {
            this.scheduleReconnect();
        });

        this.socket.on(Laya.Event.CLOSE, this, () => {
            this.scheduleReconnect();
        });

        this.socket.connectByUrl("ws://localhost:8080");
    }

    private scheduleReconnect(): void {
        Laya.timer.once(this.reconnectDelay, this, this.connect);
        this.reconnectDelay = Math.min(
            this.reconnectDelay * 2,
            this.maxReconnectDelay
        );
    }
}

2. 统一的连接管理

@regClass()
export class SocketManager {
    private static instance: SocketManager;
    private socket: Laya.Socket;

    static getInstance(): SocketManager {
        if (!SocketManager.instance) {
            SocketManager.instance = new SocketManager();
        }
        return SocketManager.instance;
    }

    private constructor() {
        this.socket = new Laya.Socket();
    }

    connect(url: string): void {
        this.socket.connectByUrl(url);
    }

    send(type: string, data: any): void {
        this.socket.send(JSON.stringify({ type, ...data }));
    }

    on(type: string, handler: Function): void {
        this.socket.on(type, handler);
    }

    disconnect(): void {
        this.socket.cleanSocket();
    }
}

// 全局使用
let socket = SocketManager.getInstance();
socket.connect("ws://localhost:8080");

3. 消息序列化优化

// 字符串数据禁用二进制转换
this.socket.disableInput = true;

// 二进制数据设置正确的字节序
this.socket.endian = Laya.Byte.LITTLE_ENDIAN;

4. 资源清理

@regClass()
export class GameScene extends Laya.Scene {
    private socket: Laya.Socket;

    onAwake(): void {
        this.socket = new Laya.Socket();
        // ... 初始化
    }

    onDestroy(): void {
        // 必须清理
        this.socket.offAll();
        this.socket.cleanSocket();  // 或 close()
    }
}

5. 连接状态检测

// 发送前检查连接
public send(msg: any): boolean {
    if (!this.socket || !this.socket.connected) {
        console.warn("Socket 未连接");
        return false;
    }
    this.socket.send(JSON.stringify(msg));
    return true;
}

6. 心跳与超时处理

// 定期发送心跳保持连接
Laya.timer.loop(30000, this, () => {
    if (this.socket.connected) {
        this.socket.send(JSON.stringify({ type: "heartbeat" }));
    }
});

测试服务器

完整的 Socket 通信需要服务端和客户端配合。本节提供:

  1. 服务端代码 - Node.js WebSocket 服务器
  2. 客户端代码 - Laya.Socket 连接测试

服务端代码

创建 socket-test-server.js:

const WebSocket = require('ws');
const PORT = 8080;
const wss = new WebSocket.Server({ port: PORT });

console.log(`WebSocket 服务器启动在 ws://localhost:${PORT}`);

// 存储所有连接的客户端
const clients = new Map();

wss.on('connection', (ws) => {
    const clientId = Math.random().toString(36).substring(2, 10);
    const clientInfo = {
        id: clientId,
        ws: ws,
        name: `Player_${clientId.substring(0, 4)}`,
        x: 0,
        y: 0,
        hp: 100
    };
    clients.set(clientId, clientInfo);

    // 发送连接成功消息
    ws.send(JSON.stringify({
        type: 'connected',
        clientId: clientId
    }));

    ws.on('message', (data) => {
        let msg = JSON.parse(data);
        handleMessage(clientInfo, msg);
    });

    ws.on('close', () => {
        clients.delete(clientId);
        broadcast({ type: 'playerLeave', id: clientId });
    });
});

function handleMessage(client, msg) {
    console.log(`[${client.name}] ${msg.type}`);

    switch (msg.type) {
        case 'ping':
            client.ws.send(JSON.stringify({ type: 'pong' }));
            break;

        case 'login':
            client.name = msg.userId || client.name;
            client.ws.send(JSON.stringify({
                type: 'login',
                success: true,
                playerId: client.id
            }));
            break;

        case 'move':
            client.x = msg.x;
            client.y = msg.y;
            broadcast({
                type: 'playerMove',
                id: client.id,
                name: client.name,
                x: client.x,
                y: client.y
            }, client.id);
            break;

        case 'chat':
            broadcast({
                type: 'chat',
                playerId: client.id,
                playerName: client.name,
                content: msg.content
            });
            break;

        case 'match':
            setTimeout(() => {
                client.ws.send(JSON.stringify({
                    type: 'matched',
                    roomId: 'room_' + Date.now(),
                    players: Array.from(clients.values()).map(c => ({
                        id: c.id,
                        name: c.name,
                        x: c.x,
                        y: c.y,
                        hp: c.hp
                    }))
                }));
            }, 1000);
            break;
    }
}

function broadcast(data, excludeId) {
    const message = JSON.stringify(data);
    clients.forEach((client, id) => {
        if (id !== excludeId && client.ws.readyState === WebSocket.OPEN) {
            client.ws.send(message);
        }
    });
}

运行服务端:

npm install ws
node socket-test-server.js

客户端代码 (Laya.Socket)

@regClass()
export class TestSocketClient extends Laya.Script {
    private socket: Laya.Socket;
    private myPlayerId: string = "";

    onAwake(): void {
        this.socket = new Laya.Socket();
        this.socket.disableInput = true;

        // 监听事件
        this.socket.on(Laya.Event.OPEN, this, this.onOpen);
        this.socket.on(Laya.Event.MESSAGE, this, this.onMessage);
        this.socket.on(Laya.Event.CLOSE, this, this.onClose);
        this.socket.on(Laya.Event.ERROR, this, this.onError);

        // 连接服务器
        this.socket.connectByUrl("ws://localhost:8080");
    }

    // ========== 发送消息 ==========

    private onOpen(): void {
        console.log("服务器连接成功");

        // 发送登录
        this.sendLogin("player123");

        // 发送聊天
        this.sendChat("Hello Server!");

        // 发送移动
        this.sendMove(100, 200);

        // 请求匹配
        this.sendMatch("1v1");
    }

    private sendLogin(userId: string): void {
        this.socket.send(JSON.stringify({
            type: "login",
            userId: userId
        }));
    }

    private sendChat(content: string): void {
        this.socket.send(JSON.stringify({
            type: "chat",
            content: content
        }));
    }

    private sendMove(x: number, y: number): void {
        this.socket.send(JSON.stringify({
            type: "move",
            x: x,
            y: y
        }));
    }

    private sendMatch(mode: string): void {
        this.socket.send(JSON.stringify({
            type: "match",
            mode: mode
        }));
    }

    private sendPing(): void {
        this.socket.send(JSON.stringify({ type: "ping" }));
    }

    // ========== 接收消息 ==========

    private onMessage(data: any): void {
        let msg = JSON.parse(data);

        switch (msg.type) {
            case "connected":
                this.myPlayerId = msg.clientId;
                console.log("连接成功,我的 ID:", this.myPlayerId);
                break;

            case "login":
                console.log("登录成功,玩家 ID:", msg.playerId);
                break;

            case "playerJoin":
            case "playerLeave":
                console.log(`玩家 ${msg.id} ${msg.type === 'playerJoin' ? '加入' : '离开'}`);
                break;

            case "playerMove":
                console.log(`${msg.name} 移动到 (${msg.x}, ${msg.y})`);
                break;

            case "chat":
                console.log(`${msg.playerName}: ${msg.content}`);
                break;

            case "matched":
                console.log("匹配成功,房间:", msg.roomId);
                console.log("房间内玩家:", msg.players);
                break;

            case "pong":
                console.log("收到心跳响应");
                break;
        }
    }

    private onClose(): void {
        console.log("连接关闭");
    }

    private onError(): void {
        console.log("连接错误");
    }

    onDestroy(): void {
        this.socket.offAll();
        this.socket.close();
    }
}

消息流程图

客户端                              服务端
  |                                   |
  |------- connectByUrl --------------->|
  |<----- Event.OPEN -----------------|
  |                                   |
  |------- send login --------------->|
  |                                   |-- 解析 login 消息
  |                                   |-- 生成 playerId
  |<----- login success -------------|
  |                                   |
  |------- send chat ---------------->|
  |                                   |-- 广播给所有客户端
  |<----- chat message --------------|
  |                                   |
  |------- send match --------------->|
  |                                   |-- 延迟 1 秒
  |                                   |-- 收集在线玩家
  |<----- matched result ------------|
  |                                   |

服务端控制台输出

WebSocket 服务器启动在 ws://localhost:8080
[10:30:15] 客户端连接: a1b2c3d4,当前在线: 1
[10:30:15] a1b2c3d4 -> login
[10:30:15] 玩家登录: player123
[10:30:16] a1b2c3d4 -> chat
[10:30:16] Player_a1b2: Hello Server!
[10:30:17] a1b2c3d4 -> move
[10:30:18] a1b2c3d4 -> match
[10:30:18] 客户端断开: a1b2c3d4,当前在线: 0

注意事项

  1. 必须监听事件:使用前必须监听 OPEN、MESSAGE、CLOSE、ERROR 事件
  2. 连接后发送:确保连接成功(OPEN 事件)后再发送数据
  3. 字节序一致性:客户端和服务器必须使用相同的字节序
  4. 清理资源:场景销毁时调用 cleanSocket() 或 close()
  5. HTTPS 限制:在 HTTPS 页面中必须使用 wss 协议
  6. 字符串优化:传输字符串时设置 disableInput = true 减少二进制转换
  7. 缓冲区清理:接收完数据后及时调用 input.clear() 释放内存

相关文档

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐