1. 程式人生 > >當SWOOLE遇上PROTOCOL

當SWOOLE遇上PROTOCOL

前言

上回我們簡單介紹了一下TCP Server的工作方式以及如何用Swoole實現一個簡單的TCP Server,這次我們來聊聊資訊流動中,非常重要基石之一——協議(PROTOCOL)。

協議,通訊的基石

每次講到協議,都會想起小時候學習語文時,有段時間特別痴迷各種文字遊戲。

那青蔥的歲月吖~

其中有一種遊戲,相信各位也應該接觸過,就是斷句遊戲——一句話,如果加上不同的標點符號,則可能會產生截然相反的歧義。

後來高中的文言文斷句練習徹底把這種愉快的遊戲毀了=。=

可能最經典但其實也不怎麼好笑的就是“下雨天留客天留我不留”,短短的一句話,斷做“下雨天留客,天留我不留”,抑或是是斷做“下雨天,留客天,留我不?留!”

其實個人呢,覺得這個斷的有點牽強,可能因為我還不是古人,吾還年輕~

那麼斷句和我說的協議又有什麼關係呢?關係大了,如果斷錯了句,傳遞的資訊就會發生誤解,網路通訊中也一樣,我們都知道計算機底層的資料本質上都可以看作0和1,也就是說再複雜的訊息,承載的時候也只是0和1,如果不能正確的斷句,那肯定是會出問題的。

啥?光量子計算機有十六個狀態位?好吧……

通訊的雙方約定一種理解的規則,以便對理解對方想表達的資訊,這種解析資訊的規則,就是今天的主題,協議。

在語文上,我們用的是標點符號;數學上,我們有各種的加減乘除……

從HTTP到TCP,從應用層回到傳輸層

相信TCP協議(Transmission Control Protocol)應該是想學習SWOOLE的童鞋最容易遇到的攔路虎之一,因為一般我們使用PHP做網站開發的時候,並不需要處理涉及TCP協議的東西,只要瞭解一部分HTTP協議(HyperText Transfer Protocol)就可以做很多事情。

甚至只是知道Get和Post就可以了,更細緻的工作,巨人們已經幫我們完成了。

在故事繼續之前,請允許我先簡單引入一下傳說中的4層協議,TCP就是傳輸層的協議,而HTTP是應用層,這兩個協議有什麼關係呢?我們做個有趣的實驗看看:

以下程式碼改編至拙作《當SWOOLE遇上TCP》

<?php

$server = new \swoole_server("127.0.0.1",8088,SWOOLE_PROCESS,SWOOLE_SOCK_TCP);

$server->on('connect', function ($serv, $fd)
{

});

