2024 年 PHP Conference Japan

socket_select

(PHP 4 >= 4.1.0, PHP 5, PHP 7, PHP 8)

socket_select在指定的 sockets 陣列上以指定的逾時時間執行 select() 系統呼叫

說明

socket_select(
    ?陣列 &$read,
    ?陣列 &$write,
    ?陣列 &$except,
    ?整數 $seconds,
    int $microseconds = 0
): int|false

socket_select() 接受 socket 陣列,並等待它們的狀態改變。熟悉 BSD sockets 的開發者會認出這些 socket 陣列實際上就是所謂的檔案描述符集合。會監控三個獨立的 socket 陣列。

參數

read

列在 read 陣列中的 socket 會被監控,以查看是否有可供讀取的字元(更精確地說,是查看讀取是否不會阻塞 - 特別是,socket 在檔案結尾也處於就緒狀態,在這種情況下 socket_read() 會返回一個零長度的字串)。

write

列在 write 陣列中的 socket 會被監控,以查看寫入是否不會阻塞。

except

列在 except 陣列中的 socket 會被監控異常。

seconds

secondsmicroseconds 一起組成 timeout 參數。timeoutsocket_select() 返回之前經過時間的上限。seconds 可以是零,這會導致 socket_select() 立即返回。這對於輪詢很有用。如果 secondsnull(無逾時),socket_select() 可以無限期阻塞。

microseconds

警告

在退出時,陣列會被修改以指示哪些 socket 的狀態實際上發生了變化。

您不需要將每個陣列都傳遞給 socket_select()。您可以省略它,並使用空陣列或 null 代替。也不要忘記這些陣列是*通過引用*傳遞的,並且在 socket_select() 返回後會被修改。

注意:

由於目前 Zend Engine 的限制,無法將像 null 這樣的常數修飾符直接作為參數傳遞給期望通過引用傳遞此參數的函數。請改用臨時變數或最左邊成員為臨時變數的表達式。

範例 #1 使用 nullsocket_select()

<?php
$e
= NULL;
socket_select($r, $w, $e, 0);
?>

返回值

成功時,socket_select() 返回修改後陣列中包含的 socket 數量,如果逾時在發生任何有趣的事情之前到期,則可能為零。發生錯誤時,返回 false。可以使用 socket_last_error() 擷取錯誤碼。

注意:

檢查錯誤時,請務必使用 === 運算子。由於 socket_select() 可能返回 0,因此與 == 的比較將會評估為 true

範例 #2 理解 socket_select() 的結果

<?php
$e
= NULL;
if (
false === socket_select($r, $w, $e, 0)) {
echo
"socket_select() 失敗,原因: " .
socket_strerror(socket_last_error()) . "\n";
}
?>

範例

範例 #3 socket_select() 範例

<?php
/* 準備讀取陣列 */
$read = array($socket1, $socket2);
$write = NULL;
$except = NULL;
$num_changed_sockets = socket_select($read, $write, $except, 0);

if (
$num_changed_sockets === false) {
/* 錯誤處理 */
} else if ($num_changed_sockets > 0) {
/* 至少在其中一個 socket 上發生了一些有趣的事情 */
}
?>

注意事項

注意:

請注意,某些 socket 實作需要非常小心地處理。以下是一些基本規則:

  • 您應該盡量在沒有逾時的情況下使用 socket_select()。如果沒有可用數據,您的程式不應該執行任何操作。依賴逾時的程式碼通常沒有移植性,而且難以除錯。
  • 如果您不打算在呼叫 socket_select() 後檢查其結果並做出適當的回應,則不得將任何 socket 加入任何集合中。在 socket_select() 返回後,必須檢查所有陣列中的所有 socket。任何可供寫入的 socket 都必須寫入,任何可供讀取的 socket 都必須讀取。
  • 如果您讀取/寫入返回陣列中的 socket,請注意,它們不一定會讀取/寫入您請求的全部數據量。請做好準備,即使只能讀取/寫入單個位元組。
  • 大多數 socket 實作的共同點是,使用 except 陣列捕獲的唯一例外是在 socket 上收到的超出範圍的數據。

參見

新增筆記

使用者貢獻的筆記 22 則筆記

