猫史档案馆


SocketWrapper.js - 模拟套接字通信

用户:NomenNomen查看:0 回复:3 评论:0 创建时间:2023-12-28T09:20:08


这里是没有开场白的Nomen,如果你用不习惯RemoteWrapper这样的形式的通信,可以尝试一下SocketWrapper

同时在这次与RemoteWrapper的更新中增加了虚拟端口这一概念,防止多个工具之间通信冲突,有了端口之后,我们就可以同时使用多个工具啦

传送门前往SocketWrapper1.1.0:

shequ.codemao.cn/community/680102

目前这个工具还较为简陋,模拟了部分WebSocket的特性与方法,这是一个简单的例子来入门


// 注:SocketWrapper系列默认通信虚拟端口为6100

// 一个简单的通信示例
// SERVER index.js
let ssw = new SocketServerWrapper(); // 初始化
ssw.on("connect", (client /* 当连接成功后,会实例化一个Socket */) => {
    console.log(`[server] 连接成功,实例id: ${client.id}`); // 当实例连接成功时打印消息
    client.listen("message", (d) => console.log(`[server] 收到来自客户端发送的消息: ${d}`));
    client.listen("disconnect", (d) => { console.log(`[server] 客户端断开了连接,理由是: ${d.reason}`) });
    client.send("你好客户端!");
    client.ping(() => console.log(`[server] pong!`)); // 尝试ping一下,当收到pong之后会执行回调
});


// CLIENT clientIndex.js
let scw = new SocketClientWrapper(); // 初始化
scw.onmessage = (d) => console.log(`[client] 收到来自服务端的消息: ${d}`);
scw.onerror = console.error; // 用于输出报错信息
scw.onopen = () => {
    scw.send("hello!"); // 发送消息
    scw.ping(() => { // 当收到pong之后用理由"1145"关闭会话
        console.log(`[client] pong!`);
        scw.close(1145);
    })
};
scw.onclose = (code)=>{
    console.log(`[client] 服务器断开了连接,理由是: ${code}`);
}

Socket.server.js



class SocketError extends Error {
    constructor(message, socket) {
        super(message);
        this.name = "SocketError";
        this.socket = socket;
    }
}
/**
 * @typedef SocketEventToken
 * @property {Function} cancel 调用后取消事件
 */
