ice framework documentation ice doc v1.10.1
    
namespace Ice\Cli\Websocket;

use Ice\Exception;

/**
 * Websocket client.
 *
 * @package     Ice/Cli
 * @category    Component
 * @author      Ice Team
 * @copyright   (c) 2014-2023 Ice Team
 * @license     http://iceframework.org/license
 */
class Client extends Websocket
{
    protected socket = null;
    protected message = null;
    protected tick = null;

    /**
     * Connect to server.
     *
     * @param string address Address to bind to, defaults to `ws://127.0.0.1:8080`
     * @param array headers Optional array of headers to pass when connecting
     * @return self
     */
    public function connect(string address = "ws://127.0.0.1:8080", var headers = [])
    {
        var addr, key, name, value, res, data, matches;

        let addr = parse_url(address);

        if addr === false || !isset addr["host"] || !isset addr["port"] {
            throw new Exception("Invalid address");
        }

        let this->socket = fsockopen(
            (isset addr["scheme"] && in_array(addr["scheme"], ["ssl", "tls", "wss"]) ? "tls://" : "") . addr["host"],
            addr["port"]
        );

        if this->socket === false {
            throw new Exception("Could not connect");
        }

        let key = this->generateKey(),
            headers = array_merge(
                this->normalizeHeaders([
                    "Host": addr["host"] . ":" . addr["port"],
                    "Connection": "Upgrade",
                    "Upgrade": "websocket",
                    "Sec-Websocket-Key": key,
                    "Sec-Websocket-Version": "13"
                ]),
                this->normalizeHeaders(headers)
            );

        let key = headers["Sec-Websocket-Key"];

        for name, value in headers {
            let headers[name] = name . ": " . value;
        }

        if isset addr["path"] && strlen(addr["path"]) {
            let res = addr["path"] . (empty addr["query"] ? "" : "?" . addr["query"]);
        } else {
            let res = "/";
        }

        array_unshift(headers, "GET " . res . " HTTP/1.1");
        this->sendClear(this->socket, implode("\r\n", headers)."\r\n");

        let data = this->receiveClear(this->socket);

        if !preg_match("(Sec-Websocket-Accept:\s*(.*)$)mUi", data, matches) {
            throw new Exception("Bad response");
        }

        if trim(matches[1]) !== base64_encode(pack("H*", sha1(key . self::magic))) {
            throw new Exception(sprintf("Bad key `%s` `%s`", trim(matches[1]), base64_encode(pack("H*", sha1(key . self::magic)))));
        }

        return this;
    }

    /**
     * Generate key.
     *
     * @return string
     */
    protected function generateKey() -> string
    {
        string chars, key;
        var length, index;
        int i = 0;

        let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"$&/()=[]{}0123456789",
            key = "",
            length = strlen(chars) - 1;

        while i < 16 {
            let index = mt_rand(0, length),
                key .= chars[(int) index],
                i++;
        }

        return base64_encode(key);
    }

    /**
     * Normalize header.
     *
     * @param array headers headers to normalize
     * @return array
     */
    protected function normalizeHeaders(array headers) -> array
    {
        var cleaned, name, value;

        let cleaned = [];

        for name, value in headers {
            if strncmp(name, "HTTP_", 5) === 0 {
                let name = substr(name, 5);
            }

            if name !== false {
                let name = ucwords(str_replace(
                    ["_", "-", " "],
                    [" ", " ", "-"],
                    strtolower(name)
                 ),  "-"),
                    cleaned[name] = value;
            }
        }

        return cleaned;
    }

    /**
     * Send a message to the server.
     *
     * @param string data The data to send
     * @param string opcode The data opcode, defaults to `text`
     * @return boolean Was the send successful
     */
    public function send(string data, string opcode = "text") -> boolean
    {
        return parent::sendData(this->socket, data, opcode, true);
    }

    /**
     * Start listening.
     *
     * @return void
     */
    public function run() -> void
    {
        var changed, write, except, socket, message;

        while 1 {
            if isset this->tick {
                if call_user_func(this->tick, this) === false {
                    break;
                }
            }

            let changed = [this->socket],
                write = [],
                except = [];

            if stream_select(changed, null, null, (isset this->tick ? 0 : null)) > 0 {
                for socket in changed {
                    let message = this->receive(socket);

                    if message !== false && isset this->message {
                        call_user_func(this->message, message, this);
                    }
                }
            }
            usleep(this->getParam("sleep", 5000));
        }
    }

    /**
     * Set a callback to execute when a message arrives.
     * The callable will receive the message string and the server instance.
     *
     * @param callable callback The callback
     * @return self
     */
    public function onMessage(callable callback)
    {
        let this->message = callback;

        return this;
    }

    /**
     * Set a callback to execute every few milliseconds.
     * The callable will receive the server instance. If it returns boolean `false` the client will stop listening.
     *
     * @param callable callback The callback
     * @return self
     */
    public function onTick(callable callback)
    {
        let this->tick = callback;

        return this;
    }
}