當SWOOLE遇上SERVER

前言

上一回講到,Swoole終于成功邂逅了PHP,現在要開始它們的奇妙路程了。

Server之初

通常,我們會把網絡系統分為B/S架構和C/S架構,而這里筆者想聊的就是這里提到的S,也就是服務(Server)在干什么?

風靡各大高校宿舍的Dota和LOL,主體上可以算是典型的C/S架構的網絡系統/軟件/應用/程序/What ever

這里筆者打算從比較常見的基于PHP的Web網站開始聊起。

這里舉例采用的是最基礎的Linux + Apache + PHP的開發環境。

當我們打開 http://127.0.0.1:80 并看到Apache的歡迎界面時,我們知道,我們已經成功的完成了一個簡單的B/S結構的程序。

雖然這里輸出的不是Hello World!雖然目前為止一句PHP也沒寫。

那么,這個時候,這里我們說B/S中的Server具體指代的是什么呢?以下兩個選項哪個是你的想法?

  1. 運行并保存著我們網站的服務器主機。
  2. Apache正在運行的進程。

其實從筆者的角度而言,上述兩個選項都是對的,因為Server這個詞本身的含義就很豐富,根據特定的語境,它既可以指服務器,也可以指服務程序。

本文中提到的Server如無特別說明,都是指提供服務的應用程序,在當前的場景中,就是Apache。

我們來簡單扒一下,在打開這個網頁的過程中,Apache作為一個Server,最少要做到哪些工作?

最基礎的工作,更深入的問題我們可以一點點討論。

首先,Apache需要先運行起來,如果Apache沒有運行,顯然沒法向瀏覽器提供服務(例如,輸出Apache的歡迎頁面)

傳統的Web網站場景中,Server是被動地提供服務的,也就是客戶端不請求,Server就不會提供服務,就像一般民事訴訟中的不告不理原則。

再者,瀏覽器需要有一個可靠的方法找到我們剛剛運行起來的Apache(就像寄快遞,要有收件方的地址)

想象一下市民中心的辦證大廳,各種各樣的窗口,不同的窗口可以辦理不同的證件,市民提交的材料就是“輸入”,服務臺提供的證件就是“輸出”,找到正確的窗口,是享受服務的前題。

例如我們在瀏覽器輸入 www.baidu.com ,我們知道會打開百度的首頁。

輸入 www.google.com ,會出現404無法訪問的錯誤?

當Apache做到了上述兩項工作時,我們可以簡單認為它具備了作為一個Server的基礎。

用物流管理的話說,就是在“正確的時間、正確的地點、正確的貨物”。

監聽:正確的地點

那么,Apache是怎么做到這兩點的呢?相信怎么運行Apache這個問題不必筆者啰嗦,我們主要開始看看第二個問題。

關于服務要穩定常駐運行的問題,可參見拙作《守護進程二三事與Supervisor》

我們怎么去定義這個“正確的地點”呢?最常見的方案,就是TCP/IP協議中的IP協議。

嚴謹地說,TCP協議是傳輸層協議;IP是網絡層協議。因為兩者常常搭配出現,就像LAMP一樣,有了TCP/IP協議這個說法。

IP協議幫助客戶端在浩瀚的網絡中找到正確的主機,例如上文中的 127.0.0.1 主機。

127.0.0.1 是IP協議中定義的一個特殊地址,表示本機,概念上有點像PHP中的$this。

如果我們的主機在局域網中被分配的IP是192.168.1.233,則其他主機也可以通過192.168.1.233這個地址找到我們的主機

IP是“IP協議”給每一臺聯網設備規定一個地址,便于互相通信和發現。

但一個主機如果只能運行一個服務,就太浪費了,因此如果說IP是用來區分不同的聯網設備的,Port在這就是用來區分同一個設備的不同服務的。

最基本的LAMP環境中,SSH需要一個端口(默認22),Apahce需要一個端口(默認80),Mysql需要一個端口(默認3306)。

一般情況下,端口的編號取值范圍是 [1, 65535],一般1000以下的端口都會被一些常用服務默認調用,所以盡可能不要使用。

就像大公司電話系統中的主機與分機一樣,IP是主機,全世界通用(沒有區號這些東西啦),Port就是分機,僅在自己的主機內通用。

寫到這里,前文我們提到的 127.0.0.1:80 的含義就更清晰了,前面的 127.0.0.1 是IP地址,后邊的80是端口。而因為80是HTTP服務的默認端口,所以訪問一般的常見網站時我們并不需要寫成 www.baidu.com:80

所以如果是自建HTTP服務的話,默認情況下還是最好提供80端口作為服務端口。

