From f70587cdcc67ca77214c82df4da16bb4b8e90539 Mon Sep 17 00:00:00 2001 From: inhere Date: Mon, 26 Mar 2018 23:51:48 +0800 Subject: [PATCH] init prject --- .editorconfig | 17 ++ .gitignore | 10 ++ LICENSE | 20 +++ README.md | 11 ++ composer.json | 32 ++++ phpunit.xml.dist | 24 +++ src/Helper.php | 187 ++++++++++++++++++++ src/WebSocketInterface.php | 110 ++++++++++++ src/WebSocketUtilTrait.php | 351 +++++++++++++++++++++++++++++++++++++ test/boot.php | 27 +++ 10 files changed, 789 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 src/Helper.php create mode 100644 src/WebSocketInterface.php create mode 100644 src/WebSocketUtilTrait.php create mode 100644 test/boot.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5919005 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +# 对所有文件生效 +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# 对后缀名为 md 的文件生效 +[*.md] +trim_trailing_whitespace = false + +[*.php] +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..86318a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea/ +.phpintel/ +!README.md +!.gitkeep +composer.lock +*.swp +*.log +*.pid +*.patch +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d839cdc --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2016 inhere + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3cdb049 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# websocket utils + +## install + +```bash +composer require mylib/websocket-utils +``` + +## license + +MIT diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..20992c2 --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "mylib/websocket-utils", + "type": "library", + "description": "some helper tool library of the php", + "keywords": ["library","tool","php"], + "homepage": "https://github.com/php-mylib/websocket-utils", + "license": "MIT", + "authors": [ + { + "name": "inhere", + "email": "in.798@qq.com", + "homepage": "http://www.yzone.net/" + } + ], + "require": { + "php": ">=7.0.0", + "mylib/arr-utils": "~1.0", + "mylib/obj-utils": "~1.0", + "mylib/str-utils": "~1.0", + "mylib/sys-utils": "~1.0", + "mylib/php-utils": "~1.0" + }, + "autoload": { + "psr-4": { + "MyLib\\WebSocket\\Client\\" : "src/" + } + }, + "suggest": { + "inhere/php-validate": "Very lightweight data validate tool", + "inhere/console": "a lightweight php console application library." + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..fb249a3 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + + + test + + + + + + src + + + diff --git a/src/Helper.php b/src/Helper.php new file mode 100644 index 0000000..cd43736 --- /dev/null +++ b/src/Helper.php @@ -0,0 +1,187 @@ + 65535) { + $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); + $frameHead[1] = ($masked === true) ? 255 : 127; + + for ($i = 0; $i < 8; $i++) { + $frameHead[$i + 2] = bindec($payloadLengthBin[$i]); + } + + // most significant bit MUST be 0 (close connection if frame too big) + if ($frameHead[2] > 127) { + // todo `$this->close()`; + return false; + } + } elseif ($payloadLength > 125) { + $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); + $frameHead[1] = ($masked === true) ? 254 : 126; + $frameHead[2] = bindec($payloadLengthBin[0]); + $frameHead[3] = bindec($payloadLengthBin[1]); + } else { + $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; + } + + // convert frame-head to string: + foreach ($frameHead as $i => $v) { + $frameHead[$i] = \chr($frameHead[$i]); + } + + // generate a random mask: + $mask = array(); + if ($masked === true) { + for ($i = 0; $i < 4; $i++) { + $mask[$i] = \chr(random_int(0, 255)); + } + + $frameHead = array_merge($frameHead, $mask); + } + + $frame = implode('', $frameHead); + + // append payload to frame: + for ($i = 0; $i < $payloadLength; $i++) { + $frame .= $masked ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; + } + + return $frame; + } + + /** + * @param string $data + * @return string + * @throws \InvalidArgumentException + */ + public static function hybi10Decode(string $data): string + { + if (!$data) { + throw new \InvalidArgumentException('data is empty'); + } + + $bytes = $data; + $secondByte = sprintf('%08b', \ord($bytes[1])); + $masked = '1' === $secondByte[0]; + $dataLength = ($masked === true) ? \ord($bytes[1]) & 127 : \ord($bytes[1]); + + //服务器不会设置mask + if ($dataLength === 126) { + $decodedData = \substr($bytes, 4); + } elseif ($dataLength === 127) { + $decodedData = \substr($bytes, 10); + } else { + $decodedData = \substr($bytes, 2); + } + + return $decodedData; + } + +} diff --git a/src/WebSocketInterface.php b/src/WebSocketInterface.php new file mode 100644 index 0000000..b581a12 --- /dev/null +++ b/src/WebSocketInterface.php @@ -0,0 +1,110 @@ + self::OPCODE_CONT, // 0 + 'text' => self::OPCODE_TEXT, // 1 + 'binary' => self::OPCODE_BINARY, // 2 + 'close' => self::OPCODE_CLOSE, // 8 + 'ping' => self::OPCODE_PING, // 9 + 'pong' => self::OPCODE_PONG, // 10 + ]; + + /** + * the driver name + * @var LoggerInterface + */ + protected $logger; + + /** + * @var string + */ + protected $host; + + /** + * @var int + */ + protected $port; + + /** + * @var Output + */ + protected $cliOut; + + /** + * @var Input + */ + protected $cliIn; + + /** + * @var bool + */ + private $initialized; + + /** + * @var array + */ + protected $config = [ + // server address HOST:PORT + 'server' => '', + + // enable ssl + 'enable_ssl' => false, + + // 数据块大小 byte 发送数据时将会按这个大小拆分发送 + 'fragment_size' => 1024, + + // 'buffer_size' => 8192, // 8kb + + // 设置写(发送)缓冲区 最大2m @see `StreamsServer::setBufferSize()` + 'write_buffer_size' => 2097152, + + // 设置读(接收)缓冲区 最大2m + 'read_buffer_size' => 2097152, + + // while 循环时间间隔 毫秒(ms) millisecond. 1s = 1000ms = 1000 000us + 'sleep_time' => 500, + + // 连接超时时间 s + 'timeout' => 2.2, + + // 最大数据接收长度 1024 / 2048 byte + 'max_data_len' => 2048, + ]; + + /** + * @return array + */ + protected function appendDefaultConfig(): array + { + return [ + // ... + ]; + } + + /** + * WSAbstracter constructor. + * @param array $config + */ + protected function initConfig(array $config) + { + $this->cliIn = new Input(); + $this->cliOut = new Output(); + + if ($append = $this->appendDefaultConfig()) { + $this->config = array_merge($this->config, $append); + } + + $this->setConfig($config); + + $this->init(); + + $this->initialized = true; + } + + /** + * init + */ + protected function init() + { + $this->handleCommandAndConfig(); + } + + /** + * handleCommandAndConfig + */ + protected function handleCommandAndConfig() + {} + + /** + * Logs data to disk or stdout + * @param string $msg + * @param int|string $level + * @param array $data + */ + public function log(string $msg, $level = LogLevel::INFO, array $data = []) + { + if ($this->isDebug() && ($info = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1))) { + $msg = sprintf(' [%s:%d] ', $info['class'] ?? 'UNKNOWN', $info['line'] ?? -1) . $msg; + } + + $this->logger->log($level, $msg, $data); + } + + /** + * Logs data to stdout + * @param string $text + * @param bool $nl + * @param bool|int $quit + */ + protected function stdout($text, $nl = true, $quit = false) + { + // CliHelper::stdout($text, $nl, $quit); + $this->getCliOut()->write($text, $nl, $quit); + } + + /** + * Logs data to stderr + * @param string $text + * @param bool $nl + * @param bool|int $quit + */ + protected function stderr($text, $nl = true, $quit = -200) + { + Cli::stderr($text, $nl, $quit); + } + + /** + * @return array + */ + public static function getOpCodes(): array + { + return self::$opCodes; + } + + /** + * {@inheritDoc} + * @throws \InvalidArgumentException + */ + public function setConfig(array $config) + { + if ($this->initialized) { + throw new \InvalidArgumentException('Has been initialize completed. don\'t allow change config.'); + } + + $this->tSetConfig($config); + } + + /** + * @param $name + * @param null $default + * @return mixed + */ + public function get($name, $default = null) + { + return $this->getValue($name, $default); + } + + /** + * @return LoggerInterface + */ + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + /** + * @param LoggerInterface $logger + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * @return string + */ + public function getDriverName(): string + { + return static::DRIVER_NAME; + } + + /** + * @return string + */ + public function getHost(): string + { + if (!$this->host) { + $this->host = self::DEFAULT_HOST; + } + + return $this->host; + } + + /** + * @return int + */ + public function getPort(): int + { + if (!$this->port || $this->port <= 0) { + $this->port = self::DEFAULT_PORT; + } + + return $this->port; + } + + /** + * @return Output + */ + public function getCliOut(): Output + { + return $this->cliOut; + } + + /** + * @param Output $output + */ + public function setCliOut(Output $output) + { + $this->cliOut = $output; + } + + /** + * @return Input + */ + public function getCliIn(): Input + { + return $this->cliIn; + } + + /** + * @param Input $cliIn + */ + public function setCliIn(Input $cliIn) + { + $this->cliIn = $cliIn; + } + + /** + * Generate a random string for WebSocket key.(for client) + * @return string Random string + * @throws \Exception + */ + public function genKey(): string + { + $key = ''; + $chars = self::TOKEN_CHARS; + $chars_length = \strlen($chars); + + for ($i = 0; $i < 16; $i++) { + $key .= $chars[\random_int(0, $chars_length - 1)]; //mt_rand + } + + return \base64_encode($key); + } + + /** + * Generate WebSocket sign.(for server) + * @param string $key + * @return string + */ + public function genSign(string $key): string + { + return \base64_encode(sha1(trim($key) . self::SIGN_KEY, true)); + } + + /** + * @return bool + */ + public function isDebug(): bool + { + return (bool)$this->getValue('debug', false); + } + + /** + * @param mixed $messages + * @param bool $nl + * @param bool|int $exit + */ + public function print($messages, $nl = true, $exit = false) + { + $this->cliOut->write($messages, $nl, $exit); + } + + /** + * @param int $code + */ + protected function quit($code = 0) + { + exit((int)$code); + } +} diff --git a/test/boot.php b/test/boot.php new file mode 100644 index 0000000..3bf5204 --- /dev/null +++ b/test/boot.php @@ -0,0 +1,27 @@ +