class Socket {
    static messageType = {
        DATA: "data",
        PING: "ping",
        PONG: "pong",
        CONNECT: "connect",
        CLOSE: "close"
    }
    static states = {
        CONNECTING: 0,
        OPEN: 1,
        CLOSED: 2
    }
    /**
     * 请注意,一般来讲你不应该手动实例化这个类,该类用于与客户端进行通信,需要进行一些配置,SocketServerWrapper会自动操作
     * @constructor
     * @param {GameEntity} entity 监听的玩家实例
     * @param {number} id 会话id
     * @param {number} port 端口号
     */
    constructor(entity, id, port) {
        this.target = entity;
        this.state = Socket.states.CONNECTING;
        this.listener = [];
        this.id = id;
        this.port = port;
        this.events = {
            "message": [],
            "error": [],
            "connect": [],
            "disconnect": [],
            "ping": [],
            "_pong": []
        };
        this._start();
    }
    /**
     * 
     * @param {"message"|"error"|"connect"|"disconnect"|"ping"} type 监听类型
     * @param {Function} f 当事件触发时运行,message事件会收到数据,其他事件没有任何参数传入
     * @returns {Object} 取消令牌
     * @returns {SocketEventToken} 事件取消
     */
    listen(type, f) {
        if (Object.keys(this.events).includes(type)) {
            this.events[type].push(f);
            return {
                cancel: () => this.events[type] = this.events[type].filter(v => v !== f)
            }
        }
    }
    removeListener(type) {
        if (this.events[type] && !type.startsWith("_")) this.events[type] = [];
    }
    /**
     * 发送一条消息
     * @param {JSONValue} message 发送的数据
     * @param {Object} [c] 额外配置 
     */
    send(message, c) {
        try {
            if (this.state === Socket.states.OPEN) remoteChannel.sendClientEvent(this.target, this.wrap({
                data: message,
                ...c
            }));
            else throw new SocketError("套接字实例非连接状态", this);
        } catch (err) {
            this._emit("error", err);
        }
    }
    /**
     * 断开连接
     * @param {number} code 根据协议,你需要提供一个连接断开的理由,默认为1005,即CLOSE_NO_STATUS
     */
    disconnect(code = 1005) {
        this.send({reason: code}, {type: "disconnect"})
        this._emit(this._parse(args).type, this._parse(args).data?.reason);
        this._stop();
    }
    wrap({ id = this.id, data, type = Socket.messageType.DATA, time = Date.now() }) {
        return {
            id,
            data,
            type,
            time
        }
    }
    /**
     * 尝试发送一条ping消息,如果客户端处于连接状态并且符合连接协议,则会返回pong消息,可以通过观察是否超时来判定
     * @example ping((time)=>console.log(`客户端依旧存活,当前时间${new Date(time).toString()}`));
     * @param {Function} f 当收到pong之后触发
     * @returns {Promise<void>} 当收到pong之后承诺被解决
     */
    async ping(f) {
        if (this.state !== Socket.states.OPEN) throw new Error("套接字实例非连接状态");
        return new Promise(resolve => {
            remoteChannel.sendClientEvent(this.target, this.wrap({
                type: Socket.messageType.PING,
            }));
            let r = this.listen("_pong", () => {
                f(Date.now());
                r.cancel();
                resolve(Date.now());
            })
        })
    }
    _emit(type, ...args) {
        if (this.events[type]) {
            this.events[type].forEach(v => v(...args));
        }
    }
    _parse(d) {
        if (typeof d === "object" && d !== null) return d;
        else return { data: d };
    }
    _start() {
        if (this.state !== Socket.states.CONNECTING) return;
        this.state = Socket.states.OPEN;
        this._emit("connect");
        this.listener.push(remoteChannel.onServerEvent(({ args }) => {
            if (this._parse(args).port === this.port && this._parse(args).id === this.id) {
                if (this._parse(args).type === Socket.messageType.PING) {
                    this.send(undefined, {type: Socket.messageType.PONG});
                    this._emit("ping");
                } else if (this._parse(args).type === Socket.messageType.CLOSE) {
                    this._emit(this._parse(args).type, this._parse(args).data?.reason);
                    this._stop();
                } else if (this._parse(args).type === Socket.messageType.DATA) {
                    this._emit("message", this._parse(args).data);
                }else {
                    this._emit(this._parse(args).type, this._parse(args).data);
                }
            }
        }), world.onPlayerLeave(({ entity }) => {
            if (entity === this.target) {
                this._emit("disconnect");
                this._stop();
            }
        }));
    }
    _stop() {
        if (this.state === Socket.states.OPEN) {
            this.state = Socket.states.CLOSED;
            this.listener.forEach(v => v.cancel());
            Object.keys(this.events).forEach(k => this.removeListener(k));
        }
    }
}

class SocketServerWrapper {
    static get defaultConfig() {
        return {
            port: 6100,
        }
    }
    /**
     * @param {Object} [config] 配置
     * @param {number} [config.port=6100] 端口号,默认为6100
     */
    constructor(config = {}) {
        this.events = {};
        this.clients = [];
        this.listeners = [];
        this._assign(config);
        this._start();
    }
    /**
     * @callback SocketServerEvent
     * @param {Socket} client Socket连接实例
     * @param {JSONValue} [data] 本次消息的数据
     */
    /**
     * 
     * @param {"connect"|"message"|"disconnect"} type 监听类型
     * @param {SocketServerEvent} f 回调函数,参数为client,data
     * @param {number} [id] 会话id
     * @returns {SocketEventToken} 事件取消
     */
    on(type, f, id) {
        this._registerEvent(type);
        if (typeof f === "function") {
            this.events[type].push({ handler: f, id });
            return {
                cancel: () => this.events[type] = this.events[type].filter(v => v.handler !== f)
            }
        }
    }
    _start() {
        this.listeners.push(remoteChannel.onServerEvent(({ entity, args }) => {
            let a = this._toData(args);
            if (a.port !== this.port) return;
            if (a.type === "connect") {
                let client = new Socket(entity, Date.now(), this.port);
                client.listen("disconnect", () => this._emit("disconnect", a.id, client))
                client.send({ id: client.id }, { type: Socket.messageType.CONNECT });
                this._emit("connect", a.id, client);
            } else if (a.type === "message" && this.clients.some(c => c.id === a.id && c.target === entity)) {
                this._emit("message", a.id, this.clients.filter(c => c.id === a.id && c.target === entity)[0], a.data);
            }
        }))
    }
    _toData(data) {
        if (typeof data === "object" && data !== null) return data
        else return { data };
    }
    _assign(c) {
        let o = {}, dc = this.constructor.defaultConfig;
        Object.keys(dc).forEach(v => {
            if (c[v]) o[v] = c[v];
            else o[v] = dc[v];
        });
        Object.assign(this, o);
    }
    _emit(type, id, ...args) {
        if (Array.isArray(this.events[type])) {
            this.events[type].filter(v => !id || v.id === id).forEach(f => {
                f.handler(...args);
            })
        }
    }
    _registerEvent(type) {
        if (!this.events[type]) this.events[type] = [];
    }
}

