Workerman官方教程學習筆記

視頻教程
文檔手冊
教程基於 workerman 3.3

基礎教程

Worker類的使用

WorkerMan中有兩個重要的類Worker與Connection。
worker 對象實際上是一個容器,它可以以特定的協議去監聽某個端口。當客戶端連接到這個容器監聽端口之後,會在這個 worker 容器內部產生一個 connection 對象。
在 WorkerMan 中通過操作這個 connection 對象來完成對客戶端發送數據以及接收客戶端數據的操作。

<?php

use Workerman\Worker;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\Request;

require_once __DIR__ . '/vendor/autoload.php';

$worker = new Worker('http://0.0.0.0:8686');

$worker->onWorkerStart = function ($worker) {
};
$worker->onConnect = function ($connection) {
    echo "connection success" . PHP_EOL;
};
$worker->onMessage = function (TcpConnection $connection, Request $request) {
    $connection->send("hello");
};
$worker->onClose = function ($connection) {
    echo "connection close" . PHP_EOL;
};
$worker->onWorkerStop = function ($worker) {
};

// 運行worker
Worker::runAll();

image.png

<?php

use Workerman\Worker;
use Workerman\Connection\TcpConnection;

require_once __DIR__ . '/vendor/autoload.php';

$worker = new Worker('websocket://0.0.0.0:8686');

$worker->onMessage = function (TcpConnection $connection, $data) {
    $connection->send("hello");
};

// 運行worker
Worker::runAll();

image.png

<?php

use Workerman\Worker;
use Workerman\Connection\TcpConnection;

require_once __DIR__ . '/vendor/autoload.php';

$worker = new Worker('tcp://0.0.0.0:80');

$worker->onMessage = function (TcpConnection $connection, $data) {
    $connection->send("hello world");
};

// 運行worker
Worker::runAll();

image.png

<?php

use Workerman\Worker;
use Workerman\Connection\UdpConnection;

require_once __DIR__ . '/vendor/autoload.php';

$worker = new Worker('udp://0.0.0.0:80');

$worker->onMessage = function (UdpConnection $connection, $data) {
    $connection->send("hello world");
};

// 運行worker
Worker::runAll();

image.png

Connection類的使用

TcpConnection類和AsyncTcpConnection類。AsyncTcpConnection 類是 TcpConnection 類的子類。
當我們在 Workerman 中需要訪問外部的某個服務的時候,通過 AsyncTcpConnection 類異步的發起一個 tcp 連接,去連接遠程的服務端,異步通訊。

<?php

use Workerman\Connection\ConnectionInterface;
use Workerman\Worker;

require_once __DIR__ . '/vendor/autoload.php';

$worker = new Worker('tcp://0.0.0.0:8686');

$worker->onConnect = function (ConnectionInterface $connection) {
    echo "connection success" . PHP_EOL;
    //類似於IP白名單
    if ($connection->getRemoteIp() !== '127.0.0.1') {
        $connection->close('bad ip');
    }
};
$worker->onMessage = function (ConnectionInterface $connection, $data) {
    $connection->send("hello");
};
$worker->onClose = function ($connection) {
    echo "connection close" . PHP_EOL;
};

// 運行worker
Worker::runAll();
<?php

use Workerman\Connection\AsyncTcpConnection;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;

require_once __DIR__ . '/vendor/autoload.php';

$worker = new Worker('http://0.0.0.0:8686');

$worker->onConnect = function (TcpConnection $connection) {
    // 作爲客戶端去鏈接其他服務端(代理)
    // 發起異步的 TCP 連接 通過實現 onConnect、onMessage、onClose 實現業務邏輯
    $connectionBaidu = new AsyncTcpConnection('ssl://www.baidu.com:443');
  	// 當百度返回數據後,轉發給瀏覽器客戶端
    $connectionBaidu->onMessage = function ($connectionBaidu, $data) use ($connection) {
        $connection->send($data);
    };
  	// 瀏覽器客戶端發來數據後轉發給百度
    $connection->onMessage = function ($connection, $data) use ($connectionBaidu) {
        $connectionBaidu->send($data);
    };
  	// 執行連接
    $connectionBaidu->connect();
};

// 運行worker
Worker::runAll();

Timer類的使用

<?php

require_once './vendor/autoload.php';