49
vardhan ( at ) rogers ( dot ) com
19 年前
一個使用 socket_select() 管理多個連線的簡單 PHP 腳本。
使用「telnet localhost 9050」連線。它會將您透過 telnet 傳送的訊息廣播給連線到伺服器的其他使用者,有點像聊天腳本。

#!/usr/local/bin/php
<?php

$port
= 9050;

// create a streaming socket, of type TCP/IP
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

// set the option to reuse the port
socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);

// "bind" the socket to the address to "localhost", on port $port
// so this means that all connections on this port are now our resposibility to send/recv data, disconnect, etc..
socket_bind($sock, 0, $port);

// start listen for connections
socket_listen($sock);

// create a list of all the clients that will be connected to us..
// add the listening socket to this list
$clients = array($sock);

while (
true) {
// create a copy, so $clients doesn't get modified by socket_select()
$read = $clients;

// get a list of all the clients that have data to be read from
// if there are no clients with data, go to next iteration
if (socket_select($read, $write = NULL, $except = NULL, 0) < 1)
continue;

// check if there is a client trying to connect
if (in_array($sock, $read)) {
// accept the client, and add him to the $clients array
$clients[] = $newsock = socket_accept($sock);

// send the client a welcome message
socket_write($newsock, "no noobs, but ill make an exception :)\n".
"There are ".(count($clients) - 1)." client(s) connected to the server\n");

socket_getpeername($newsock, $ip);
echo
"New client connected: {$ip}\n";

// remove the listening socket from the clients-with-data array
$key = array_search($sock, $read);
unset(
$read[$key]);
}

// loop through all the clients that have data to read from
foreach ($read as $read_sock) {
// read until newline or 1024 bytes
// socket_read while show errors when the client is disconnected, so silence the error messages
$data = @socket_read($read_sock, 1024, PHP_NORMAL_READ);

// check if the client is disconnected
if ($data === false) {
// remove client for $clients array
$key = array_search($read_sock, $clients);
unset(
$clients[$key]);
echo
"client disconnected.\n";
// continue to the next client to read from, if any
continue;
}

// trim off the trailing/beginning white spaces
$data = trim($data);

// check if there is any data after trimming off the spaces
if (!empty($data)) {

// send this to all the clients in the $clients array (except the first one, which is a listening socket)
foreach ($clients as $send_sock) {

// if its the listening sock or the client that we got the message from, go to the next one in the list
if ($send_sock == $sock || $send_sock == $read_sock)
continue;

// write the message to the client -- add a newline character to the end of the message
socket_write($send_sock, $data."\n");

}
// end of broadcast foreach

}

}
// end of reading foreach
}

// close the listening socket
socket_close($sock);
?>
8
malcolm.murphy
8 年前
[Viorel] 上面有提到這一點,但我認為有必要再重複一次……

[vardhan ( at ) rogers ( dot ) com] 提供的範例,雖然在其他方面很出色,但它使用 int(0) 作為 $tv_sec 的值,這將導致迭代以最快的速度循環,從而耗盡所有可用的 CPU 時間。

*** 理想情況下,$tv_sec 應該始終設定為 NULL ***,尤其是在迴圈中使用 socket_select 時。如果您必須暫時停止監聽事件以執行其他任務,則逾時應該設定得越高越好,以減少 CPU 負擔(另一個筆記建議透過設定較低的 $tv_usec 值來防止 CPU 使用率達到 100%,這只會稍微減輕問題,但並不能解決問題)。

將逾時設定為明確的 null 值基本上與將其設定為無限相同。腳本只會在每次發生事件時執行一次 while 迴圈。
2
Richard Neill
20 年前
使用 socket_select 監聽一組 socket 的輸入,然後使用 PHP_NORMAL_READ 進行 socket_read() 可能不是一個好主意。

雖然這看起來很理想,但如果有人傳送給您格式錯誤的輸入,缺少尾隨的 \n \r,您的程式可能會永久阻塞。猜猜我是怎麼發現的。
1
jean at briskula dot si
13 年前
正如已經說過的,有些客戶端需要 \0 字元來結束傳輸,例如 Flash 的 XMLSocket。

您還應該準備好讀取比您請求的更少的資料。

以下是一個 socket 緩衝區的範例 - 它是一個陣列,其中 socket 資源作為鍵,值則是一個包含時間戳記和接收資料的陣列。

