轉載請註明文章出處: tlanyan.me/php-review-…php
web開發一直是PHP的主戰場,也是PHP最爲被世人所熟知的一面。其實只要你願意去發掘,PHP除了作網頁在許多其餘方面也是小能手。web
本文簡要介紹PHP的Socket編程。redis
在開始以前,但願你已經知道網絡編程中的一些基本概念。好比OSI七層模型、TCP/IP四層模型;TCP中的三次握手、四次揮手等。這些概念是網絡編程的理論基礎,實踐中不必定用獲得,但能讓你把握總體脈絡,更快的定位編程中出現的問題。數據庫
再說一下Socket。咱們常說的網絡編程就是指Socket編程,它既指代實現了TCP/IP協議簇的一套網絡編程API,也指代一個客戶端與服務器的鏈接。socket是插座/接口的意思,計算機中常翻譯成「套接字」。實際中能夠簡單的認爲網絡編程與Socket編程等價,一個tcp鏈接的說法等價於一個socket。編程
PHP中有以socket
開頭的一套函數API用於Socket編程,PHP5引入「流」的抽象概念後,以stream
開頭的一套API也能夠用於網絡編程。二者的主要區別是:json
socket
系列函數相對底層,而stream
系列函數是高層的抽象。若是你想體驗原味Socket編程,用socket
開頭的API比較適合;不然建議使用流函數。有關流的知識,請參考本人以前的博文:PHP回顧之流。服務器
接下來咱們用流函數實現一個簡單的TCP客戶端和服務端。cookie
客戶端網絡編程能夠歸結爲簡單的三步:網絡
下面是客戶端的代碼,發送10條消息到服務端:session
// client.php
$host = "127.0.0.1";
$port = 8000;
$socket = @stream_socket_client("tcp://{$host}:{$port}", $errno, $errMsg);
if ($socket === false) {
throw new \RuntimeException("unable to create socket: " . $errMsg);
}
fwrite(STDOUT, "success connect to server: [{$host}:{$port}]...\n");
foreach (range(1, 10) as $i) {
if ($i % 5 === 0) {
$method = "broadcast";
} else {
$method = "echo";
}
$args = [sprintf("The %dth greeting", $i)];
$message = json_encode([
"method" => $method,
"args" => $args,
]);
fwrite(STDOUT, "\nsend to server: $message\n");
$len = @fwrite($socket, $message);
if ($len === 0) {
fwrite(STDOUT, "socket closed\n");
break;
}
$msg = @fread($socket, 4096);
if ($msg) {
fwrite(STDOUT, "receive server: $msg\n");
}
elseif (feof($socket)) {
fwrite(STDOUT, "socket closed\n");
break;
}
sleep(2);
}
fwrite(STDOUT, "close connnection...\n");
fclose($socket);
複製代碼
客戶端已經搞定,接下來看服務端。
服務端編程也很簡單,四步搞定:
因爲服務端通常是長時間運行,除非重啓或進程被殺死,極少會主動關閉服務。另外服務端通常須要長時間運行,因此應當運行在CLI模式下(短連的客戶端代碼能夠在web中使用,例如代替CURL獲取網頁內容,鏈接redis/MQ等)。
咱們簡單的將收到的消息返回客戶端(Echo服務器):
// server.php
$port = 8000;
$socket = @stream_socket_server("tcp://0.0.0.0:$port", $errno, $errMsg);
if ($socket === false) {
throw new \RuntimeException("fail to listen on port: {$port}!");
}
fwrite(STDOUT, "socket server listen on port: {$port}" . PHP_EOL);
while (true) {
$client = @stream_socket_accept($socket);
if ($client == false) {
continue;
}
fwrite(STDOUT, "client:" . (int)$client . " connnected.\n");
@fwrite($client, "Welcome aboard!\n");
while (true) {
$msg = @fread($client, 4096);
if ($msg) {
fwrite(STDOUT, "\nreceive client: $msg\n");
// echo
@fwrite($client, $msg);
} elseif (feof($client)) {
fwrite(STDOUT, "client:" . (int)$client . " disconnnect!\n");
fclose($client);
break;
}
}
}
複製代碼
先啓動服務端腳本:php server.php
, 而後打開新的窗口啓動客戶端:php client.php
。能夠看到消息被正確的發送和接收。客戶端退出後,可屢次從新運行客戶端腳本查看效果。
同時運行兩個或以上客戶端,會發現第二個起卡住,前面的客戶端退出後才繼續運行。回顧服務端代碼,能夠看到accept一個客戶端後,服務端就專心爲其服務,直到斷開才服務下一個。
同時服務多個客戶端,這纔是咱們指望的。默認狀況下socket處於阻塞模式,無數據時fread
函數會一直等待,致使程序不能抽身服務其餘客戶端。要同時服務多個客戶端,第一步是設置非阻塞模式,第二步是更改輪詢方式。流函數中的stream_set_blocking
和stream_select
兩個函數是咱們想要的。
將服務端的代碼更改以下:
// server.php
<?php
$port = 8000;
$socket = @stream_socket_server("tcp://0.0.0.0:$port", $errno, $errMsg);
if ($socket === false) {
throw new \RuntimeException("fail to listen on port: {$port}!");
}
fwrite(STDOUT, "socket server listen on port: {$port}" . PHP_EOL);
stream_set_blocking($socket, false);
$clients = [];
$changed = [];
while (true) {
checkMessage();
fwrite(STDOUT, "\nnew read message\n");
accept();
handleMessage();
}
function checkMessage() {
global $socket, $changed, $clients;
$changed = array_merge([$socket], $clients);
$write = null;
$except = null;
stream_select($changed, $write, $except, null);
}
function accept() {
global $socket, $changed, $clients;
if (!in_array($socket, $changed)) {
return;
}
while ($client = @stream_socket_accept($socket, 0)) {
$clients[] = $client;
fwrite(STDOUT, "client:" . (int)$client . " connnected.\n");
fwrite($client, "welcome aboard!");
stream_set_blocking($client, false);
$key = array_search($client, $changed);
unset($changed[$key]);
}
}
function handleMessage() {
global $changed, $clients;
foreach ($changed as $key => $client) {
while (true) {
$msg = @fread($client, 4096);
if ($msg) {
fwrite(STDOUT, "receive client " . (int)$client . " message: $msg\n");
$json = json_decode($msg, true);
if ($json) {
$method = $json["method"];
if ($method === 'echo') {
@fwrite($client, $msg);
} else {
foreach ($clients as $cl) {
@fwrite($cl, "message from " . (int)$client . ": $msg");
}
}
}
} else {
if (feof($client)) {
fwrite(STDOUT, "\nclient " . (int)$client . " closed.\n");
fclose($client);
$key = array_search($client, $clients);
unset($clients[$key]);
}
break;
}
}
}
}
複製代碼
而後啓動服務端:php server.php
,再同時啓動多個客戶端,或者用多個進程同時發送消息(需安裝pcntl
拓展):
// client.php
for ($index = 0; $index < 10; ++ $index) {
$pid = pcntl_fork();
if ($pid < 0) {
fwrite(STDERR, "fail to fork!\n");
exit;
}
if ($pid === 0) {
connectServer(); // connectServer就是上文中client.php中的代碼
exit;
}
}
// 父進程先退出,不會出現殭屍進程,忽略孤兒進程的處理
複製代碼
啓動客戶端後,能夠看到服務端正確的同時處理多個客戶端,這正是咱們期待的。
上述代碼實現了客戶端和可併發的服務端,做爲演示基本夠用。若是要投入到實踐中使用,至少有如下方面的不足:
每一個方面展開來講至少都是一篇長文。本文目的是簡要介紹PHP中的Socket編程,行文到此已經達到目的。因爲網絡協議十分繁雜,想深刻網絡編程請參閱更多權威文檔。
本文基於PHP5引入的流簡要介紹了PHP中的Socket編程,並給出了一個簡單併發服務器的實現。文中代碼僅作演示用,在生產環境中,請使用成熟的網絡框架/庫。