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:关闭连接,移除监听,重置状态 |
属性
| 属性 | 类型 | 只读 | 说明 |
|---|---|---|---|
connected |
boolean |
✅ | 当前是否已连接 |
input |
Laya.Byte |
✅ | 从服务器接收的数据缓存 |
output |
Laya.Byte |
✅ | 需要发送到服务器的数据缓冲区 |
endian |
string |
❌ | 字节序(取值为 Laya.Byte.BIG_ENDIAN 或 Laya.Byte.LITTLE_ENDIAN) |
disableInput |
boolean |
❌ | 是否禁用输入缓存(字符串数据建议设为 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 连接(推荐)
最简洁的连接方式:
typescript
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. 通过主机和端口连接
typescript
// 普通 WebSocket 连接
let socket = new Laya.Socket();
socket.connect("localhost", 8080);
// 安全 WebSocket 连接 (wss)
socket.connect("localhost", 8080, true);
3. 发送字符串消息
typescript
// 连接成功后发送
private onOpen(): void {
// 直接发送字符串
this.socket.send("Hello Server!");
}
4. 发送二进制数据
typescript
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. 接收并解析数据
typescript
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 通信
typescript
@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: 二进制协议通信
typescript
// 协议定义
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: 心跳保活机制
typescript
@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: 断线重连与消息队列
typescript
@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: 房间匹配与游戏同步
typescript
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. 协议封包与解包
typescript
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. 消息处理器注册
typescript
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 安全连接
typescript
// 使用 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. 子协议协商
typescript
// 指定子协议
let protocols = ["chat", "superchat"];
let socket = new Laya.Socket();
socket.connectByUrl("ws://localhost:8080", protocols);
// 或者使用 connect
socket.connect("localhost", 8080, false, protocols);
5. 处理大文件传输
typescript
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. 错误处理与重连
typescript
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. 统一的连接管理
typescript
@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. 消息序列化优化
typescript
// 字符串数据禁用二进制转换
this.socket.disableInput = true;
// 二进制数据设置正确的字节序
this.socket.endian = Laya.Byte.LITTLE_ENDIAN;
4. 资源清理
typescript
@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. 连接状态检测
typescript
// 发送前检查连接
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. 心跳与超时处理
typescript
// 定期发送心跳保持连接
Laya.timer.loop(30000, this, () => {
if (this.socket.connected) {
this.socket.send(JSON.stringify({ type: "heartbeat" }));
}
});
测试服务器
完整的 Socket 通信需要服务端和客户端配合。本节提供:
- 服务端代码 - Node.js WebSocket 服务器
- 客户端代码 - Laya.Socket 连接测试
服务端代码
创建 socket-test-server.js:
javascript
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);
}
});
}
运行服务端:
bash
npm install ws
node socket-test-server.js
客户端代码 (Laya.Socket)
typescript
@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
注意事项
- 必须监听事件:使用前必须监听 OPEN、MESSAGE、CLOSE、ERROR 事件
- 连接后发送:确保连接成功(OPEN 事件)后再发送数据
- 字节序一致性:客户端和服务器必须使用相同的字节序
- 清理资源 :场景销毁时调用
cleanSocket()或close() - HTTPS 限制:在 HTTPS 页面中必须使用 wss 协议
- 字符串优化 :传输字符串时设置
disableInput = true减少二进制转换 - 缓冲区清理 :接收完数据后及时调用
input.clear()释放内存