2024 年 PHP Conference Japan

stream_socket_server

(PHP 5, PHP 7, PHP 8)

stream_socket_server建立網際網路或 Unix 網域伺服器通訊端

說明

stream_socket_server(
    字串 $address,
    整數 &$error_code = null,
    字串 &$error_message = null,
    整數 $flags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
    ?資源 $context = null
): 資源|false

在指定的 address 上建立資料流或資料包通訊端。

此函式僅建立通訊端,要開始接受連線,請使用 stream_socket_accept()

參數

address

所建立的通訊端類型由使用標準 URL 格式指定的傳輸方式決定:transport://target

對於網際網路網域通訊端 (AF_INET),例如 TCP 和 UDP,remote_socket 參數的 target 部分應包含主機名稱或 IP 位址,後接冒號和埠號。對於 Unix 網域通訊端,target 部分應指向檔案系統上的通訊端檔案。

根據環境,Unix 網域通訊端可能無法使用。可以使用 stream_get_transports() 取得可用傳輸方式的清單。有關內建傳輸方式的清單,請參閱 支援的通訊端傳輸方式清單

error_code

如果存在選用的 error_codeerror_message 參數,它們將被設定為指示在系統層級的 socket()bind()listen() 呼叫中發生的實際系統層級錯誤。如果 error_code 中返回的值為 0 且函式返回 false,則表示錯誤發生在 bind() 呼叫之前。這很可能是由於初始化通訊端時出現問題所致。請注意,error_codeerror_message 參數將始終以傳址方式傳遞。

error_message

請參閱 error_code 說明。

flags

一個位元遮罩欄位,可以設定為任何通訊端建立旗標的組合。

注意:

對於 UDP 通訊端,您必須使用 STREAM_SERVER_BIND 作為 flags 參數。

context

返回值

返回已建立的資料流,或在發生錯誤時返回 false

更新日誌

版本 說明
8.0.0 context 現在可以為 null。

範例

範例 #1 使用 TCP 伺服器通訊端

<?php
$socket
= stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr);
if (!
$socket) {
echo
"$errstr ($errno)<br />\n";
} else {
while (
$conn = stream_socket_accept($socket)) {
fwrite($conn, 'The local time is ' . date('n/j/Y g:i a') . "\n");
fclose($conn);
}
fclose($socket);
}
?>

底下的範例展示了如何作為時間伺服器,回應時間查詢,如同在 stream_socket_client() 的範例所示。

注意大多數系統需要 root 權限才能在 1024 以下的連接埠建立伺服器通訊端。

範例 #2 使用 UDP 伺服器通訊端

<?php
$socket
= stream_socket_server("udp://127.0.0.1:1113", $errno, $errstr, STREAM_SERVER_BIND);
if (!
$socket) {
die(
"$errstr ($errno)");
}

do {
$pkt = stream_socket_recvfrom($socket, 1, 0, $peer);
echo
"$peer\n";
stream_socket_sendto($socket, date("D M j H:i:s Y\r\n"), 0, $peer);
} while (
$pkt !== false);

?>

注意事項

注意指定數值 IPv6 位址(例如 fe80::1)時,必須將 IP 位址用方括號括起來 — 例如,tcp://[fe80::1]:80

參見

新增註記

使用者貢獻的註記 6 則註記