我們把某個服務通過某個端口對外提供服務的行為稱為“監聽”,形象的說,前文的Apache監聽著本機的80端口,如果有客戶通過這個端口發來請求,操作系統就會把請求交給Apache,Apache就可以根據請求的具體內容進行處理,并給出響應(例如,歡迎使用Apache!)。

常用“Listen”或者“Bind”這兩個動詞。

最后,我們可以整理出簡單LAMP環境中,基于HTTP協議的Web服務的交互邏輯:

  1. 客戶端(瀏覽器)將請求提交給指定IP的主機。
  2. 操作系統根據請求中的PORT,轉交給監聽了這個PORT的Apache。
  3. Apache根據配置找到合適的目錄,并獲取目錄中的PHP腳本。
  4. 調用Zend對該PHP腳本進行解析,并獲得輸出結果。
  5. 將輸出結果返回給客戶端(瀏覽器).

而一般情況下,PHP腳本的工作就在第四步中處理具體業務,至于怎么與瀏覽器保持通信,PHPer一般是不關心的,直到,Swoole重新定義了PHP。

其實還有別的方案,但,俺們的主題是Swoole(寫了快一百行才提到Swoole的筆者表示說這句話的時候有點心虛...)

Swoole Server做了什么?

前文我們以Apache作為例子,簡單梳理了一下Server在做什么的問題,再回來看Swoole Server,就好理解了,Swoole允許通過PHP構造一個新的Server,提供跟Apache類似的功能,監聽請求,作出響應。

其實也回答了群里很常見的一個問題,為什么我運行Swoole Server的時候會跟Apache\Nginx沖突?因為這里用PHP寫的不再只是網頁的業務邏輯,同時也包括Server的部分。

那么,我們現在開始第一個簡單的Swoole TCP Server的Demo。

<?php
//vi swoole_server_demo.php
$server = new \swoole_server("127.0.0.1", 8088, SWOOLE_PROCESS, SWOOLE_SOCK_TCP);
$server -> start();

后兩個參數涉及到了Swoole的運行方式和傳輸層使用的協議問題,我們以后再聊。

這里我們新建了一個Swoole Server對象,這個對象監聽了IP 127.0.0.1,和端口 8088 。然后我們打開瀏覽器,訪問http://127.0.0.1/swoole_server_demo.php,然后我們會驚訝地發現:報錯,swoole_server只能運行在cli模式下。

這是很多童鞋第一次接觸Swoole會遇到的另一個問題,還記得我前邊說的么?SwooleServer已經是一個獨立的服務了,它不再依賴于Apache,它本身就是一個完整的網絡Server,所以它需要運行在cli模式下,同時也不需要通過瀏覽器訪問的方式執行了。

所以正確的啟動方式應該是在shell中執行:

php swoole_server_demo.php
> PHP Fatal error:  swoole_server::start(): require onReceive/onPacket callback in swoole_server_demo.php

關于cli的問題,可以參考拙作《當Swoole遇上PHP》

這里的報錯又是什么原因呢?因為例子中使用的是TCP Server,我們用打電話的例子來梳理一下,客戶給服務打電話,對于服務來說,有這么幾個關鍵事件:

  1. OnConnect,建立連接,也就是電話被撥通的時候發生。
  2. OnReceive,收到消息,也就是服務聽到客戶說的話。
  3. OnClose,關閉連接,也就是客戶\服務其中一方掛掉了電話時發生。

一個完整的TCP服務需要處理這三個過程,而我們訪問網頁的時候,這些過程都被瀏覽器\服務程序內部處理掉了,平時寫網頁的時候只需要考慮對請求作出響應即可。

這種抽象進一步降低了PHP的上手門檻,也讓很多人低估了PHP的能力。

而使用Swoole Server的時候,我們需要自己來管理這個過程,同時,也可以在這個基礎上做到更多的事情。

所以說學習使用Swoole需要了解更多的是操作系統、計算機網絡方面的知識,有相關背景的童鞋學習使用Swoole其實并不困難,了解一下做不同事情需要使用的接口是什么,再了解一下運作機制,就差不多可以上手了。

那我們把這幾個回調函數補上,這樣就能運行一個完整的Server了:

<?php 
//vi swoole_server_demo.php
    $server = new \swoole_server("127.0.0.1",8088,SWOOLE_PROCESS,SWOOLE_SOCK_TCP);

    $server->on('connect', function ($serv, $fd){
            echo "Client:Connect.\n";
});
    $server->on('receive', function ($serv, $fd, $from_id, $data) {
            //打印收到的消息
            echo "Receive message: $data";
            //關閉連接(當然,也可以不關閉)
            $serv->close($fd);
});
    $server->on('close', function ($serv, $fd) {
            echo "Client: Close.\n";
});

$server -> start();

然后,重新在shell里邊執行:

php swoole_server_demo.php
> 