我發現傳送資料的最佳做法是在資料後加上換行符和零字元 (\n\0),因為您可能會遇到不同類型的客戶端,它們從 socket 讀取資料的行為不同。有些需要 \n 來觸發事件,有些需要 \0。

對於接收資料,有時您會收到分割的資料 - 這可能是因為緩衝區已滿(在我的範例中是 8192 位元組),或者它只是在較低層次的傳輸過程中損壞了。

有時您可以一次讀取兩條訊息,但它們之間有一個零字元,因此您可以使用 preg_split() 來分割訊息。第二條訊息可能不完整,因此您將其新增到緩衝區中。

<?php
const message_delimiter = "\n\0";

/*
* Clear socket buffers older than 1 hour
*/
function clear_buffer() {
foreach(
$this->buffer as $key=>$val) {
if(
time() - $val['ts'] > 3600) {
unset(
$this->buffer[$key]);
}
}
}

/*
* Add data to a buffer
*/
function buffer_add($sock,$data) {
if(!isset(
$this->buffer[$sock])) {
$this->buffer[$sock]['data'] = '';
}

$this->buffer[$sock]['data'] .= $data;
$this->buffer[$sock]['ts'] = time();
}

function
buffer_get($sock) {
// split buffer by the end of string
$lines = preg_split('/\0/',$this->buffer[$sock]['data']);

// reset buffer to the last line of input
// if the buffer was sent completely, the last line of input should be
// an empty string
$this->buffer[$sock]['data'] = trim($lines[count($lines)-1]);

if(!empty(
$this->buffer[$sock]['data'])) {
debug("buffer is not empty for $sock, len: ".strlen($this->buffer[$sock]['data']));
}

// remove the last line of input (incomplete data)
// parse any complete data
unset($lines[count($lines)-1]);

// return only the fully sent data
return $lines;
}

function
read(&$sock,$len=8192,$flag=MSG_DONTWAIT) {
$lines = array();

$this->clear_buffer();

$bytes_read = @socket_recv($sock,$read_data,$len,$flag);

if (
$bytes_read === false || $bytes_read == 0) {
return
false;
} else {
debug("recv: $read_data");
$this->buffer_add($sock,$read_data);
return
$this->buffer_get($sock);
}
}

/*
* Write to a socket
* add a newline and null character at the end
* some clients don't read until new line is recieved
*
* try to send the rest of the data if it gets truncated
*/
function write(&$sock,$msg) {
$msg = $msg.self::message_delimiter;
$length = strlen($msg);
while(
true) {
$sent = @socket_write($sock,$msg,$length);
if(
$sent <= 0) {
return
false;
}
if(
$sent < $length) {
$msg = substr($msg, $sent);
$length -= $sent;
debug("Message truncated: Resending: $msg");
} else {
return
true;
}
}
return
false;
}
?>
1
danny at dansheps dot com
16 年前
補充一點。由於筆記中包含的資訊有些過時。現在看來鍵值是被保留的。

所以,如果您需要知道哪些鍵值需要處理,並且像我一樣認為它沒有保留鍵值。那麼它確實保留了。
2
calimero dot NOSPAM at NOSPAM dot creatixnet dot com
21 年前
請注意,timeout 參數會對腳本的 CPU 使用率產生重要的副作用。

將 timeout 設為 0 將會使 CPU 不斷循環,沒有時間休息並處理系統上其他正在執行的程序,導致腳本執行期間系統負載大幅增加。

我個人會將此參數設為 15 毫秒。這樣可以確保良好的監聽頻率,同時讓系統負載保持暢通。

範例
$read = array($ListeningSocket);
$num_changed_sockets = socket_select($read, $write = NULL, $except = NULL, 0, 10);

希望以上說明有所幫助。
2
antti r
8 年前
截至 2015 年 12 月以及 PHP 5.3.3,socket_select() 的最大 socket 數量仍然是 1024,而且無法透過程序檔案限制 (bash ulimit -n) 提高。需要重新編譯 PHP 才能提高限制。或者,可以選擇使用 fork 或多個二進制檔案。
2
John
14 年前
文件中描述 socket_select() 如何處理被輪詢讀取的 socket 的方式相當晦澀難懂。

文件說明它會檢查讀取是否不會「阻塞」,但 socket_select() 的整體描述卻說它會檢查阻塞狀態的變化。很遺憾,這兩者互相矛盾。