39
Heretic86 at roadrunner dot com
4 年前
這是一個如何設定 stream_socket_server 以透過 SSL/TLS 作為傳輸層,連線到多個 WSS (wss://) 安全 WebSockets 的範例。

這是在具有已註冊網域和透過 Certbot 從 LetsEncrypt 取得的 SSL 憑證的 Windows Apache 伺服器上執行的。

從控制台執行此指令碼「php server.php」,並使用以下 JavaScript HTML 作為客戶端:
socket = new WebSocket('wss://php.dev.org.tw:1234');

<?php
$host
= '192.168.1.2';
$port = 1234;
$path = 'C:/Certbot/live/php.net/';
$transport = 'tlsv1.3';
$ssl = ['ssl' => [
'local_cert' => $path . 'cert.pem', // SSL Certificate
'local_pk' => $path . 'privkey.pem', // SSL Keyfile
'disable_compression' => true, // TLS compression attack vulnerability
'verify_peer' => false, // Set this to true if acting as an SSL client
'ssltransport' => $transport, // Transport Methods such as 'tlsv1.1', tlsv1.2'
] ];
$ssl_context = stream_context_create($ssl);
$server = stream_socket_server($transport . '://' . $host . ':' . $port, $errno, $errstr, STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, $ssl_context);
if (!
$server) { die("$errstr ($errno)"); }
$clients = array($server);
$write = NULL;
$except = NULL;
while (
true) {
$changed = $clients;
stream_select($changed, $write, $except, 10);
if (
in_array($server, $changed)) {
$client = @stream_socket_accept($server);
if (!
$client){ continue; }
$clients[] = $client;
$ip = stream_socket_get_name( $client, true );
echo
"New Client connected from $ip\n";

stream_set_blocking($client, true);
$headers = fread($client, 1500);
handshake($client, $headers, $host, $port);
stream_set_blocking($client, false);

send_message($clients, mask($ip . ' connected'));
$found_socket = array_search($server, $changed);
unset(
$changed[$found_socket]);
}
foreach (
$changed as $changed_socket) {
$ip = stream_socket_get_name( $changed_socket, true );
$buffer = stream_get_contents($changed_socket);
if (
$buffer == false) {
echo
"Client Disconnected from $ip\n";
@
fclose($changed_socket);
$found_socket = array_search($changed_socket, $clients);
unset(
$clients[$found_socket]);
}
$unmasked = unmask($buffer);
if (
$unmasked != "") { echo "\nReceived a Message from $ip:\n\"$unmasked\" \n"; }
$response = mask($unmasked);
send_message($clients, $response);
}
}
fclose($server);

function
unmask($text) {
$length = @ord($text[1]) & 127;
if(
$length == 126) { $masks = substr($text, 4, 4); $data = substr($text, 8); }
elseif(
$length == 127) { $masks = substr($text, 10, 4); $data = substr($text, 14); }
else {
$masks = substr($text, 2, 4); $data = substr($text, 6); }
$text = "";
for (
$i = 0; $i < strlen($data); ++$i) { $text .= $data[$i] ^ $masks[$i % 4]; }
return
$text;
}
function
mask($text) {
$b1 = 0x80 | (0x1 & 0x0f);
$length = strlen($text);
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.$text;
}
function
handshake($client, $rcvd, $host, $port){
$headers = array();
$lines = preg_split("/\r\n/", $rcvd);
foreach(
$lines as $line)
{
$line = rtrim($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')));
//hand shaking header
$upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"WebSocket-Origin: $host\r\n" .
"WebSocket-Location: wss://$host:$port\r\n".
"Sec-WebSocket-Version: 13\r\n" .
"Sec-WebSocket-Accept:$secAccept\r\n\r\n";
fwrite($client, $upgrade);
}
function
send_message($clients, $msg){
foreach(
$clients as $changed_socket){
@
fwrite($changed_socket, $msg);
}
}
?>
30
blackmac01 at gmail dot com
9 年前
我正在編寫一個 HTTP 伺服器,我需要 SSL 支援,但要讓它與 PHP 資料流正確運作需要一些嘗試和錯誤。對於任何嘗試讓 HTTP SSL 伺服器與 stream_socket_server 一起運作的人:

1) 您的 SSL 內容需要包含 'local_cert'。如果您沒有在 local_cert 中包含您的私鑰,您還需要指定 'local_pk',這是您的 RSA 金鑰。您的金鑰和憑證應該採用 PEM 編碼,這表示 base-64。如果您的憑證具有中繼憑證,您需要按照正確的順序指定它們:您的簽名憑證、中繼憑證 1、中繼憑證 2 等。列表中的每個憑證都需要驗證其上方的憑證,但您不需要包含 SSL 簽署者提供的 CA 根憑證;它應該已經包含在客戶端的軟體中(即信任根憑證)。

您可以將您的私鑰附加到憑證檔案中,但我將我的私鑰保存在自己的檔案中。如果您使用文字檢視器檢視金鑰時看到「加密」一詞,您需要輸入正確的通行碼並指定內容「passphrase」,否則您可以省略該內容。

作為伺服器,verify_peer 無關緊要,應設定為 false(如果您作為 SSL 客戶端,則應始終為 true)。cafile 和 capath 內容對於作為 SSL/TLS 伺服器運作並非必要,但如果您使用 PHP 作為客戶端建立 SSL 連線,則需要它們。

最後,'ciphers' 內容應設定為安全加密的列表。搜尋「mozilla recommended ciphers」並選擇適合您的加密字串,因為並非所有 openssl 支援的加密都是安全的。我選擇了「intermediate」列表,它提供了高安全性和相容性。

2) 當您為 stream_socket_server() 建立綁定時,請確保選擇 tcp:// 包裝器。不要使用 ssl:// 或 tls://。除了 tcp:// 之外,任何其他方式都無法以伺服器身分正常運作,這些傳輸方式是用於 PHP 作為客戶端建立連線時使用的。

請記住,加密在 SSL 握手完成後才會開始,因此伺服器必須以非加密模式監聽新的連線,並且在交換憑證並選擇加密方式之前,加密不會開始。當新的連線到達時,您可以使用 stream_socket_accept() 接受它,然後使用 stream_socket_enable_crypto() 來啟動 SSL 工作階段。

3) 請記住,SSL 握手需要時間,而且由於 stream_socket 包裝器是高階的,並且由於它們產生的額外開銷,它們不像 socket 擴充功能那樣具有回應性。因此,您需要啟用阻斷以接受新的連線。