use Workerman\Worker;
use Workerman\Lib\Timer;

$worker = new Worker('websocket://0.0.0.0:8686');
$worker->onConnect = function ($connection) {
    // 爲每個客戶端創建10s的賬戶認證
    Timer::add(10, function () use ($connection) {
        if (!isset($connection->name)) {
            $connection->close('auth timeout and close');
        }
    }, null, false);
};

$worker->onMessage = function ($connection, $data) {
    if (!isset($connection->name)) {
        $data = json_decode($data, true);
        if (!isset($data['name']) || !isset($data['password'])) {
            $connection->close('auth fail and close');
            return;
        }
        //假設此處進行數據庫驗證
        $connection->name = $data['name'];
        broadcast($connection->name . " login");
        return;
    }
    broadcast($connection->name . " said:" . $data);
};

function broadcast($msg)
{
    global $worker;
    foreach ($worker->connections as $connection) {
        if (!isset($connection->name)) {
            continue;
        }
        $connection->send($msg);
    }
}

Worker::runAll();

image.png

WebServer的使用

Workerman 4.x 版本去掉了 WebServer,推薦使用 webman
image.png
image.png

原理解析

Stream 函數

Worker類

<?php

/**
 * 單進程IO複用select
 * 同時處理多個連接
 * ab -n100000 -c100 -k http://127.0.0.1:1215/
 */
class Worker
{
    // 連接事件回調
    public $onConnect = null;
    // 消息事件回調
    public $onMessage = null;
    // 連接關閉事件回調
    public $onClose = null;
    // 監聽端口的socket
    protected $socket = null;
    // 所有socket,包括客戶端socket和監聽端口socket
    protected $allSockets = [];

    // 構造函數
    public function __construct($address)
    {
        // 創建監聽socket
        $this->socket = stream_socket_server($address, $errno, $errstr);
        echo "listen $address\r\n";
        // 設置爲非阻塞
        // 阻塞:當讀取一個socket時,如果對方沒有發來任何數據,這個讀取操作會一直卡着,直到這個socket超時
        // 非阻塞:當讀取一個socket時,如果對方沒有發來任何數據,讀取操作會立刻返回,返回空數據
        // 在workerman中不想因爲讀取某個客戶端的連接,而導致整個workerman卡住,所以設置非堵塞
        stream_set_blocking($this->socket, 0);
        // 將監聽socket放入allSockets
        $this->allSockets[(int)$this->socket] = $this->socket;
    }

    // 運行
    public function run()
    {
        while (1) {
            // 這裏不監聽socket可寫事件和外帶數據可讀事件
            $write = $except = null;
            // 監聽所有socket可讀事件,包括客戶端socket和監聽端口的socket
            $read = $this->allSockets;
            // stream_select是個IO複用函數,可以監聽一個socket集合的可讀、可寫。
            // 整個程序阻塞在這裏,等待$read裏面的socket可讀,這裏$read是個引用參數,也就是說stream_select()返回後$read是會被重新賦值
            // $read值的內容就是所有狀態可讀的socket集合
            stream_select($read, $write, $except, 60);
            // $read被重新賦值,遍歷所有狀態爲可讀的socket
            foreach ($read as $index => $socket) {
                // 如果是監聽socket可讀,說明有新連接
                if ($socket === $this->socket) {
                    // 通過stream_socket_accept獲取新連接(客戶端)
                    $new_conn_socket = stream_socket_accept($this->socket);
                    if (!$new_conn_socket) continue;
                    // 如果有onConnect事件回調,則嘗試觸發
                    if ($this->onConnect) {
                        call_user_func($this->onConnect, $new_conn_socket);
                    }
                    // 將新的客戶端連接socket放入allSockets,以便stream_select監聽其可讀事件
                    $this->allSockets[(int)$new_conn_socket] = $new_conn_socket;
                } else { 
                    // 如果是客戶端連接可讀,說明對應連接的客戶端有數據發來
                    // 讀數據
                    $buffer = fread($socket, 65535);
                    // 數據爲空,代表連接已經斷開
                    if ($buffer === '' || $buffer === false) {
                        // 嘗試觸發onClose回調
                        if ($this->onClose) {
                            call_user_func($this->onClose, $socket);
                        }
                        fclose($socket);
                        // 從allSockets裏刪除對應的連接,不再監聽這個socket可讀事件
                        unset($this->allSockets[(int)$socket]);
                        continue;
                    }
                    // 嘗試觸發onMessage回調
                    call_user_func($this->onMessage, $socket, $buffer);
                }
            }
        } //end while
    }
}

