此篇博客是接着上篇php socekt阻塞模型PHP代碼(php socket IO阻塞方式的Server/Client)的進階,IO阻塞模型只能是同一個時刻只能由一個客戶端進行訪問,除非利用多進程或多線程才能達到多個用戶併發訪問的,因涉及到多進程和多線程,暫時跳過,
此片爲linux的IO操作的5大模型第三種模型:IO複用,而IO複用又有多種方式實現,常見的如select、poll、epoll函數。這幾個函數也會使進程阻塞,但是和阻塞I/O所不同的的,這些函數可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O函數進行檢測,直到有數據可讀或可寫時,才真正調用I/O操作函數,這些定義網上資料都很多,我這就不一一描述,如有需要可參考:socket阻塞與非阻塞,同步與異步
下面是socket IO複用 select 模型代碼PHP 的代碼描述,講述如何使用PHP代碼實現select模型。其中也對socket_select的作用,進行自我總結
select_server.php
<?php
/**
* server.php.
* User: lvfk
* Date: 2017/12/1 0001
* Time: 16:47
* Desc:
*/
set_time_limit(0);
class SelectSocketServer
{
private static $socket;
private static $timeout = 60;
private static $maxconns = 1024;
private static $connections = array();
function __construct($port)
{
global $errno, $errstr;
if ($port < 1024) {
die("Port must be a number which bigger than 1024\n");
}
$socket = socket_create_listen($port);
if (!$socket) die("Listen $port failed");
socket_set_nonblock($socket); // 非阻塞
while (true)
{
$readfds = array_merge(self::$connections, array($socket));
$writefds = array();
// 選擇一個連接,獲取讀、寫連接通道
$e = NULL;
/*
* socket_select是阻塞,有數據請求才處理,否則一直阻塞
* 此處$readfds會讀取到當前活動的連接
* 比如執行socket_select前的數據如下(描述socket的資源ID):
* $socket = Resource id #4
* $readfds = Array
* (
* [0] => Resource id #5 //客戶端1
* [1] => Resource id #4 //server綁定的端口的socket資源
* )
* 調用socket_select之後,此時有兩種情況:
* 情況一:如果是新客戶端2連接,那麼 $readfds = array([1] => Resource id #4),此時用於接收新客戶端2連接
* 情況二:如果是客戶端1(Resource id #5)發送消息,那麼$readfds = array([1] => Resource id #5),用戶接收客戶端1的數據
*
* 通過以上的描述可以看出,socket_select有兩個作用,這也是實現了IO複用
* 1、新客戶端來了,通過 Resource id #4 介紹新連接,如情況一
* 2、已有連接發送數據,那麼實時切換到當前連接,接收數據,如情況二
*/
if (socket_select($readfds, $writefds, $e, self::$timeout))
{
// 如果是當前服務端的監聽連接
if (in_array($socket, $readfds)) {
echo "socket_accept\n";
// 接受客戶端連接
$newconn = socket_accept($socket);
$i = (int) $newconn;
$reject = '';
if (count(self::$connections) >= self::$maxconns) {
$reject = "Server full, Try again later.\n";
}
// 將當前客戶端連接放入 socket_select 選擇
self::$connections[$i] = $newconn;
// 輸入的連接資源緩存容器
$writefds[$i] = $newconn;
// 連接不正常
if ($reject) {
socket_write($writefds[$i], $reject);
unset($writefds[$i]);
self::close($i);
} else {
echo "Client $i come.\n";
}
// remove the listening socket from the clients-with-data array
$key = array_search($socket, $readfds);
unset($readfds[$key]);
}
// 輪循讀通道
foreach ($readfds as $rfd) {
// 客戶端連接
$i = (int) $rfd;
// 從通道讀取
$line = @socket_read($rfd, 2048, PHP_NORMAL_READ);
if ($line === false) {
// 讀取不到內容,結束連接
echo "Connection closed on socket $i.\n";
self::close($i);
continue;
}
$tmp = substr($line, -1);
if ($tmp != "\r" && $tmp != "\n") {
// 等待更多數據
continue;
}
// 處理邏輯
$line = trim($line);
if ($line == "quit") {
echo "Client $i quit.\n";
self::close($i);
break;
}
if ($line) {
echo "Client $i >>" . $line . "\n";
//發送客戶端
socket_write($rfd, "$i=>$line\n");
}
}
// 輪循寫通道
foreach ($writefds as $wfd) {
$i = (int) $wfd;
socket_write($wfd, "Welcome Client $i!\n");
}
}
}
}
function close ($i)
{
socket_shutdown(self::$connections[$i]);
socket_close(self::$connections[$i]);
unset(self::$connections[$i]);
}
}
new SelectSocketServer(3000);
select_client.php
<?php
/**
* client.php.
* User: lvfk
* Date: 2017/12/1 0001
* Time: 17:05
* Desc:
*/
function debug ($msg)
{
error_log($msg, 3, '/tmp/socket.log');
}
if ($argv[1]) {
$socket_client = stream_socket_client('tcp://127.0.0.1:3000', $errno, $errstr, 30);
// stream_set_timeout($socket_client, 0, 100000);
if (!$socket_client) {
die("$errstr ($errno)");
} else {
$msg = trim($argv[1]);
for ($i = 0; $i < 5; $i++) {
$res = fwrite($socket_client, "$msg($i)\n");
usleep(100000);
debug(fread($socket_client, 1024)); // 將產生死鎖,因爲 fread 在阻塞模式下未讀到數據時將等待
}
fwrite($socket_client, "quit\n"); // add end token
debug(fread($socket_client, 1024));
fclose($socket_client);
}
}
else {
$phArr = array();
for ($i = 0; $i < 5; $i++) {
$phArr[$i] = popen("php ".__FILE__." '{$i}:test'", 'r');
}
foreach ($phArr as $ph) {
pclose($ph);
}
}