<在 ServerListenStream 上啟用阻斷>
newConnStream = stream_socket_accept(ServerListenStream);
<在 ServerListenStream 上停用阻斷>

<在 newConnStream 上啟用阻斷>
stream_socket_enable_crypto(newConnStream, true, STREAM_CRYPTO_METHOD_SSLv23_SERVER);
<在 newConnStream 上停用阻斷>

請注意,這主要適用於 HTTP。如果您嘗試執行 SMTP 之類的操作,則您的腳本必須對「starttls」指令做出反應,但它與上述類似,只是您需要等待「starttls」指令,然後再在客戶端的串流上呼叫 stream_socket_enable_crypto() 函式。

TLS 1.0 通常是首選方式,SSLv3 不安全,而 SSLv2 有錯誤。如果您在上下文中使用 Mozilla 建議的加密清單,則會沒問題。希望這對某些人有所幫助!
21
davidm at marketo dot com
15 年前
在某些特定情況下,您可能想要建立 AF_INET 通訊端 (UDP 或 TCP),但讓系統為您選擇未使用的連接埠。這是網際網路通訊端的標準功能,但似乎沒有說明如何使用 stream_socket_server 函式執行此操作。您可以透過為連接埠號碼選擇零來獲得此行為,例如,我的以下測試列印了「127.0.0.1:4960」。

<?php
$sock
= stream_socket_server("udp://127.0.0.1:0");
$name = stream_socket_get_name($sock);
echo
$name;
?>
13
frxstrem
14 年前
使用 OpenSSL 擴充功能,PHP 可以自動產生自簽 SSL 憑證,可用於 SSL 伺服器的基本驗證和加密(雖然我建議使用已簽名的憑證)。

我擴充了 'e at osterman dot com' 的腳本,以自動建立自簽憑證

<?php
// Hello World! SSL HTTP Server.
// Tested on PHP 5.1.2-1+b1 (cli) (built: Mar 20 2006 04:17:24)

// Certificate data:
$dn = array(
"countryName" => "UK",
"stateOrProvinceName" => "Somerset",
"localityName" => "Glastonbury",
"organizationName" => "The Brain Room Limited",
"organizationalUnitName" => "PHP Documentation Team",
"commonName" => "Wez Furlong",
"emailAddress" => "wez@example.com"
);

// Generate certificate
$privkey = openssl_pkey_new();
$cert = openssl_csr_new($dn, $privkey);
$cert = openssl_csr_sign($cert, null, $privkey, 365);

// Generate PEM file
# Optionally change the passphrase from 'comet' to whatever you want, or leave it empty for no passphrase
$pem_passphrase = 'comet';
$pem = array();
openssl_x509_export($cert, $pem[0]);
openssl_pkey_export($privkey, $pem[1], $pem_passphrase);
$pem = implode($pem);