$server
->on('receive', function ($serv, $fd, $from_id, $data) { // 這次,我們只需要簡單的把收到的資料打印出來即可 // 但是,我們會在一頭一尾各列印一行邪惡的分隔線 // 以便清楚的劃分收到的資料內容 echo "====================邪惡的開頭分隔線====================".PHP_EOL; echo $data;//列印收到的資料正文 echo "====================邪惡的結尾分隔線====================".PHP_EOL; } $server->on('close', function ($serv, $fd) { echo "client: close.\n"; }); $server -> start();

遠端主機\IP\埠的問題,本文就掠過啦,有需要看本系列的前作。

好,我們之前是通過telnet,實現與SWOOLE的TCP Server之間的簡單通訊的,這次我們玩點不一樣的,首先仍然是啟動SWOOLE Server,然後,開啟瀏覽器,沒錯,在位址列中輸入:“http://127.0.0.1:8088” ————

喂,我執行的是TCP Server,開瀏覽器幹什麼啦?

顯然,瀏覽器什麼都沒有輸出,又或者爆出一個錯誤,但這個時候返回我們的終端看看:

> php swoole_server_demo.php
====================邪惡的開頭分隔線====================
GET / HTTP/1.1
Host: 127.0.0.1:8088
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/250.36 (KHTML, like Gecko) Chrome/52.0.2743.250 Safari/250.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8

====================邪惡的結尾分隔線====================

沒錯,雖然我們執行的是TCP Server,雖然我們是使用瀏覽器,而不是telnet訪問的,我們的Server仍然打印出了顯然非常有規律的資訊,相信很多童鞋已經發現了,我們使用Chrome開發網頁時,經常使用的除錯工具箱裡,就會在Network工具中的Header中看到類似的東西。

這就是根據HTTP協議編寫的一段資訊。

而編寫者是誰呢?沒錯,就是我們一直默默無聞而幾乎是網際網路改變世界的基石之一,瀏覽器,每當我們通過瀏覽器訪問不同的網站時,瀏覽器都會默默生成類似的文字作為WebRequest的正文,提交給對應的服務端。

有興趣的童鞋,可以試試使用附帶Get請求、Post請求等方式訪問,看看Server端收到的文字所有什麼不同

木有錯,這就是超文字傳輸協議的本體,也是為什麼叫超文字的原因,它是通過特定格式的字串完成請求的描述的,在《當SWOOLE遇上SERVER》一文中,我曾經提到Apache收到客戶端請求以後,經過一定的解析,再由Zend呼叫PHP指令碼執行業務工作並完成輸出;這裡提到的請求就是這個。

當然,瀏覽器上經常遇到協議還有HTTPS,這裡先按下不表。

完整的HTTP協議非常複雜,筆者這裡就不詳細敘述了,但HTTP協議有一個基本規則,各個欄位之間,是通過“\r\n”進行分割的,簡單說,當我們收到一個“完整的”HTTP請求的時候,可以用explode方法快速的劃分區段,然後再根據區段進行解析,就能知道使用者請求的是什麼了。

看格式其實或多或少都能猜到寫了什麼

知道使用者請求的是什麼,我們就可以選擇性的輸出使用者想要的東西,例如:

$server->on('receive', function ($serv, $fd, $from_id, $data)
{
    $reqAry = explode("\r\n",$data);

    if (stripos($reqAry[0],"Hello.php") !== FALSE )
    {
        echo "使用者想呼叫Hello.php".PHP_EOL;
        $serv->send($fd,"你呼叫了Hello.php方法");
    }
    else if (stripos($reqAry[0],"World.php") !== FALSE )
    {
        echo "使用者想呼叫World.php".PHP_EOL;
        $serv->send($fd,"你呼叫了World.php方法");
    }
    else
    {
        echo "使用者想請求了一個不支援的方法".PHP_EOL;
        $serv->send($fd,"404,你呼叫的方法我們不支援。");
    }
}
> php swoole_server_demo.php
使用者想呼叫Hello.php
使用者想呼叫World.php
使用者想請求了一個不支援的方法

然後我們會發現瀏覽器分別列印了我們在分支語句中send回來的內容,就像平時呼叫了echo一樣。

嚴格來說,這樣寫,不一定能輸出出來,因為HTTP協議對返回值的格式也有約定。

如果我們對這個方法做的更完善一些,例如根據請求名,反射出Controller例項,並執行Controller的某個Method,整個過程幾乎就跟我們常見的MVC框架一樣了。

事實上,在筆者看來,C中執行的業務邏輯,可以看作是”業務層”協議了

無論是根據“\r\n”分段也好,根據“ ”拆分每段內部的欄位也好,這些規則,都是協議本身的一部分。

一般網路小說中,掌握了規則的強者總是開始努力打破規則,乃至定製自己的規則(所謂我的領域做主)

HTTP的規則簡單介紹的這裡,我們回到一開始的問題,為什麼我運行了一個TCP Server,卻能實現HTTP的內容?

相信盆友強忍著讀到這裡估計都會覺得筆者太羅嗦了,HTTP協議在傳輸層就是TCP協議實現的嘛

想象一下你和你的基友正在打電話,你們說的是漢語、英語、德語、法語或者基語,是不是都不會影響兩個電話之間的通訊,電話工作是隻要保證把聲音傳達到位,至於裡邊的內容,電話是不關心的。

所謂不在其位,不謀其政

而分層協議的工作原理也是一樣,TCP作為傳輸層協議,它僅實現了傳輸層的某些特性,例如長連線,例如一個高可靠性的傳輸到位確認機制,但它對它傳輸的內容,具體怎麼被識別或者處理,是不關心的。

TCP也有自己的互動流程和解析機制,但要比HTTP複雜,這裡就不討論了。

而HTTP協議是應用層協議,顧名思義,它關注的是應用,也就是收到傳輸層TCP收到的訊息以後,根據具體的應用進行處理。

除了HTTP以外,常見的諸如HTTPS、FTP、WebSocket等,也都是應用層協議,而它們的傳輸層都是TCP實現的。

應用層協議百花齊放,傳輸層的協議卻要凋零的多,最常見的,無非是TCP和UDP。

就像有聲語言可能有千百種,一個電話一個簡訊就夠了。

所以,在架設自己的TCP Server的時候,要解決的第一個問題,就是,我的應用層協議是什麼?

我心即天心

首先,要解決應用層協議的問題,先要選擇一個傳輸層協議,基於這個協議的特點,我們再去設計應用層協議。

就像選擇開發語言和開發環境一樣,雖然說語言只是工具,但工具也有適用場景,不是說絕對不行,只是事倍功半的事兒,必要時還是可以避免的。

就像前文所言,協議的設計完全是由掌握了規則之力的人決定的(例如CTO),筆者這邊就不多討論怎麼設計協議才是對的,僅介紹設計基於TCP協議時要注意的問題。

無盡的資料流

TCP協議最大的一個特點,就是其傳輸的資料流是連續的,就像打電話一樣,打電話的時候,我們以語氣的停頓、語音、語調等作為理解對方意圖的輔助元素,那TCP協議傳輸的資料流,OnReceive的時候也分分鐘會遇到類似這樣的問題:

假設我們在tellnet中執行了以下的虛擬碼,向Server傳送了7條資料

> TCP協議最大的一個特點
> 就是其傳輸的資料流是連續的
> 就像打電話一樣
> 打電話的時候
> 我們以語氣的停頓
> 語音
> 語調等作為理解對方意圖的輔助元素

此時,雖然Server仍然有90%的可能(主要是網路通暢和輸入的速度),OnReceive方法會被回撥7次,而且每次收到的資料都與傳送時一模一樣,仍然不能排除會有以下的可能出現:

TCP協議最大的一個特點就是其傳輸的資料流是連
續的
就像打
電話一樣打電話的時候我
們以語氣的停
頓語音語調等作為理解對方意圖的輔助元素

首先,並不一定會回撥7次,可能會回撥1次就收到了所有資料,也可能要回調70次才能完整的收到所有資料,但是,無論回撥多少次,收到的順序是與傳送順序保持一致的,也就是不會出現以下情況:

續的
就像打
TCP協議最大的一個特點就是其傳輸的資料流是連
們以語氣的停
頓語音語調等作為理解對方意圖的輔助元素
電話一樣打電話的時候我

所以,很多時候,我們都會稱呼TCP的資料叫資料流,從傳輸層來看,TCP的資料包之間沒有邊界,怎麼從TCP的資料流中正確的擷取每個資料包,是設計TCP協議的第一步。

這就是傳說中的分包和合包

最常見的資料包處理方式有兩種,分別是結束符和固定包頭兩種,Swoole也非常貼心的替我們提供了這兩種方案的常規處理,這樣我們在使用的時候就不需要自己寫分包合包的程式碼了。

結束符(EOF)

結束符處理方式很簡單,雙方約定各個資料包的結尾有穩定的結束符,且在資料包的正文中不要出現該結束符,那麼資料的接收方,只要逐個位元組地檢查收到的資料,一旦發現結束符,就把上一個結束符(也可能是開頭),到當前結束符之間的資料拆出來,作為一個數據包,進行進一步的處理

常見的應用層協議中,MEMCACHE\FTP\STMP都是採用這種思路,它們使用的結束符是“\r\n”

而在Swoole中,可以在配置中這樣寫:

$server = new \swoole_server("127.0.0.1",8088,SWOOLE_PROCESS,SWOOLE_SOCK_TCP);
$server->set(
    [
        'open_eof_split' => true,
        'package_eof' => "\r\n"
    ]
);
// 回撥方法略

$server-start();

此時,假如發來的資料是根據”\r\n”作為結束符分包的資料流,每次OnReceive的時候,就一定是Swoole已經幫我們分好的資料包,我們直接做進一步的應用協議處理就好了。

固定包頭+包體

這種方案也是非常非常常見的解決方案,核心設計思路是,每個資料包由兩部分組成,分別是固定長度的包頭,和不確定長度的包體。包頭中描述了包體的長度,接收資料的時候,先按包頭的固定長度讀取一定的資料,然後解析包頭中的內容,獲得這個資料包包體的長度,然後繼續接收資料,直到收到了跟包頭中描述的包體長度一樣的資料,進而截斷出完整的資料包。

可以說,基本上除了EOF的方式以外,都是這種處理方式

例如說,我們的資料包可以這麼寫:

這個資料包由十九個字組成今天天氣好好啊這個資料包由二十個字組成昨天晚上又加班了

每個資料包的前12個字就是包頭,讀了包頭,我們就知道了整個資料包的長度,減去包頭12個字,就知道這個資料包剩下還要讀取長了。

當然,作為計算機,使用二進位制的方式直接描述資料包才是更常見解決方案。

例如說,我們約定包頭的長度是4個byte,這4個byte按照大端序就組成了一個int,而這個int資料描述的就是整個資料包的長度(包括包頭本身的4個byte的長度),那麼此時,Swoole中的配置應該是:

$server->set(
    [
        'open_length_check' => true,
        'package_length_type' => 'N', //N表示32bit的大端序
        'package_length_offset' => 0,//從第幾個位元組開始是長度,比如包頭長度為4個byte,第0個byte開始就是長度值,那這裡就填入0
        'package_body_offset' => 2,//從第幾個位元組開始計算長度,比如包頭為長度為4個byte,第0個位元組為長度值,包體長度為1000。如果長度包含包頭,這裡填入0,如果不包含包頭,這裡填入4
        'package_max_length' => 1024//最大允許的包長度。因為在一個請求包完整接收前,需要將所有資料儲存在記憶體中,所以需要做保護。避免記憶體佔用過大。
    ]
);

雖然今天是教師節,如果把大端序的問題也講進來估計就超時了,大端序、小端序以及php的pack方法、unpack方法等,就暫時按下不表了

關於TCP的通訊協議問題,SWOOLE手冊中也有相關的說明網路通訊協議設計

小結

今天,筆者簡單介紹了應用層協議和傳輸層協議的關係,並基於TCP協議,給出了基於TCP的應用層協議時,應當注意的問題,也給出了Swoole中相關的一些解決方案,希望能給剛接觸網路通訊的PHPer們帶來一點啟發。

回顧

這個系列我已經上傳到了github上,歡迎圍觀:github.com/szyhf/swoole_study

  1. 當SWOOLE遇上PHP 【SWOOLE安裝、PHP的CLI模式】
  2. 當SWOOLE遇上SERVER 【TCP/IP】
  3. 當SWOOLE遇上TCP【TCP】

番外:

  1. 守護程序二三事與Supervisor