如果一切順利的話,我們會看到整個命令行好像卡住了,一個光標在閃動,但沒有任何輸出?這是什么情況?

筆者第一次遇到的時候思路也沒轉過彎來,一直以為故障了。

這個時候,其實Server已經啟動了,并且正在運行,監聽了本機的8088端口,此時Server處于等待的狀態,所以沒有任何輸出。

這個時候如果有客戶端訪問本機的8088端口,就會觸發OnConnect事件了。

我們打開另一個交互窗口(注意別關了Swoole Server正在運行的窗口),用telnet來試試:

# 在第二個Shell窗口
telnet 127.0.0.1 8088
> Trying 127.0.0.1...
> Connected to 127.0.0.1.
> Escape character is '^]'. 

此時,我們再返回第一個窗口,就會看到剛才卡住的光標有輸出了:

php swoole_server_demo.php
> Client:Connect.

輸出的正是我們在OnConnect回調中設置的內容。

想想貝爾第一個打通電話的瞬間。

這時我們可以隨便輸入一些字符看看:

# 在第二個Shell窗口
telnet 127.0.0.1 8088
> Trying 127.0.0.1...
> Connected to 127.0.0.1.
> Escape character is '^]'. 
Hello SwooleServer!
> Connection closed by foreign host.

此時再切換回第一個Shell,我們看到輸出增加了:

php swoole_server_demo.php
> Client:Connect.
> Receive message: Hello SwooleServer!
> Client: Close.

OK,我們來看看整個過程發生了什么事:

  1. 運行了一個TCP Server,它監聽了 127.0.0.1 主機的 8088 端口。
  2. 用telnet工具作為客戶端,它嘗試連接 127.0.0.1 主機的 8088 端口 的服務。
  3. Server收到了連接請求,根據TCP的握手機制完成了連接過程,并觸發了OnConnect回調,此時Server端輸出了字符串“Client:Connect.\n”
  4. telnet也獲得了連接成功的消息,輸出了“Connected to 127.0.0.1.”、“Escape character is '^]'.”等消息,此時,相當于電話已經打通了。
  5. telnet向Server發送了一個字符串“Hello SwooleServer!”。
  6. Server收到了telnet發來的字符串,并觸發了OnReceive回調,在該回調中,Server打印了字符串“Receive message: Hello SwooleServer!”,然后將與telnet的連接關閉了。
  7. 連接關閉后,Server觸發OnClose回調,輸出了字符串“Client: Close.”。
  8. 連接關閉后,telnet也輸出了字符串“Connection closed by foreign host.”

如果把這個過程弄清楚,那么就朝著Swoole的應用又邁出了一大步。

PPPPPPS:本來一直是兩周一更的節奏但今天女排奪冠了啊啊啊啊啊啊啊所以爬起來擼了這篇。

學習Swoole有時候很難,有時候又并不難,難點不在于Swoole的接口有多復雜,機制有多麻煩,而更多在于不知道它到底解決了什么問題,本文筆者對Swoole Server解決的問題做了簡單的梳理和介紹,希望能給剛剛接觸Swoole的童鞋一點啟發和借鑒。

彪悍的Swoole工具箱

Swoole具備一系列強大的工具,允許我們借助PHP高效開發的特性,寫出高性能的Web服務,那么這個工具箱里除了Swoole Server以外還有什么呢?以下引用自官網的手冊

Swoole Server

強大的TCP/UDP Server框架,多線程,EventLoop,事件驅動,異步,Worker進程組,Task異步任務,毫秒定時器,SSL/TLS隧道加密。

  • swoole_http_server是swoole_server的子類,內置了Http的支持
  • swoole_websocket_server是swoole_http_server的子類,內置了WebSocket的支持

Swoole Client

TCP/UDP客戶端,支持同步并發調用,也支持異步事件驅動。

Swoole Event

EventLoop API,讓用戶可以直接操作底層的事件循環,將socket,stream,管道等Linux文件加入到事件循環中。

eventloop接口僅可用于socket類型的文件描述符,不能用于磁盤文件讀寫

Swoole Async

異步IO接口,提供了 異步文件系統IO,異步DNS查詢,異步MySQL等API。包括2個重要的子模塊:

  • swoole_timer,異步毫秒定時器,可以實現間隔時間或一次性的定時任務
  • file,文件系統操作的異步接口

Swoole Process

進程管理模塊,可以方便的創建子進程,進程間通信,進程管理。

Swoole Buffer

強大的內存區管理工具,像C一樣進行指針計算,又無需關心內存的申請和釋放,而且不用擔心內存越界,底層全部做好了。

Swoole Table

基于共享內存和自旋鎖實現的超高性能內存表。徹底解決線程,進程間數據共享,加鎖同步等問題。

swoole_table的性能可以達到單線程每秒讀寫50W次

來源

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容