// Save PEM file
$pemfile = './server.pem';
file_put_contents($pemfile, $pem);

$context = stream_context_create();

// local_cert must be in PEM format
stream_context_set_option($context, 'ssl', 'local_cert', $pemfile);
// Pass Phrase (password) of private key
stream_context_set_option($context, 'ssl', 'passphrase', $pem_passphrase);

stream_context_set_option($context, 'ssl', 'allow_self_signed', true);
stream_context_set_option($context, 'ssl', 'verify_peer', false);

// Create the server socket
$server = stream_socket_server('ssl://0.0.0.0:9001', $errno, $errstr, STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, $context);

while(
true)
{
$buffer = '';
print
"waiting...";
$client = stream_socket_accept($server);
print
"accepted " . stream_socket_get_name( $client, true) . "\n";
if(
$client )
{
// Read until double CRLF
while( !preg_match('/\r?\n\r?\n/', $buffer) )
$buffer .= fread($client, 2046);
// Respond to client
fwrite($client, "200 OK HTTP/1.1\r\n"
. "Connection: close\r\n"
. "Content-Type: text/html\r\n"
. "\r\n"
. "Hello World! " . microtime(true)
.
"<pre>{$buffer}</pre>");
fclose($client);
} else {
print
"error.\n";
}
}

?>
9
andrey at php dot net
20 年前
這只是一個關於如何使用此函式以及 stream_select() 來建立可接受多個連線(可以有多個客戶端連線)的伺服器的小範例
在 master 中,我們保存所有已開啟的連線。就在呼叫 stream_select 之前,我們將陣列複製到 $read,然後將其傳遞給 stream_select()。如果我們可以從至少一個通訊端讀取,$read 將包含通訊端描述元。需要 $master 才能不遺失我們已開啟連線的參考。
stream_server.php
<?php

$master
= array();
$socket = stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr);
if (!
$socket) {
echo
"$errstr ($errno)<br />\n";
} else {
$master[] = $socket;
$read = $master;
while (
1) {
$read = $master;
$mod_fd = stream_select($read, $_w = NULL, $_e = NULL, 5);
if (
$mod_fd === FALSE) {
break;
}
for (
$i = 0; $i < $mod_fd; ++$i) {
if (
$read[$i] === $socket) {
$conn = stream_socket_accept($socket);
fwrite($conn, "Hello! The time is ".date("n/j/Y g:i a")."\n");
$master[] = $conn;
} else {
$sock_data = fread($read[$i], 1024);
var_dump($sock_data);
if (
strlen($sock_data) === 0) { // connection closed
$key_to_del = array_search($read[$i], $master, TRUE);
fclose($read[$i]);
unset(
$master[$key_to_del]);
} else if (
$sock_data === FALSE) {
echo
"Something bad happened";
$key_to_del = array_search($read[$i], $master, TRUE);
unset(
$master[$key_to_del]);
} else {
echo
"The client has sent :"; var_dump($sock_data);
fwrite($read[$i], "You have sent :[".$sock_data."]\n");
fclose($read[$i]);
unset(
$master[array_search($read[$i], $master)]);
}
}
}
}
}
?>
stream_client.php
<?php
$fp
= stream_socket_client("tcp://127.0.0.1:8000", $errno, $errstr, 30);
if (!
$fp) {
echo
"$errstr ($errno)<br />\n";
} else {
fwrite($fp, "Aloha");
while (!
feof($fp)) {
var_dump(fgets($fp, 1024));
}
fclose($fp);
}
?>

謝謝
4
peterjb at me dot com
14 年前
我試圖將 TLS 通訊端塞入現有的 TCP 程式中,過程非常艱辛。對我來說,像 stream_socket_recvfrom 和 stream_socket_sendto 這樣的函式似乎無法與 TLS/SSL 一起使用(這對 PHP 專家來說可能很明顯...如果很明顯的話,很抱歉,我有點力不從心)。

最後,我使用 fread() 和 fwrite() 進行所有 I/O 操作,這解決了我所有的問題。
To Top