namespace Ice\Cli\Websocket;
use Ice\Exception;
/**
* Websocket client.
*
* @package Ice/Cli
* @category Component
* @author Ice Team
* @copyright (c) 2014-2025 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;
}
}