1. 程式人生 > 實用技巧 >Swoole從入門到入土(9)——TCP伺服器[協程風格]

Swoole從入門到入土(9)——TCP伺服器[協程風格]

上一篇,我們一起初步接觸了協程。我相信只有一節的討論,很多小夥伴對於“協程”與“執行緒”的區分可能還有點模糊。我們這裡以兩者的比較作為本篇開頭,進行一番比較。

首先,“協程”與“執行緒”的任務排程機制不一樣。“協程”重在“協調”,“執行緒”重在“搶佔”。

舉個例子:現在有一個任務,需要5個“子協程”同時進行處理,那麼執行順序是:子協程1執行->子協程1暫停->子協程2執行->子協程2暫停……順序執行,直到結束。所以,協程的特點是某個協程讓出時間片後,下一個協程會接上;如果這個協程還在阻塞中,會再次移交給下一個協程。這樣往復執行,直到結束。

同樣的,如果現在有一個任務,需要5個“執行緒”同時執行,那麼執行順序是,主執行緒同時開闢啟動5個執行緒,這5個執行緒哪一個先執行,當先執行的執行緒讓出時間片後,接下來由哪一個執行緒接上,這些完全聽天由命。所以執行緒的執行才叫做“搶佔式”,直到任務結束。

其次,“協程”與“執行緒”解決的事情不一樣。“協程”解決的的是“併發”問題,“執行緒”解決的是“並行”問題。

舉個栗子,我們的伺服器需要支撐500人同時訪問,以大名鼎鼎的TOMCAT為例,正常情況下會開闢300個執行緒進行處理。某個執行緒一旦接收了請求就會進行阻塞,在處理完成之前就不會再接收第二個請求,這就決定了這時TOMCAT只能“並行”支援300人同時訪問,另外的200人只能收到500的報錯。但是這裡存在一個問題,500人的請求業務並不是全都是阻塞行為(訪問資料庫、讀寫檔案等),所以我們完全可以在一個執行緒中開闢N個協程,一個執行緒同時接受N個請求,對於有阻塞的業務請求進行等待直到業務完成,對於沒有阻塞的請求,我們就可以立即響應,這樣就可以將“併發”從300,提高到300*N(N>=2)。

當然,從上面這個栗子,我們可以看出“協程”針對的是IO密集型的業務(多阻塞),而執行緒針對的是計算密集型的業務。但我們不能簡單認為IO密集型業務,協程開得越多越好。因為對於IO的處理也需要IO資源,如果同時接受太多的需要IO請求,伺服器對於IO的處理在超時時間範圍內無法處理完,同樣會出現超時錯誤。

有了以上的比較,我們對於“協程”與“執行緒”的比較應該是更加明晰了。接下來,我們就一起進入協程風格的TCP伺服器話題。先看以下這段程式碼:

//多程序管理模組
$pool = new Swoole\Process\Pool(2);
//讓每個OnWorkerStart回撥都自動建立一個協程
$pool
->set(['enable_coroutine' => true]); $pool->on('workerStart', function ($pool, $id) { //每個程序都監聽9501埠 $server = new Swoole\Coroutine\Server('127.0.0.1', '9501' , false, true); //收到15訊號關閉服務 Swoole\Process::signal(SIGTERM, function () use ($server) { $server->shutdown(); }); //接收到新的連線請求 並自動建立一個協程 $server->handle(function (Swoole\Coroutine\Server\Connection $conn) { while (true) { //接收資料 $data = $conn->recv(); if (empty($data)) { $conn->close(); break; } //傳送資料 $conn->send('hello'); \Co::sleep(1); } }); //開始監聽埠 $server->start(); }); $pool->start();

這是一段完整的協程風格TCP伺服器的程式碼,對於這段程式碼涉及到的技術點進行以下討論。

1、關於Swoole\Process\Pool

對於協程程式碼,我們一般需要一個“協程容器”,來保證一組協程的完整性。在swoole中,建立“協程容器”的方法有兩種(編者注:官網寫的是三種,但這裡不考慮將非同步風格轉為協程風格的方式):

1) 呼叫管理模組 Process 和 Process\Pool 的 start 方法,指定建構函式的 enable_coroutine 引數,此種啟動方式會在程序啟動的時候建立協程容器。

2) 手動建立協程容器,即呼叫Co\run() 函式,或者使用Coroutine\Scheduler(這兩者是一樣的,前者是後者的簡易呼叫)。

對於TCP伺服器,我們需要併發,需要多個程序進行管理,所以選擇了Swoole\Process\Pool(這個程序池的使用,後續我們會詳細討論)。

對於Pool初使化後,在子程序建立時(onWorkerStart事件觸發),內部例項化了協程化的TCP伺服器。

當$pool->start()呼叫後,這時會創建出2個子程序(Pool建構函式中傳入的子程序數量),所以建立後共有3個程序。主程序進入wait狀態,對子程序進行管理,一旦子程序退出,就會再進行啟動。這時我們可以把onWorkerStart事件的處理函式看成是一個大的協程容器。

2、Swoole\Coroutine\Server->__construct

Swoole\Coroutine\Server->__construct(string $host, int $port = 0, bool $ssl = false, bool $reuse_port = false);

$host:監聽的地址,有三種格式:0.0.0.0/127.0.0.1: IPv4 地址 || ::/::1: IPv6 地址 || unix:/tmp/test.sock: UnixSocket 地址

$port:監聽的埠【如果為 0 將由作業系統隨機分配一個埠】

$ssl:是否開啟 SSL 加密

$reuse_port:是否開啟埠重用

Swoole\Coroutine\Server 是一個完全協程化的類,用於建立協程 TCP 伺服器,支援 TCP 和 unixSocket 型別。它與 Server 模組不同之處在於以下兩點:

1)動態建立銷燬,在執行時可以動態監聽埠,也可以動態關閉伺服器;

2)處理連線的過程是完全同步的,程式可以順序處理 Connect、Receive、Close 事件

3、Swoole\Coroutine\Server->handle:設定連線處理函式,必須在 start() 之前設定處理函式

Swoole\Coroutine\Server->handle(callable $fn);

$fn:設定連線處理函式

注意:

-伺服器在 Accept(建立連線) 成功後,會自動建立協程並執行 $fn
-$fn 是在新的子協程空間內執行,因此在函式內無需再次建立協程
-$fn 接受一個引數,型別為 Swoole\Coroutine\Server\Connection 物件
-可以使用 exportSocket() 得到當前連線的 Socket 物件

示例:

$server->handle(function (Swoole\Coroutine\Server\Connection $conn) {
    while (true) {
        $data = $conn->recv();
    }
});

4、關於粘包處理Swoole\Coroutine\Server->set

Swoole\Coroutine\Server->set(array $options);

用法與非同步風格的TCP伺服器完全一致,如果已經遺忘的小夥伴,請點選這裡

示例:

$server->set([
    'open_eof_split' => true,
    'package_eof' => "abc"
]);

如果客戶端傳送,1234abc5678abc,則服務端就會recv到兩條資料,分別是1234abc和5678abc。

關於協程風格的TCP伺服器就介紹到這裡。如果想要了解協程風格的類Swoole\Coroutine\Server、Swoole\Coroutine\Server\Connection、Swoole\Process\Pool請閱讀官網,這部分非常直觀沒有什麼太多難點。

--------------------------- 我是可愛的分割線 ----------------------------

最後博主借地宣傳一下,漳州程式設計小組招新了,這是一個面向漳州青少年資訊學/軟體設計的學習小組,有意向的同學點選連結,聯絡我吧。