如果 socket 的緩衝區中已有數據,則在該 socket 上呼叫 socket_select() 將永遠不會返回 (假設 timeout 為 null),並且會永遠阻塞。 :-( 這是因為阻塞狀態不會改變。它只會保持「非阻塞」狀態。

切記不要對可能已有可用數據的 socket 執行 select()。

一個例子...
<?php
//... $socket 已存在...
$done = false;
$n = 0;
do{
$tmp = 0;
$r = $w = $e = array();
$r = array($socket);
socket_select($r,$w,$e,null);
$n = socket_recv($socket, $tmp, 1024, 0);

//$done = true; //某些條件判斷我們已完成讀取...
}while(!$done);
?>
這可能無法正常運作... socket_select() 總是被呼叫... 但輸入緩衝區中可能已有數據。

我們需要確保上次讀取時沒有讀到任何東西... (緩衝區為空)
<?php
// ... $socket 已存在...
$done = false;
$n = 0;
do {
$tmp = 0;
$r = $w = $e = array();
$r = array($socket);
if (
$n === 0) socket_select($r, $w, $e, null);
$n = socket_recv($socket, $tmp, 1024, 0);

// $done = true; //某些條件判斷讀取完成...
} while (!$done);
?>
2
Viorel
11 年前
你可能想要使用

// 取得所有有資料可供讀取的客戶端列表
// 如果沒有客戶端有資料,則進入下一個迭代
if (socket_select($read, $write = NULL, $except = NULL, NULL) < 1)
continue;

來取代
if (socket_select($read, $write = NULL, $except = NULL, 0) < 1)
continue;
這會讓你的 CPU 使用率達到 100%(如果沒有任何事情要做,則立即返回)
0
simon dot riget at gmail dot com
10 年前
一個非常簡單的用 PHP 寫的 HTTP 網頁伺服器
在 shell 中使用 php <檔案名稱> 執行它,並在瀏覽器中使用 <伺服器位址>:8080/test 進行測試

<?php
// Reduce the amount of warnings displayed
error_reporting(E_ALL ^ E_NOTICE);

// Set up socket for listening
$host_socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if(!
$host_socket) die("Failed to start event server. socket_create: ". socket_strerror(socket_last_error())."\n");

// set the option to reuse the port
if(! socket_set_option($host_socket, SOL_SOCKET, SO_REUSEADDR, 1) )
die(
"Failed to start event server. socket_set_option: ". socket_strerror(socket_last_error())."\n");

// bind host socket to localhost or 0.0.0.0 on port 8080
if(! socket_bind($host_socket,"0.0.0.0",8080) )
die(
"Failed to start event server. socket_bind: ". socket_strerror(socket_last_error())."\n");

// start listening for connections
if(! socket_listen($host_socket,10) )
die(
"Failed to start event server. socket_listen: ".socket_strerror(socket_last_error())."\n");

while (
true) {
// Make list of sockets to listen for changes in, including host
$read = array($host_socket);

// get a list of all the clients that have data to be read from
$ready=@socket_select($read, $write = NULL, $except = NULL,0);
if (
$ready=== false)
die(
"Failed to listen for clients: ". socket_strerror(socket_last_error()));

// a client request service
elseif($ready>0){
// accept new client
$newsocket = socket_accept($host_socket);

// Read from socket
$input = socket_read($newsocket, 1024);
if(
$input){
unset(
$client_header);
// Read headers; Split into safe lines
$line=explode("\n",preg_replace('/[^A-Za-z0-9\-+\n :;=%*?.,\/_]/','',substr($input,0,2000)));
// Split request line into its parts
list($client_header["method"],$client_header["url"],$client_header["protocol"])=explode(" ",$line[0]);
// Remove the request line again.
unset($line[0]);
// Make key=value array of headers
foreach($line as $l){
list(
$key,$val)=explode(": ",$l);
if(
$key) $client_header[strtolower($key)]=$val;
}
// Get IP of client
socket_getpeername($newsocket, $client_header['ip']);

// Decode url
$client_header+=(array)parse_url($client_header['url']);
parse_str($client_header['query'],$client_header['arg']);

print_r($client_header);

// Serve file
if(strpos($client_header['path'],".html") && file_exists(__DIR__.$client_header['path'])){
echo
"Sending a HTML page to client\n";
socket_write($newsocket,"$client_header[protocol] 200 OK\r\n");
socket_write($newsocket,"Content-type: text/html; charset=utf-8\r\n\r\n");
socket_write($newsocket,file_get_contents(__DIR__.$client_header['path'])."\r\n\r\n");
socket_close($newsocket);
}elseif(
$client_header['path']=="/test"){
echo
"Sending test HTML page to client\n";
socket_write($newsocket,"<!DOCTYPE HTML><html><head><html><body><h1>Its working!</h1>Have fun\r\n");
socket_write($newsocket,"<pre>Request header: ". print_r($client_header,true) . "</pre>\r\n");
socket_write($newsocket,"</body></html>\r\n\r\n");
socket_close($newsocket);
}else{
echo
"$client_header[protocol] 404 Not Found\r\n";
socket_write($newsocket,"$client_header[protocol] 404 Not Found\r\n\r\n");
socket_close($newsocket);
}
}
}
}
socket_close($host_socket);
?>
0
magemerlin at list dot ru
14 年前
如果您使用 Flash 客戶端,您應該了解一些特殊的功能

1) 當客戶端連接到伺服器時,它會向您發送 "<policy-file-request/>".\0 字串。伺服器應該回覆一個 XML 策略檔案,然後斷開與該客戶端的連接。程式碼類似於

