在作PHP開發的過程當中,大部分咱們都在和http協議打交道,在ISO模型裏面,http屬於應用層協議,它底層會用到TCP協議。http協議很是簡單,它是一個文本協議,一個請求對應一個響應,客戶端發起一個請求,服務端響應這個請求。http是一個一問一答的對話,每次請求都得從新創建對話(這裏暫不討論Keep-Alive),若是你想經過一個請求進行屢次對話,那就是長鏈接通訊,必須使用TCP或者UDP協議。php
互聯網運行的基石是創建在一些協議上的,目前而言主要是TCP/IP協議族,大部分協議都是公開開放的,計算機遵循這些協議咱們才能通訊,固然也有一些私有協議,私有協議只有本身知道如何去解析,至關來講更安全,好比QQ所用的協議就是本身定義的。在ISO模型裏面,我們經常使用的有http、ftp、ssh、dns等,可是不經常使用的數不勝數,發明一個協議不難,難的是如何設計的更好用,並且你們都喜歡用。html
Socket並非一個協議,本質上說Socket是對 TCP/IP 協議的封裝,它是一組接口,在設計模式中,Socket 其實就是一個門面(facade)模式,它把複雜的 TCP/IP 協議族隱藏在 Socket 接口後面,對用戶來講,一組簡單的接口就是所有,讓 Socket 去組織數據,以符合指定的協議。編程
下圖展現了Socket在ISO模型裏面大概位置:json
雖然PHP的強項是處理文本,通常用來寫網頁和http接口,可是官方依然提供了Socket擴展,編譯PHP時在配置中添加--enable-sockets 配置項來啓用,若是使用apt或yum安裝,默認狀況下是已啓用。設計模式
官方文檔裏面列出了大概40個函數,可是經常使用的也就那幾個,跟着文檔,我們一塊兒來學學如何使用,首先聲明一下,本人對Socket編程並不熟悉,若有錯誤的地方,但願你們指出來。數組
我們先看一幅圖,關於TCP客戶端和服務端之間的通訊過程,我們平時寫http接口的時候並未作這麼多工做,那是客戶端給封裝好了:瀏覽器
<?php
set_time_limit(0);
$ip = '127.0.0.1';
$port = 8888;
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($sock, $ip, $port);
socket_listen($sock, 4);
echo "Server Started, Listen On $ip:$port\n";
$accept = socket_accept($sock);
socket_write($accept, "Hello World!\n", 8192);
$buf = socket_read($accept, 8192);
echo "Receive Msg: " . $buf . "\n";
socket_close($sock);
複製代碼
簡單說一下,爲便於演示,因此省略了全部的錯誤處理代碼,能夠看到分爲create、bind、listen、accept、write\read、close這幾步,看上去很是簡單!具體參數你們能夠看一下文檔!在服務端啓動以後,當收到一個請求以後,咱們首先返回了一個Hello World\n
,而後又讀取了8192個字節的數據,打印出來!最後關閉鏈接。安全
因爲這裏,咱尚未寫客戶端,因此暫時使用curl訪問一下,運行效果以下:swoole
===>服務端:網絡
===>客戶端:
從這個例子裏面咱們能夠看出來,curl發出是一個標準的http請求,實際上它的每一行後面是有\n的,在http協議裏面,這幾行文本實際上是頭(header),可是在這個例子裏面,對於咱們來講,它就是一段文本而已,服務端只是把它的內容打印出來了,並無去按照http協議去解析。雖然咱們返回了Hello World!\n
,可是這也並無按照http協議的格式去作,缺乏響應頭。我只能說curl比較強大,若是使用瀏覽器訪問的話會失敗,提示127.0.0.1 sent an invalid response
。
可是稍加改造,咱們就能夠返回一個標準的http響應:
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Server: Socket-Http\r\n";
$response .= "Content-Type: text/html\r\n";
$response .= "Content-Length: 13\r\n\r\n";
$response .= "Hello World!\n";
socket_write($accept, $response, 8192);
複製代碼
這時候若是再用瀏覽器訪問,就能夠看到 Hello World!了,可是這個服務端目前是一次性的,就是說它只能處理一次請求,而後就結束了,正常的服務端是能夠處理屢次請求的,很簡單,加一個死循環就好了!
只貼一下改動的部分,代碼以下:
while (true) {
$accept = socket_accept($sock);
$buf = socket_read($accept, 8192);
echo "Receive Msg: " . $buf . "\n";
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Server: Socket-Http\r\n";
$response .= "Content-Type: text/html\r\n";
$response .= "Content-Length: 13\r\n\r\n";
$response .= "Hello World!\n";
socket_write($accept, $response, 8192);
socket_close($accept);
}
複製代碼
搖身一變,就是一個http服務了,使用ab測了一下,併發上萬,是否是有點小激動?
然而,之因此這麼快是由於邏輯簡單,假如你在while裏面任何位置加一個 sleep(1) 你就會發現,原來這特麼是串行的,一個個執行的,並非並行,這段腳本一次只能處理一個請求!
解決這個問題方法有不少種,具體能夠參考 PHP併發IO編程之路, 看看前半段就好了,後半段是廣告!該文章總結了3種方法:最先是採用多進程多線程方式,因爲進程線程開銷大,這種方式效率最低。後來演進出master-worker模型,也就是相似如今fpm採用的方式。目前最早進的方式就是異步io多路複用,基於epoll實現的。理論上講C能實現的,PHP都能經過擴展去實現,並且PHP確實提供了相關擴展,其思想和C寫的都差很少,然而今天咱不是說高併發編程的,仍是接着說Socket吧!
以前的例子裏面咱們使用的是curl訪問的,也可使用瀏覽器或者telnet,這些工具均可以算做是客戶端,客戶端也能夠本身實現。
set_time_limit(0);
$port = 8888;
$ip = '127.0.0.1';
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
echo "Connecting $ip:$port\n";
socket_connect($sock, $ip, $port);
$input = "Hello World Socket";
socket_write($sock, $input, strlen($input));
$out = socket_read($sock, 8192);
echo "Receive Msg: $out\n";
socket_close($sock);
複製代碼
這段代碼一樣省略了錯誤處理代碼,能夠看到第一步都是create,可是第二步變成connect,而後是read\write、最後close。
具體運行效果這裏再也不展現,和curl訪問沒多大區別,可是這個客戶端也是一次性的,執行完了就結束!
接下來,咱們來寫一個基於TCP通訊的應用,這個應用很是簡單,就是加減乘除!
(1)服務端代碼:
<?php
set_time_limit(0);
$ip = '127.0.0.1';
$port = 8888;
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($sock, $ip, $port);
socket_listen($sock, 4);
echo "Server Started, Listen On $ip:$port\n";
while (true) {
$accept = socket_accept($sock);
$buf = socket_read($accept, 8192);
echo "Receive Msg: " . $buf . "\n";
$params = json_decode($buf, true);
$m = $params['m'];
$a = $params['a'];
$b = $params['b'];
switch ($m) {
case '+';
$response = $a + $b;
break;
case '-';
$response = $a - $b;
break;
case '*';
$response = $a * $b;
break;
case '/';
$response = $a / $b;
break;
default:
$response = $a + $b;
}
socket_write($accept, $response."\n", 8192);
socket_close($accept);
}
複製代碼
(2)客戶端代碼:
<?php
set_time_limit(0);
$port = 8888;
$ip = '127.0.0.1';
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
echo "Connecting $ip:$port\n";
socket_connect($sock, $ip, $port);
$input = json_encode([
'a' => 15,
'b' => 10,
'm' => '+'
]);
socket_write($sock, $input, strlen($input));
$out = socket_read($sock, 8192);
echo "Receive Msg: $out\n";
socket_close($sock);
複製代碼
在這些代碼裏面,我按照本身的需求定義了一個「協議」,我把須要運算的數和方式經過一個json數組傳輸,約定了一個格式,這個協議只有我本身清楚,因此只有我才知道怎麼調用。服務端在接受到參數以後,經過運算得出結果,而後把結果返回給客戶端。
可是這個例子還有問題,客戶端依然是一次性的,參數都被硬編碼在代碼裏面,不夠靈活,最關鍵是沒有用到TCP長鏈接的特性,咱們每次計算都得從新發起請求、從新創建鏈接,實際上,我須要的是一次鏈接,屢次對話,也就是進行屢次計算!
目前爲止,這些演示代碼都沒有複用鏈接,由於在服務端最後我close了這個鏈接,這意味着每次都是一個新的請求,若是是http服務的話尚且能夠用一下,如何去實現一個TCP長鏈接呢?
select系統調用的目的是在一段指定時間內,監聽用戶感興趣的文件描述符上的可讀、可寫和異常事件,雖然這個方式也比較低效,可是不妨瞭解一下,經過這種方式咱們能夠複用鏈接,完整的代碼以下:
<?php
set_time_limit(0);
$ip = '127.0.0.1';
$port = 8888;
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($sock, $ip, $port);
socket_listen($sock, 4);
echo "Server Started, Listen On $ip:$port\n";
socket_set_nonblock($sock);
$clients = [];
while (true) {
$rs = array_merge([$sock], $clients);
$ws = [];
$es = [];
//監聽文件描述符變更
$ready = socket_select($rs, $ws, $es, 3);
if (!$ready) {
continue;
}
if (in_array($sock, $rs)) {
$clients[] = socket_accept($sock);
$key = array_search($sock, $rs);
unset($rs[$key]);
}
foreach ($rs as $client) {
$input = socket_read($client, 8096);
if ($input == null) {
$key = array_search($client, $clients);
unset($clients[$key]);
continue;
}
echo "input: " . $input;
//解析參數,計算結果
preg_match("/(\d+)(\W)(\d+)/", $input, $params);
if (count($params) === 4) {
$a = intval($params[1]);
$b = intval($params[3]);
$m = $params[2];
} else {
continue;
}
switch ($m) {
case '+';
$result = $a + $b;
break;
case '-';
$result = $a - $b;
break;
case '*';
$result = $a * $b;
break;
case '/';
$result = $a / $b;
break;
default:
$result = $a + $b;
}
$output = "output: $result\n";
echo $output;
socket_write($client, $output, strlen($output));
}
}
複製代碼
而後我使用了telnet鏈接服務端進行操做,運行效果以下,一個基於TCP長鏈接的網絡版簡易計算器:
在這個例子,傳參的「協議」稍微有點變化,只是爲了更方便在telnet裏面交互,可是很容易理解。這裏面最關鍵是定義了一個全局變量用來存儲鏈接資源描述符,而後經過select去監聽變化,最後遍歷整個數組,讀取\寫入數據!
經過上面的簡單介紹,但願你們都對PHP Socket編程有一些瞭解和認識,其實做爲Web開發來講,不多會用到裸TCP去鏈接,大部分時候都是使用基於TCP的http協議,只有涉及到一些對響應速度要求很是高的應用,好比說遊戲、實時通訊、物聯網纔會用到,若是真的用到,不妨嘗試一下Workman、Swoole這些成熟的框架!