前言
前文再續,就書接上一回(拍一下驚堂木,然后喝口茶install一下B),話說筆者當初最早接觸Swoole的時候,正迫切的期望能找到一個使用PHP作為主要開發語言的TCP Server的解決方案,因為公司業務中積累了大量的PHP代碼,而新增的業務又迫切需要實現與客戶端的主動通信,最終在盆友的推薦下,找到了Swoole。
輪詢與長連接
一般情況下,我們接觸PHP都是作為一個Web網站的開發語言而接觸的,例如一個最簡單的HelloWorld.php,往往是這么寫的:
<?php
echo "Hello PHP";
LAMP的配置這里就不多說了
不自覺的,驀然間會讓我們產生一種錯覺,PHP只能用來處理這種場景的工作,其他事情并不合適。
亦或者說,很多盆友并沒有意識到,PHP其實還隱藏了洪荒之力
當時,筆者需要開發一個實時的消息服務APP,消息的實時性要求較高,也就是說,服務端需要可以主動向客戶端推送消息,而這個時候,如果再采用傳統的http api的方式,勢必陷入輪詢的困局。
客戶端每隔1s向服務器請求,檢查是否有新的數據,這種場景可能會產生大量無用的請求,也會極大的增加服務端的負荷。
傳統的Web服務,采用http/https作為應用層協議,并且通過“請求->響應”的機制實現客戶端和服務端的通訊,也就是說,服務端總是“被動”的提供服務,服務端“難以”主動的將消息告知客戶端。
這其實也是是websocket的產生背景。
這個時候,我們可以考慮實現自己的TCP Server,以解決這個問題。
顯然,這里討論的問題并不局限于開發語言,.Net、Java、Go、NodeJS等都有對應的解決方案。
通過TCP協議構建的Server,是可以實現服務端和客戶端保持一個持久的鏈接,鏈接一旦建立,就像電話打通了一樣,通話的雙方都可以主動向對方發送消息。
其實http/https協議的傳輸層也是tcp協議,但為啥http/https協議變成了一次性的服務呢?有緣的話,下回分解。
因此,雙方的鏈接會呈現出“持久在線”的狀態,也就是長連接這一說法的由來。
有興趣的盆友可以自行查找TCP是怎么實現“在線”這個狀態的,還記得筆者上學時,計算機網絡的老師的一句話,網絡通信上絕對的可靠是不存在的。
TCP Server在干啥?
回到我們的應用場景,客戶端需要先與服務端建立TCP長連接,并維持這個鏈接,當服務端產生了新的消息時,服務端主動將新消息發送給客戶端,客戶端接收消息并解析,然后將結果展示給客戶端。
以下例子,改編自拙作《當SWOOLE遇上SERVER》
<?php
//vi swoole_tcp_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();
如果你是在遠程服務器上運行的,請將127.0.0.1替換為你的遠程服務器公網IP(或者你能訪問的內網IP)。
上一章的例子中,我們每次receive了一個客戶端的消息以后,就關閉了與這個客戶端的鏈接,并沒有向客戶端發出響應,但事實上,服務端完全可以在收到消息以后,向客戶端發出一個回復,就像“請求->響應”的工作機制一樣:
<?php
//我們修改一下on reveive的回調,然后啟動服務
$server->on('receive', function ($serv, $fd, $from_id, $data)
{
//根據收到的消息做出不同的響應
switch($data)
{
case 1:
{
$serv->send($fd,"1 for apple\n");
break;
}
case 2:
{
$serv->send($fd,"2 for boy\n");
break;
}
default:
{
$serv->send($fd,"Others is default\n");
}
}
});
用telnet作為客戶端訪問一下我們剛剛啟動Server
> telnet 127.0.0.1 8088
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
然后分別輸入“1"、“2”、“hello”并回車
以下是telnet的輸出
> telnet 127.0.0.1 8088
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
1
1 for apple
2
2 for boy
3
others is default
5
others is default
hello
others is default
這段代碼很簡單,如果receive了客戶端的消息,對消息做一個switch,根據switch的結果,向客戶端返回不同的消息。
這個場景是一個很典型的“請求->響應”的場景
那有些盆友也許會問了,這樣做的話,跟我使用URL訪問網站獲取響應有什么區別?
這個問題很好,思考是不斷進步的階梯
那么我們來做些不一樣的,繼續修改on receive的回調:
<?php
//我們修改一下on reveive的回調,然后啟動服務
$server->on('receive', function ($serv, $fd, $from_id, $data)
{
//根據收到的消息做出不同的響應
switch($data)
{
case 1:
{
foreach($serv->connections as $tempFD)
{
$serv->send($tempFD,"1 for apple\n");
}
break;
}
case 2:
{
$serv->send($fd,"2 for boy\n");
break;
}
default:
{
$serv->send($fd,"Others is default\n");
}
}
});
當case 1的時候,我們遍歷了$serv的connections成員,獲得了與當前服務器連接的所有客戶端,并且向所有的客戶端都發送了“1 for apple\n”這個字符串。繼續用telnet作為客戶端,我們這次需要打開兩個telnet,當兩個telnet都成功連接了Server之后,用第一個telnet發送1:
第一個telnet客戶端的輸出
> telnet 127.0.0.1 8088
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
1
1 for apple
第二個telnet客戶端的輸出
> telnet 127.0.0.1 8088
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
1 for apple
第二個telnet客戶端雖然并沒有向server端發送“1”作為消息,server端仍然向第二個客戶端發送了消息“1 for apple\n”,這可以做什么?如果我們要做一個聊天室的話,就可以簡單的實現發送公共聊天消息的功能。
如果打開一下腦洞,在Server的業務中將用戶分類存儲,發送的時候有選擇的向不同的用戶發送消息,就可以實現私聊,亦或者是分組消息。
如果只是這樣,可能又有童鞋問了,僅僅這樣做,還是一個“請求->響應”的工作模式吖,只不過是將一對一的請求響應,變成了一對多的請求響應?
確實有點這個感覺,那我們來做點不一樣,這次,server會不斷向客戶端發送消息,不管有沒有請求。
<?php
//這次我們要修改的是on connect回調哦!
$server->on('connect', function ($serv, $fd)
{
$serv->tick(1000, function() use ($serv, $fd) {
$serv->send($fd, "這是一條定時消息\n");
});
});
以上代碼中的tick方法,表示啟動一個定時器,該定時器每1000毫秒觸發一次,并執行回調方法。
Swoole Tick是Swoole工具箱中的一個強大工具,它比PHP原生的pcntl_alarm更加精確,也支持異步調用,非常方便,更多介紹可以參考手冊。
這次仍然是打開telnet
> telnet 127.0.0.1 8088
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
這是一條定時消息
這是一條定時消息
這是一條定時消息
這是一條定時消息
這是一條定時消息
這是一條定時消息
這是一條定時消息
2
這是一條定時消息
2 for boy
這是一條定時消息
這是一條定時消息
這是一條定時消息
這是一條定時消息
這是一條定時消息
這是一條定時消息
...
這次,只要連接上服務器,不管客戶端說沒說話,會一直收到“這是一條定時消息”的消息,并且,如果我們見縫插針地寫個2并發送,就會收到on receive中的反饋“2 for boy”,并不會與“這是一條定時消息”沖突。
這里,前者就是服務端主動發出,客戶端被動接受的消息;而后者,卻又是“請求->響應”的工作模式,兩者并不沖突,僅取決于具體的代碼實現。
小結
好滴,今天的三分熱度就到這了,再多就得超時了,這篇的內容主要列舉了TCP Server的幾個基本工作場景,及這些場景通過Swoole Server的簡單實現。其實TCP Server的核心應用特征就在于,一旦連接建立,雙方都可以平等地自由選擇什么時候向對方發出消息,并選擇是否對收到的消息做出響應。
想象一下,你跟你的基友在電話兩旁自說自話 QAQ