if ('<policy-file-request/>'==substr($data, 0, 22))
{
echo "policy requset.\n";
flush();ob_flush();
$msg = '<'.'?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-access-from domain="*" to-ports="*" />
</cross-domain-policy>'."\0";
echo "回覆客戶端 (crossdomain.xml) ... ";
flush();ob_flush();
socket_write($read_sock, $msg, strlen($msg));
echo "OK\n";
flush();ob_flush();

echo "關閉中 ... ";
flush();ob_flush();
socket_close($read_sock);
echo "OK\n";
flush();ob_flush();
}
else
{
// 這裡是與客戶端的正常 I/O 操作
}

2) 每個輸出到客戶端的內容都應該以 "\0" 結尾(如果在 Flash 客戶端中使用 XMLSocket) - 否則 Flash 將不會產生 onData 事件

俄文範例在這裡 - http://www.flasher.ru/forum/showpost.php?p=901346&postcount=7
0
eidberger at jakota dot de
15 年前
剛注意到,使用 UDP 時必須迴圈執行 socket_select() 才能取得所有佇列中的封包

<?php
while (socket_select ($aRead, $aWrite, $aExcept, 1) > 0) {
foreach (
$aReadUdp as $oSocket) {
$this->clientReadUdp ($oSocket);
}
}
?>

這很重要,因為每次在 UDP 上呼叫 socket_select() 只會返回一個結果。但可能會有 10,000 個結果排隊,如果您的周轉時間太慢(伺服器忙碌、其他程式休眠等),您將永遠無法近乎即時地處理所有結果。
0
qartis at gmail dot com
17 年前
關於 vardhan ( at ) rogers ( dot ) com 發布的程式碼,看起來在以下這一行
if (socket_select($read, $write = NULL, $except = NULL, 0) < 1)
timeout 參數意外地被設為 0,而不是 NULL。這表示 select 呼叫會立即返回,而不是無限期阻塞。

將 socket_select 行更改為以下內容即可獲得巨大的成功
if (socket_select($read, $write = NULL, $except = NULL, NULL) < 1)
0
匿名
17 年前
如果您想使用簡單的小數值作為 timeout

<?php
socket_select
(..., floor($timeout), ceil($timeout*1000000));
?>
1
julian dot haupt at gmx dot de
22 年前
您好,
我剛製作了一個類似於 Perl 的 IO::Select 的類別,以便讓 socket 選擇變得非常容易

您的腳本應該看起來像這樣

<?php
$server
= new Server;
$client = new Client;

for (;;) {
foreach (
$select->can_read(0) as $socket) {

if (
$socket == $client->socket) {
// 新的客戶端 Socket
$select->add(socket_accept($client->socket));
}
else {
// $socket 上有東西要讀取
}
}
}
?>

您當然應該實作一些例程程序來偵測損壞的 socket 並將它們從 select 物件中移除。

您也可以執行輸出緩衝,並在主迴圈中檢查準備寫入的 socket