$server = new Worker('tcp://0.0.0.0:1215');

$server->onConnect = function ($conn) {
    echo "onConnect\n";
};

$server->onMessage = function ($conn, $msg) {
    fwrite($conn, "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nServer: workerman/1.1.4\r\nContent-length:5\r\n\r\nhello");
};

$server->onClose = function ($conn) {
    echo "onClose\n";
};

$server->run();

image.png

TcpConnection類

<?php

/**
 * 單進程IO複用select
 * 同時處理多個連接
 * ab -n100000 -c100 -k http://127.0.0.1:1215/
 */
class Worker
{
    // 連接事件回調
    public $onConnect = null;
    // 消息事件回調
    public $onMessage = null;
    // 連接關閉事件回調
    public $onClose = null;
    // 監聽端口的socket
    protected $socket = null;
    // 所有socket,包括客戶端socket和監聽端口socket
    protected $allSockets = [];
    // 所有tcpconnection類的實例,也就是所有客戶端的連接對象
    public $connections = [];

    // 構造函數
    public function __construct($address)
    {
        // 創建監聽socket
        $this->socket = stream_socket_server($address, $errno, $errstr);
        echo "listen $address\r\n";
        // 設置爲非阻塞
        // 阻塞:當讀取一個socket時,如果對方沒有發來任何數據,這個讀取操作會一直卡着,直到這個socket超時
        // 非阻塞:當讀取一個socket時,如果對方沒有發來任何數據,讀取操作會立刻返回,返回空數據
        stream_set_blocking($this->socket, 0);
        // 將監聽socket放入allSockets
        $this->allSockets[(int)$this->socket] = $this->socket;
    }

    // 運行
    public function run()
    {
        while (1) {
            // 這裏不監聽socket可寫事件和外帶數據可讀事件
            $write = $except = null;
            // 監聽所有socket可讀事件,包括客戶端socket和監聽端口的socket
            $read = $this->allSockets;
            // stream_select是個IO複用函數。整個程序阻塞在這裏,等待$read裏面的socket可讀,這裏$read是個引用參數
            stream_select($read, $write, $except, 60);
            // $read被重新賦值,遍歷所有狀態爲可讀的socket
            foreach ($read as $index => $socket) {
                // 如果是監聽socket可讀,說明有新連接
                if ($socket === $this->socket) {
                    // 通過stream_socket_accept獲取新連接
                    $new_conn_socket = stream_socket_accept($this->socket);
                    if (!$new_conn_socket) continue;
                    $conn = new TcpConnection($new_conn_socket);
                    $this->connections[(int)$new_conn_socket] = $conn;
                    // 如果有onConnect事件回調,則嘗試觸發
                    if ($this->onConnect) {
                        call_user_func($this->onConnect, $conn);
                    }
                    // 將新的客戶端連接socket放入allSockets,以便stream_select監聽其可讀事件
                    $this->allSockets[(int)$new_conn_socket] = $new_conn_socket;
                } else { // 是客戶端連接可讀,說明對應連接的客戶端有數據發來
                    // 讀數據
                    $buffer = fread($socket, 65535);
                    // 數據爲空,代表連接已經斷開
                    if ($buffer === '' || $buffer === false) {
                        // 嘗試觸發onClose回調
                        if ($this->onClose) {
                            call_user_func($this->onClose, $this->connections[(int)$socket]);
                        }
                        fclose($socket);
                        // 從allSockets裏刪除對應的連接,不再監聽這個socket可讀事件
                        unset($this->allSockets[(int)$socket], $this->connections[(int)$socket]);
                        continue;
                    }
                    // 嘗試觸發onMessage回調
                    call_user_func($this->onMessage, $this->connections[(int)$socket], $buffer);
                }
            }
        } //end while
    }
}

問題:

  • 上述 onConnect、onMessage、onClose 回調中需要使用 php 原始 api 來操作 socket
  • TcpConnection 其實是對這些 socket 的二次封裝
<?php

class TcpConnection
{
    protected $_socket = null;

    public function __construct($socket)
    {
        $this->_socket = $socket;
    }