Socket.client.js




function SocketClientWrapper({ port } = {}){
    if (!new.target) throw "SocketClientWrapper must be called with new";
    this.port = port || 6100;
    this.state = 0;
    this.listeners = [];
    this.id = null;
    Object.assign(this, {
        CONNECTING: 0,
        OPEN: 1,
        CLOSED: 2,
        onopen: ()=>{},
        onmessage: ()=>{},
        onerror: ()=>{},
        onclose: ()=>{},
        onpong: ()=>{}
    });
    let wrap = (message, type) => {
        return {
            port: this.port,
            type,
            id: this.id,
            data: message,
        }
    }
    function exec(f, ...args){
        if(typeof f==="function") return f(...args);
        else return null;
    }
    let error = (e)=>{
        exec(this.onerror, e);
        return new Error(e);
    }
    this.ping = function (f) {
        this.send(undefined, "ping");
        this.onpong = f;
    }
    this.send = function (message, type = "data") {
        if (this.state !== this.OPEN) {
            throw error("套接字实例非连接状态");
        }
        if (!this.id) {
            throw error("无法正确获取会话id,请尝试重新实例化!");
        }
        remoteChannel.sendServerEvent(wrap(message, type));
    }
    this.close = function (code = 1005) {
        this.send({reason: code}, "disconnect");
        this.state = this.CLOSED;
        exec(this.onclose, code);
        this.listeners.forEach(v => remoteChannel.events.remove("client", v));
    }
    let _l = (v) => {
        let value = (typeof v === "object" && v !== null) ? v : { data: v };
        if (this.state === this.CONNECTING) {
            if (value.type === "connect") {
                this.state = this.OPEN;
                this.id = value.data.id;
                exec(this.onopen, this);
            }
        } else if (this.state === this.OPEN) {
            if (value.type === "ping") {
                this.send(undefined, "_pong");
            } else if (value.type === "data") {
                exec(this.onmessage, value.data);
            } else if (value.type === "disconnect") {
                this.state = this.CLOSED;
                this.listeners.forEach(v => remoteChannel.events.remove("client", v));
                exec(this.onclose, value.data.reason);
            } else if (value.type === "pong"){
                exec(this.onpong);
            }
        }
    }
    this.listeners.push(_l);
    remoteChannel.events.on("client", _l);
    remoteChannel.sendServerEvent({type:"connect", port: this.port});
}

---

好消息,七莓大佬编写的UiWrapper收录进了Box3-Tools仓库中,该库能够便捷的管理GUI

github.com/helloyork/Box3-Tools/tree/main/src/GUI/UiWrapper/1.1.0


回复

上一页1 页 / 共 1下一页
神岛吉吉喵神岛吉吉喵

imgsrc="https://static.codemao.cn/emoji/codemao/%E7%BC%96%E7%A8%8B%E7%8C%AB_%E7%82%B9%E8%B5%9E.gif"alt="emotion_编程猫_点赞"

点赞0


评论


一个STUB用户_7236882一个STUB用户_7236882

点赞0


评论


一个STUB用户_7236882一个STUB用户_7236882

点赞0


评论