<?php
類別 select {
var
$sockets;

函式
select($sockets) {

$this->sockets = 陣列();

foreach (
$sockets as $socket) {
$this->add($socket);
}
}

函式
add($add_socket) {
array_push($this->sockets,$add_socket);
}

函式
remove($remove_socket) {
$sockets = 陣列();

foreach (
$this->sockets as $socket) {
if(
$remove_socket != $socket)
$sockets[] = $socket;
}

$this->sockets = $sockets;
}

函式
can_read($timeout) {
$read = $this->sockets;
socket_select($read,$write = NULL,$except = NULL,$timeout);
return
$read;
}

函式
can_write($timeout) {
$write = $this->sockets;
socket_select($read = NULL,$write,$except = NULL,$timeout);
return
$write;
}
}
?>
0
Whosawhatsis at that google email thingy
18 年前
另一個解決金鑰無法保留問題的方法是使用額外的陣列來查找套接字,該陣列使用其資源識別碼作為金鑰。在某些情況下,可以使用 array_flip() 來取得,但如果每個套接字都與一個物件相關聯,則特別有用。在這種情況下,您可以讓物件的建構函式使用其套接字資源識別碼作為金鑰,將指向自身的指標添加到查找陣列中,並使用以下程式碼為 socket_select() 返回的每個套接字相關聯的物件執行讀取方法

<?php
socket_select
($reads, $writes, $excepts, 0);

foreach (
$sockets as $socket) {
$lookuparray[$socket]->read();
}
?>
0
crimson at NOSPAMtechnologist dot com
19 年前
請注意,陣列經過此函式處理後**不會保留鍵值** (PHP 4.3.2)

執行前
陣列
(
[Client_Socket] => 資源 id #6
[Server_Socket] => 資源 id #9
)

執行後
陣列
(
[0] => 資源 id #6
[1] => 資源 id #9
)

如果鍵值能保留下來,就能輕鬆判斷要從哪個串流接收資料,但現在你必須使用一些技巧性的 foreach 迴圈來判斷要檢查哪些通訊端。
0
drenintell
19 年前
這是我先前於 2005 年 4 月 28 日上午 10:19 發表的文章的續篇,網址如下:
http://ca3.php.net/manual/en/function.socket-select.php

網址如下:(連結分為兩部分)

'http://gtkphp.org/php_socket_select_hangs
_explanation_and_solution.html'
-1
ludvig dot ericson at gmail dot com
19 年前
關於以下的評論,不,它不會保留鍵值,這是一個系統呼叫,我認為要保留鍵值相當困難。

此外,使用 socket_select 時,應該把它視為一個使用者輸入的陣列,你不知道你傳入了什麼。

<?php
$reads
= $clients;
$reads[] = $server;

socket_select($reads);