    public function send($buffer)
    {
        //檢測socket是否斷開
        if (feof($this->_socket)) return false;
        return fwrite($this->_socket, $buffer);
    }
}
<?php

require_once 'Worker.php';
require_once 'TcpConnection.php';

$server = new Worker('tcp://0.0.0.0:1215');

$server->onConnect = function ($conn) {
    $conn->send('input your name: ');
};

$server->onMessage = function ($conn, $msg) {
    if (!isset($conn->name)) {
        $conn->name = trim($msg);
        broadcast("{$conn->name} come");
        return;
    }
    broadcast("{$conn->name} said: $msg");
};

$server->onClose = function ($conn) {
    broadcast("{$conn->name} logout");
};

function broadcast($msg)
{
    global $server;
    foreach ($server->connections as $conn) {
        $conn->send("$msg\r\n");
    }
}

$server->run();

image.png

源碼解析

客戶端與worker進程的關係
image.png

  • 連接哪個Worker進程由操作系統根據系統的負載情況自動分配

主進程與worker子進程關係
image.png

Workerman                      // workerman內核代碼
    ├── Connection                 // socket連接相關
    │   ├── ConnectionInterface.php// socket連接接口
    │   ├── TcpConnection.php      // Tcp連接類
    │   ├── AsyncTcpConnection.php // 異步Tcp連接類
    │   └── UdpConnection.php      // Udp連接類
    ├── Events                     // 網絡事件庫
    │   ├── EventInterface.php     // 網絡事件庫接口
    │   ├── Event.php              // Libevent網絡事件庫
    │   ├── Ev.php                 // Libev網絡事件庫
    │   ├── Swoole.php             // Swoole網絡事件庫
    │   └── Select.php             // Select網絡事件庫
    ├── Lib                        // 常用的類庫
    │   ├── Constants.php          // 常量定義
    │   └── Timer.php              // 定時器
    ├── Protocols                  // 協議相關
    │   ├── ProtocolInterface.php  // 協議接口類
    │   ├── Http                   // http協議相關
    │   │   ├── Chunk.php    // http chunk類
    │   │   ├── Request.php  // http 請求類
    │   │   ├── Response.php  // http響應類
    │   │   ├── ServerSentEvents.php  // SSE類
    │   │   ├── Session
    │   │   │   ├── FileSessionHandler.php  // session文件存儲
    │   │   │   └── RedisSessionHandler.php // session redis存儲
    │   │   ├── Session.php  // session類
    │   │   └── mime.types   // mime映射文件
    │   ├── Http.php               // http協議實現
    │   ├── Text.php               // Text協議實現
    │   ├── Frame.php              // Frame協議實現
    │   └── Websocket.php          // websocket協議的實現
    ├── Worker.php                 // Worker
    ├── WebServer.php              // WebServer
    └── Autoloader.php             // 自動加載類

事件循環

  • event
    • 這是一個擴展,有效地調度I/O,時間和信號基礎事件使用最好的可用於特定平臺的I/O通知機制。這是將libevent移植到PHP基礎設施的一個端口。
  • libevent
    • Libevent 是一個庫,它提供了一種機制,在文件描述符上發生特定事件時或超時後執行回調函數。
    • PHP8 開始在文檔中被移除。。。
  • select
    • stream_select - 在給定的流數組上運行與 select() 系統調用等效的操作,超時由 tv_sec 和 tv_usec 指定
protected static $_availableEventLoops = array(
  'event'    => '\Workerman\Events\Event',
  'libevent' => '\Workerman\Events\Libevent'
);

/**
 * Get event loop name.
 *
 * @return string
 */
protected static function getEventLoopName()
{
    if (static::$eventLoopClass) {
        return static::$eventLoopClass;
    }

    if (!\class_exists('\Swoole\Event', false)) {
        unset(static::$_availableEventLoops['swoole']);
    }

    $loop_name = '';
    foreach (static::$_availableEventLoops as $name=>$class) {
        if (\extension_loaded($name)) {
            $loop_name = $name;
            break;
        }
    }

    if ($loop_name) {
        static::$eventLoopClass = static::$_availableEventLoops[$loop_name];
    } else {
        static::$eventLoopClass =  '\Workerman\Events\Select';
    }
    return static::$eventLoopClass;
}

疑難問題定位

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章