使用?Phalcon7?的協程庫實現簡易的?websocket?服務

對應的客戶端源碼類似wscat:https://github.com/dreamsxin/cphalcon7/blob/master/examples/async/websocket-client.php

服務端源碼:

```php

<?php

//Phalcon\Debug::enable();

$loader = new Phalcon\Loader();

$loader->registerDirs(array(

__DIR__.DIRECTORY_SEPARATOR."websocket-plugins".DIRECTORY_SEPARATOR,

));

$loader->register();

class Pool

{

private $channel;

private $concurrency;

private $count = 0;

private $context;

public function __construct(int $concurrency = 1, int $capacity = 0)

{

$this->concurrency = max(1, $concurrency);

$this->channel = new \Phalcon\Async\Channel($capacity);

$this->context = \Phalcon\Async\Context::background();

}

public function close(?\Throwable $e = null): void

{

$this->count = \PHP_INT_MAX;

$this->channel->close($e);

}

public function submit(callable $work, $socket, ...$args): \Phalcon\Async\Awaitable

{

if ($this->count < $this->concurrency) {

$this->count++;

Websocket::info('Pool count '.$this->count);

\Phalcon\Async\Task::asyncWithContext($this->context, static function (iterable $it) {

try {

foreach ($it as list ($defer, $context, $work, $socket, $args)) {

try {

$defer->resolve($context->run($work, $socket, ...$args));

} catch (\Throwable $e) {

Websocket::err($e->getMessage());

$defer->fail($e);

} finally {

}

}

} catch (\Throwable $e) {

Websocket::err($e->getMessage());

} finally {

--$this->count;

}

}, $this->channel->getIterator());

}

$this->channel->send([

$defer = new \Phalcon\Async\Deferred(),

\Phalcon\Async\Context::current(),

$work,

$socket,

$args

]);

return $defer->awaitable();

}

}

class Websocket

{

static public $debug = false;

protected $port = null;

protected $host = null;

protected $server = null;

protected $callback = null;

protected static $opcodes = array(

'continuation' => 0,

'text' => 1,

'binary' ? => 2,

'close' => 8,

'ping' => 9,

'pong' => 10,

);

public function __construct($host, int $port, callable $callback = NULL, int $concurrency = 1, int $capacity = 0) {

$this->port = $port;

$this->host = $host;

$this->callback = $callback;

$this->pool = new Pool($concurrency, $capacity);

}

public function start()

{

$callback = $this->callback;

$ws = $this;

$worker = static function ($socket) use ($ws, $callback) {

// echo ('memory'.memory_get_usage().PHP_EOL);

//$socket->setOption(TcpSocket::NODELAY, false);

$socket->isHttp = false;

$socket->parser = new \Phalcon\Http\Parser();

$socket->isHandshake = false;

$socket->is_closing = false;

$socket->fragment_status = 0;

$socket->fragment_length = 0;

$socket->fragment_size = 4096;

$socket->read_length = 0;

$socket->huge_payload = '';

$socket->payload = '';

$socket->headers = NULL;

$socket->request_path = NULL;

try {

$buffer = '';

while (!$socket->is_closing && null !== ($chunk = $socket->read())) {

if ($socket->isHandshake === false) {

$buffer .= $chunk;

$pos = strpos($buffer, "\r\n\r\n");

if ($pos) {

$header = substr($buffer, 0, $pos+4);

$buffer = substr($buffer, $pos+4);

if ($ws->handShake($socket, $header)) {

}

}

} elseif ($socket->isHttp) {

$buffer = $chunk;

} else {

$buffer .= $chunk;

}

if ($socket->isHttp) {

$ret = $socket->parser->execute($buffer);

if (!$ret) {

throw new \Exception('HTTP parse failed');

}

if ($socket->parser->status() == \Phalcon\Http\Parser::STATUS_END) {

$body = \Phalcon\Arr::get($ret, 'BODY');

if ($callback && \is_callable($callback)) {

$callback($socket, $socket->headers, $socket->request_path, $body);

}

$socket->is_closing = true;

break;

}

} else if ($ws->process($socket, $buffer)) {

$buffer = substr($buffer, $socket->read_length);

if ($callback && \is_callable($callback)) {

$callback($socket, $socket->headers, $socket->request_path, $socket->payload);

}

$socket->fragment_status = 0;

$socket->fragment_length = 0;

$socket->read_length = 0;

$socket->huge_payload = '';

$socket->payload = '';

}

}

} catch (\Throwable $e) {

self::err($e->getMessage());

} finally {

$socket->close();

}

};

try {

$this->server = \Phalcon\Async\Network\TcpServer::listen($this->host, $this->port);

echo Phalcon\Cli\Color::info('start server listen:'.$this->host.':'.$this->port).PHP_EOL;

while (true) {

$socket = $this->server->accept();

if ($socket === false) {

continue;

}

// \Phalcon\Async\Task::async($worker, $socket);

$this->pool->submit($worker, $socket);

}

} catch (\Throwable $e) {

self::err($e->getMessage());

} finally {

if ($this->server) {

$this->server->close();

}

}

}

/**

* 請求握手

* @return boolean

*/

static public function handShake($socket, $header)

{

self::info('recv:'.$header);

$request = $socket->parser->execute($header, true);

if (!$request || !isset($request['HEADERS'])) {

throw new \Exception('Handshake failed, HEAD error');

}

$headers = $request['HEADERS'];

$socket->headers = $headers;

$socket->request_path = $request['QUERY_STRING'];

if (!isset($headers['Sec-WebSocket-Key'])) {

$socket->isHttp = true;

return true;

} else if ($request['REQUEST_METHOD'] != 'GET') {

throw new \Exception('Handshake failed, No GET in HEAD');

}

$socket->isHandshake = true;

$wsKey = trim($headers['Sec-WebSocket-Key']);

// 根據客戶端傳遞過來的 key 生成 accept key

$acceptKey = base64_encode(sha1($wsKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));

// 拼接回復字符串

$msg = "HTTP/1.1 101 Switching Protocols\r\n";

$msg .= "Upgrade: websocket\r\n";

$msg .= "Sec-WebSocket-Version: 13\r\n";

$msg .= "Connection: Upgrade\r\n";

$msg .= "Sec-WebSocket-Accept: " . $acceptKey . "\r\n\r\n";

$socket->write($msg);

return true;

}

static public function readFragment0($socket, $buffer) {

if (strlen($buffer) < 2) {

return false;

}

$data = substr($buffer, 0, 2);

$socket->read_length = 2;

// Is this the final fragment? // Bit 0 in byte 0

/// @todo Handle huge payloads with multiple fragments.

$socket->final = (boolean) (ord($data[0]) & 1 << 7);

// Should be unused, and must be false… // Bits 1, 2, & 3

$rsv1 = (boolean) (ord($data[0]) & 1 << 6);

$rsv2 = (boolean) (ord($data[0]) & 1 << 5);

$rsv3 = (boolean) (ord($data[0]) & 1 << 4);

// Parse opcode

$opcode_int = ord($data[0]) & 31; // Bits 4-7

$opcode_ints = array_flip(self::$opcodes);

if (!array_key_exists($opcode_int, $opcode_ints)) {

throw new \Exception('Bad opcode in websocket frame: '.$opcode_int);

}

$opcode = $opcode_ints[$opcode_int];

// record the opcode if we are not receiving a continutation fragment

if ($opcode !== 'continuation') {

$socket->last_opcode = $opcode;

}

// Masking?

$socket->mask = (boolean) (ord($data[1]) >> 7); // Bit 0 in byte 1

$socket->payload = '';

// Payload length

$socket->payload_length = (integer) ord($data[1]) & 127; // Bits 1-7 in byte 1

if ($socket->payload_length > 125) {

$socket->fragment_status = 1;

} else {

$socket->fragment_status = 2;

}

return true;

}

static public function readFragment1($socket, $buffer) {

if ($socket->payload_length === 126) {

if ($socket->fragment_length - $socket->read_length < 2) {

return false;

}

$data = substr($buffer, $socket->read_length, 2); // 126: Payload is a 16-bit unsigned int

$socket->read_length += 2;

} else {

if ($socket->fragment_length - $socket->read_length < 8) {

return false;

}

$data = substr($buffer, $socket->read_length, 8); // 127: Payload is a 64-bit unsigned int

$socket->read_length += 8;

}

$socket->payload_length = bindec(self::sprintB($data));

$socket->fragment_status = 2;

return true;

}

static public function readFragment2($socket, $buffer) {

// Get masking key.

if ($socket->mask) {

if ($socket->fragment_length - $socket->read_length < (4 + $socket->payload_length)) {

return false;

}

$masking_key = substr($buffer, $socket->read_length, 4);

$socket->read_length += 4;

} elseif ($socket->fragment_length - $socket->read_length < $socket->payload_length) {

return false;

}

// Get the actual payload, if any (might not be for e.g. close frames.

if ($socket->payload_length > 0) {

$data = substr($buffer, $socket->read_length, $socket->payload_length);

$socket->read_length += $socket->payload_length;

if ($socket->mask) {

// Unmask payload.

for ($i = 0; $i < $socket->payload_length; $i++) {

$socket->payload .= ($data[$i] ^ $masking_key[$i % 4]);

}

} else {

$socket->payload = $data;

}

}

$socket->fragment_status = 3;

return true;

}

static public function sendFragment($socket, $payload, $opcode = 'text', $masked = true) {

if (!in_array($opcode, array_keys(self::$opcodes))) {

throw new \Exception('Bad opcode '.$opcode.', try text or binary.');

}

// record the length of the payload

$payload_length = strlen($payload);

$fragment_cursor = 0;

// while we have data to send

while ($payload_length > $fragment_cursor) {

// get a fragment of the payload

$sub_payload = substr($payload, $fragment_cursor, $socket->fragment_size);

// advance the cursor

$fragment_cursor += $socket->fragment_size;

// is this the final fragment to send?

$final = $payload_length <= $fragment_cursor;

// send the fragment

self::send_frame($socket, $final, $sub_payload, $opcode, $masked);

// all fragments after the first will be marked a continuation

$opcode = 'continuation';

}

}

static public function send_frame($socket, $final, $payload, $opcode, $masked) {

// Binary string for header.

$frame_head_binstr = '';

// Write FIN, final fragment bit.

$frame_head_binstr .= (bool) $final ? '1' : '0';

// RSV 1, 2, & 3 false and unused.

$frame_head_binstr .= '000';

// Opcode rest of the byte.

$frame_head_binstr .= sprintf('%04b', self::$opcodes[$opcode]);

// Use masking?

$frame_head_binstr .= $masked ? '1' : '0';

// 7 bits of payload length...

$payload_length = strlen($payload);

if ($payload_length > 65535) {

$frame_head_binstr .= decbin(127);

$frame_head_binstr .= sprintf('%064b', $payload_length);

}

elseif ($payload_length > 125) {

$frame_head_binstr .= decbin(126);

$frame_head_binstr .= sprintf('%016b', $payload_length);

}

else {

$frame_head_binstr .= sprintf('%07b', $payload_length);

}

$frame = '';

// Write frame head to frame.

foreach (str_split($frame_head_binstr, 8) as $binstr) $frame .= chr(bindec($binstr));

// Handle masking

if ($masked) {

// generate a random mask:

$mask = '';

for ($i = 0; $i < 4; $i++) $mask .= chr(rand(0, 255));

$frame .= $mask;

}

// Append payload to frame:

for ($i = 0; $i < $payload_length; $i++) {

$frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];

}

$socket->write($frame);

}

/**

* Helper to convert a binary to a string of '0' and '1'.

*/

protected static function sprintB($string) {

$ret = '';

for ($i = 0; $i < strlen($string); $i++) {

$ret .= sprintf("%08b", ord($string[$i]));

}

return $ret;

}

/**

* 解析數據包

*

* @return string

*/

static public function process($socket, $buffer)

{

startfragment:

self::info('process fragment_status:'.$socket->fragment_status.' buffer length:'.strlen($buffer));

$socket->fragment_length = strlen($buffer);

switch ($socket->fragment_status) {

case 0:

// Just read the main fragment information first.

if (!self::readFragment0($socket, $buffer)) {

return false;

}

goto startfragment;

break;

case 1:

if (!self::readFragment1($socket, $buffer)) {

return false;

}

goto startfragment;

break;

case 2:

if (!self::readFragment2($socket, $buffer)) {

return false;

}

goto startfragment;

break;

case 3:

{

if ($socket->last_opcode === 'close') {

// Get the close status.

if ($socket->payload_length >= 2) {

$status_bin = $socket->payload[0] . $socket->payload[1];

$status = bindec(sprintf("%08b%08b", ord($socket->payload[0]), ord($socket->payload[1])));

$socket->close_status = $status;

$socket->payload = substr($socket->payload, 2);

self::sendFragment($socket, $status_bin . 'Close acknowledged: ' . $status, 'close', true); // Respond.

}

$socket->is_closing = true; // A close response, all done.

}

// if this is not the last fragment, then we need to save the payload

if (!$socket->final) {

$socket->huge_payload .= $socket->payload;

self::info('final:'.$socket->final.', payload:'.$socket->payload);

return false;

} else {

// sp we need to retreive the whole payload

$socket->huge_payload .= $socket->payload;

$socket->payload = $socket->huge_payload;

$socket->huge_payload = null;

self::info('final:'.$socket->final.', payload:'.$socket->payload);

return true;

}

break;

}

}

return false;

}

static public function info($message)

{

if (self::$debug) {

echo Phalcon\Cli\Color::info($message).PHP_EOL;

}

}

static public function err($message)

{

echo Phalcon\Cli\Color::error($message).PHP_EOL;

}

}

$opts = new \Phalcon\Cli\Options('Websocket CLI');

$opts->add([

? ? 'type' => \Phalcon\Cli\Options::TYPE_STRING,

? ? 'name' => 'server',

? ? 'shortName' => 's',

? ? 'required' => false, // 可選,需要用=號賦值

'help' => "address"

]);

$opts->add([

? ? 'type' => \Phalcon\Cli\Options::TYPE_INT,

? ? 'name' => 'port',

? ? 'shortName' => 'p',

? ? 'required' => false,

'help' => "port"

]);

$opts->add([

? ? 'type' => \Phalcon\Cli\Options::TYPE_BOOLEAN,

? ? 'name' => 'concurrency',

? ? 'shortName' => 'c',

? ? 'required' => false

]);

$opts->add([

? ? 'type' => \Phalcon\Cli\Options::TYPE_BOOLEAN,

? ? 'name' => 'capacity',

? ? 'shortName' => 'C',

? ? 'required' => false

]);

$opts->add([

? ? 'type' => \Phalcon\Cli\Options::TYPE_BOOLEAN,

? ? 'name' => 'debug',

? ? 'shortName' => 'v',

? ? 'required' => false,

'help' => "enable debug"

]);

$vals = $opts->parse();

if ($vals === false ) {

exit;

}

/**

* 客戶端測試

* sudo apt install node-ws

* wscat -c ws://localhost:10001

* curl -v http://localhost:10001/hello

* 運行 php websocket-server.php

*/

if (isset($vals['debug'])) {

Websocket::$debug = true;

}

$ws = new Websocket(\Phalcon\Arr::get($vals, 'server', '0.0.0.0'), \Phalcon\Arr::get($vals, 'port', 10001), function($socket, $headers, $path, $data) {

if ($path) {

$handlerName = 'Index';

$actionName = 'Index';

$params = NULL;

if (preg_match("#^/([a-zA-Z0-9_-]++)/?+$#", $path, $matches)) {

$handlerName = $matches[1];

} else if (preg_match("#^/([a-zA-Z0-9_-]++)/([a-zA-Z0-9\\._]++)(/.*+)?+$#", $path, $matches)) {

$handlerName = $matches[1];

$actionName = $matches[2];

$params = isset($matches[3]) ? $matches[3] : NULL;

}

$handlerName = Phalcon\Text::camelize($handlerName);

$handlerName .= 'Plugin';

$actionName .= 'Action';

if (class_exists($handlerName) && method_exists($handlerName, $actionName)) {

$data = call_user_func($handlerName.'::'.$actionName, $data);

}

}

if ($socket->isHttp) {

$sendchunk = \sprintf("HTTP/1.1 200 OK\r\nServer: webserver\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n%x\r\n%s\r\n0\r\n\r\n", \strlen($data), $data);

$socket->write($sendchunk);

} else {

Websocket::sendFragment($socket, $data);

}

}, \Phalcon\Arr::get($vals, 'concurrency', 500), \Phalcon\Arr::get($vals, 'capacity', 1));

$ws->start();

```

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容