foreach (
$reads as $read) {
/* 執行一些操作 */
}
?>
-1
juanfe0245 at gmail dot com
4 年前
<?php
//Creamos un socket de transmisión de tipo TCP / IP.
$servidorSocket=socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
//Configuramos la opción para reutilizar el puerto.
socket_set_option($servidorSocket,SOL_SOCKET,SO_REUSEADDR,1);
/*
Vinculamos el socket a nuestro puerto y dirección IP.
Ahora, todas las conecciones en este puerto son nuestro responsabilidad para recibir y enviar datos.
*/
socket_bind($servidorSocket,'localhost',9000);
//Comenzamos a escuchar conexiones.
socket_listen($servidorSocket);
/*
Creamos un arreglo con todos los clientes que se conectarán a nuestro servidor.
Añadimos nuestro socket servidor al arreglo de clientes.
*/
$clientes=[$servidorSocket];
$null=null;
while(
true){
//Creamos una copia de $clientes, debido a que $clientes no se modifica por la función socket_select().
$nuevoClienteLector=$clientes;
//Obtenemos una lista de todos los clientes que tienen datos para leer.
socket_select($nuevoClienteLector,$null,$null,10);
//Verificamos si hay un cliente tratando de conectarse.
if(in_array($servidorSocket,$nuevoClienteLector)){
//Aceptamos el cliente y lo añadimos al arreglo de $clientes.
$nuevoCliente=socket_accept($servidorSocket);
echo
"Socket aceptado.\n";
$clientes[]=$nuevoCliente;
//Obtenemos el encabezado del cliente y realizamos la comprobación del handshake.
$encabezado=socket_read($nuevoCliente,1024);
handshake($nuevoCliente,$encabezado);
//Eliminamos el cliente del arreglo $nuevoClienteLector.
$nuevoClienteIndex=array_search($servidorSocket,$nuevoClienteLector);
unset(
$nuevoClienteLector[$nuevoClienteIndex]);
}
//Recorremos todos los clientes que tienen datos por leer.
foreach($nuevoClienteLector as $cliente){
//Recibimos información del cliente conectado.
while(socket_recv($cliente,$datosCliente,1024,0)>=1){
//Decodificamos el mensaje que viene en bytes.
$mensaje=decodificar($datosCliente);
//Enviamos el mensaje a todos los sockets conectados.
enviar($clientes,$mensaje);
echo
$mensaje."\n";
break
2;
}
//Verificamos si el cliente esta desconectado.
$datosCliente=@socket_read($cliente,1024,PHP_NORMAL_READ);
if(
$datosCliente===false){
$clienteIndex=array_search($cliente,$clientes);
//Eliminamos el cliente del arreglo $clientes.
unset($clientes[$clienteIndex]);
echo
"Cliente desconectado.\n";
}
}
}
function
enviar($clientes,$mensaje){
$mensaje=codificar($mensaje);
foreach(
$clientes as $cliente){
@
socket_write($cliente,$mensaje,strlen($mensaje));
}
}
socket_close($servidorSocket);
function
codificar($socketData){
$b1 = 0x80 | (0x1 & 0x0f);
$length = strlen($socketData);

if(
$length <= 125)
$header = pack('CC', $b1, $length);
elseif(
$length > 125 && $length < 65536)
$header = pack('CCn', $b1, 126, $length);
elseif(
$length >= 65536)
$header = pack('CCNN', $b1, 127, $length);
return
$header.$socketData;
}
function
decodificar($socketData) {
$length = ord($socketData[1]) & 127;
if(
$length == 126) {
$masks = substr($socketData, 4, 4);
$data = substr($socketData, 8);
}
elseif(
$length == 127) {
$masks = substr($socketData, 10, 4);
$data = substr($socketData, 14);
}
else {
$masks = substr($socketData, 2, 4);
$data = substr($socketData, 6);
}
$socketData = "";
for (
$i = 0; $i < strlen($data); ++$i) {
$socketData .= $data[$i] ^ $masks[$i%4];
}
return
$socketData;
}
function
handshake($client_socket_resource,$received_header) {
$headers = array();
$lines = preg_split("/\r\n/", $received_header);
foreach(
$lines as $line){
$line = chop($line);
if(
preg_match('/\A(\S+): (.*)\z/', $line, $matches)) $headers[$matches[1]] = $matches[2];
}
$secKey = $headers['Sec-WebSocket-Key'];
$secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$buffer = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept:$secAccept\r\n\r\n";
socket_write($client_socket_resource,$buffer,strlen($buffer));
}
-2
renmengyang567 at gmail dot com
5 年前
<說明>
一個簡單的 socket-select 案例 ^_^

<?php
$sock
= socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp'));
socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($sock, '127.0.0.1',5000);
socket_listen($sock,1024);
$all_sock[(int)$sock] = $sock;

while (
true) {
$read = $write = $except = [];
$tv_sec = 5;
$tv_usec = 5000;
$read = $all_sock;

$changed = socket_select($read, $write, $except, $tv_sec, $tv_sec);

if (
false == $changed)
print
"[錯誤]socket_select() 失敗
("
.socket_strerror(socket_last_error()) . ")\n";

if (
$changed < 1)
continue;

//var_dump($read);
//sleep(2);
foreach ($read as $rsock) {
// 伺服器端連線
if ($rsock === $sock) {
$client = socket_accept($sock);
$all_sock[(int)$client] = $client;

} else {
//客戶端連線
$msg= socket_read($rsock, 400,PHP_NORMAL_READ);
if (
$msg !== '') {
var_dump($msg);
}
}
}
}
socket_close($sock);
-5
daveb at optusnet dot com dot au
22 年前
如果您之前沒有做過任何網路程式設計,PHP 的 socket_select() 可能看起來有點奇怪。我寫了一個簡單的 php「派對線」腳本來示範在 http://dave.dapond.com/socketselect.php.txt 中使用 select 的多重 socket 用法。
To Top