socket的中文名字叫做套接字,這種東西就是對TCP/IP的“封裝”。現實中的網路實際上只有四層而已,從上至下分別是應用層、傳輸層、網路層、資料鏈路層。最常用的http協議則是屬於應用層的協議,而socket,可以簡單粗暴的理解為是傳輸層的一種東西。如果還是很難理解,那再粗暴地點兒tcp://218.221.11.23:9999
,看到沒?這就是一個tcp socket。
socket賦予了我們操控傳輸層和網路層的能力,從而得到更強的效能和更高的效率,socket程式設計是解決高併發網路伺服器的最常用解決和成熟的解決方案。任何一名伺服器程式設計師都應當掌握socket程式設計相關技能。
在php中,可以操控socket的函式一共有兩套,一套是socket_系列的函式,另一套是stream_系列的函式。socket_是php直接將C語言中的socket抄了過來得到的實現,而stream_系則是php使用流的概念將其進行了一層封裝。下面用socket_*系函式簡單為這一系列文章開個篇。
先來做個最簡單socket伺服器:
<?php
$host = '0.0.0.0';
$port = 9999;
// 建立一個tcp socket
$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
// 將socket bind到IP:port上
socket_bind( $listen_socket, $host, $port );
// 開始監聽socket
socket_listen( $listen_socket );
// 進入while迴圈,不用擔心死迴圈當機,因為程式將會阻塞在下面的socket_accept()函式上
while( true ){
// 此處將會阻塞住,一直到有客戶端來連線伺服器。阻塞狀態的程式是不會佔據CPU的
// 所以你不用擔心while迴圈會將機器拖垮,不會的
$connection_socket = socket_accept( $listen_socket );
// 向客戶端傳送一個helloworld
$msg = "helloworld\r\n";
socket_write( $connection_socket, $msg, strlen( $msg ) );
socket_close( $connection_socket );
}
socket_close( $listen_socket );
將檔案儲存為server.php,然後執行php server.php執行起來。客戶端我們使用telnet就可以了,開啟另外一個終端執行telnet 127.0.0.1 9999按下回車即可。執行結果如下:
簡單解析一下上述程式碼來說明一下tcp socket伺服器的流程:
- 1.首先,根據協議族(或地址族)、套接字型別以及具體的的某個協議來建立一個socket。
- 2.第二,將上一步建立好的socket繫結(bind)到一個ip:port上。
- 3.第三,開啟監聽linten。
- 4.第四,使伺服器程式碼進入無限迴圈不退出,當沒有客戶端連線時,程式阻塞在accept上,有連線進來時才會往下執行,然後再次迴圈下去,為客戶端提供持久服務。
上面這個案例中,有兩個很大的缺陷:
- 1.一次只可以為一個客戶端提供服務,如果正在為第一個客戶端傳送helloworld期間有第二個客戶端來連線,那麼第二個客戶端就必須要等待片刻才行。
- 2.很容易受到攻擊,造成拒絕服務。
分析了上述問題後,又聯想到了前面說的多程式,那我們可以在accpet到一個請求後就fork一個子程式來處理這個客戶端的請求,這樣當accept了第二個客戶端後再fork一個子程式來處理第二個客戶端的請求,這樣問題不就解決了嗎?OK!擼一把程式碼演示一下:
<?php
$host = '0.0.0.0';
$port = 9999;
// 建立一個tcp socket
$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
// 將socket bind到IP:port上
socket_bind( $listen_socket, $host, $port );
// 開始監聽socket
socket_listen( $listen_socket );
// 進入while迴圈,不用擔心死迴圈當機,因為程式將會阻塞在下面的socket_accept()函式上
while( true ){
// 此處將會阻塞住,一直到有客戶端來連線伺服器。阻塞狀態的程式是不會佔據CPU的
// 所以你不用擔心while迴圈會將機器拖垮,不會的
$connection_socket = socket_accept( $listen_socket );
// 當accept了新的客戶端連線後,就fork出一個子程式專門處理
$pid = pcntl_fork();
// 在子程式中處理當前連線的請求業務
if( 0 == $pid ){
// 向客戶端傳送一個helloworld
$msg = "helloworld\r\n";
socket_write( $connection_socket, $msg, strlen( $msg ) );
// 休眠5秒鐘,可以用來觀察時候可以同時為多個客戶端提供服務
echo time().' : a new client'.PHP_EOL;
sleep( 5 );
socket_close( $connection_socket );
exit;
}
}
socket_close( $listen_socket );
將程式碼儲存為server.php,然後執行php server.php,客戶端依然使用telnet 127.0.0.1 9999
,只不過這次我們開啟兩個終端來執行telnet。重點觀察當第一個客戶端連線上去後,第二個客戶端時候也可以連線上去。執行結果如下:
透過接受到客戶端請求的時間戳可以看到現在伺服器可以同時為N個客戶端服務的。但是,接著想,如果先後有1萬個客戶端來請求呢?這個時候伺服器會fork出1萬個子程式來處理每個客戶端連線,這是會死人的。fork本身就是一個很浪費系統資源的系統呼叫,1W次fork足以讓系統崩潰,即便當下系統承受住了1W次fork,那麼fork出來的這1W個子程式也夠系統記憶體喝一壺了,最後是好不容易費勁fork出來的子程式在處理完畢當前客戶端後又被關閉了,下次請求還要重新fork,這本身就是一種浪費,不符合社會主義主流價值觀。如果是有人惡意攻擊,那麼系統fork的數量還會呈直線上漲一直到系統崩潰。
所以,我們就再次提出增進型解決方案。我們可以預估一下業務量,然後在服務啟動的時候就fork出固定數量的子程式,每個子程式處於無限迴圈中並阻塞在accept上,當有客戶端連線擠進來就處理客戶請求,當處理完成後僅僅關閉連線但本身並不銷燬,而是繼續等待下一個客戶端的請求。這樣,不僅避免了程式反覆fork銷燬巨大資源浪費,而且透過固定數量的子程式來保護系統不會因無限fork而崩潰。
<?php
$host = '0.0.0.0';
$port = 9999;
// 建立一個tcp socket
$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
// 將socket bind到IP:port上
socket_bind( $listen_socket, $host, $port );
// 開始監聽socket
socket_listen( $listen_socket );
// 給主程式換個名字
cli_set_process_title( 'phpserver master process' );
// 按照數量fork出固定個數子程式
for( $i = 1; $i <= 10; $i++ ){
$pid = pcntl_fork();
if( 0 == $pid ){
cli_set_process_title( 'phpserver worker process' );
while( true ){
$conn_socket = socket_accept( $listen_socket );
$msg = "helloworld\r\n";
socket_write( $conn_socket, $msg, strlen( $msg ) );
socket_close( $conn_socket );
}
}
}
// 主程式不可以退出,程式碼演示比較粗暴,為了不保證退出直接走while迴圈,休眠一秒鐘
// 實際上,主程式真正該做的應該是收集子程式pid,監控各個子程式的狀態等等
while( true ){
sleep( 1 );
}
socket_close( $connection_socket );
將檔案儲存為server.php後php server.php執行,然後再用ps -ef | grep phpserver | grep -v grep
來看下伺服器程式狀態:
可以看到master程式存在,除此之外還有10個子程式處於等待服務狀態,再同一個時刻可以同時為10個客戶端提供服務。我們透過telnet 127.0.0.1 9999來嘗試一下,執行結果如下圖:
文章來源:segmentfault.com/a/119000001622657...
本作品採用《CC 協議》,轉載必須註明作者和本文連結