PHP Conference Japan 2024

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 条备注

Heretic86 at roadrunner dot com
4 年前
这是一个关于如何设置 stream_socket_server 以连接多个使用 SSL / TLS 作为传输的 WSS(wss://)上的安全 Websocket 的示例。

这在带有已注册域名和来自 LetsEncrypt 通过 Certbot 的 SSL 证书的 Windows Apache 服务器上运行。

从控制台“php server.php”运行此脚本,以及带有以下内容的 javascript html 作为客户端
socket = new WebSocket('wss://php.net: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);
}
}
?>
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 推荐的密码”,并选择适合您的密码字符串,因为并非所有 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”命令做出反应,但它与上述类似,只是您将在调用客户端流上的 stream_socket_enable_crypto() 函数之前等待“starttls”命令。

TLS 1.0 通常是最佳选择,SSLv3 不安全,SSLv2 有错误。如果您在上下文中使用 mozilla 推荐的密码列表,您将一切正常。希望这对某些人有所帮助!
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;
?>
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)

// 证书数据:
$dn = array(
"countryName" => "UK",
"stateOrProvinceName" => "Somerset",
"localityName" => "Glastonbury",
"organizationName" => "The Brain Room Limited",
"organizationalUnitName" => "PHP Documentation Team",
"commonName" => "Wez Furlong",
"emailAddress" => "[email protected]"
);

// 生成证书
$privkey = openssl_pkey_new();
$cert = openssl_csr_new($dn, $privkey);
$cert = openssl_csr_sign($cert, null, $privkey, 365);

// 生成 PEM 文件
# 可选地将密码从 'comet' 更改为您想要的任何内容,或者将其留空表示不使用密码
$pem_passphrase = 'comet';
$pem = array();
openssl_x509_export($cert, $pem[0]);
openssl_pkey_export($privkey, $pem[1], $pem_passphrase);
$pem = implode($pem);

// 保存 PEM 文件
$pemfile = './server.pem';
file_put_contents($pemfile, $pem);

$context = stream_context_create();

// local_cert 必须为 PEM 格式
stream_context_set_option($context, 'ssl', 'local_cert', $pemfile);
// 私钥的密码
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);

// 创建服务器套接字
$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 )
{
// 读取直到双 CRLF
while( !preg_match('/\r?\n\r?\n/', $buffer) )
$buffer .= fread($client, 2046);
// 响应客户端
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";
}
}

?>
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);
}
?>

谢谢
peterjb at me dot com
14 年前
我尝试将 TLS 套接字塞入现有的 TCP 程序中,遇到了很大的麻烦。在我看来,诸如 stream_socket_recvfrom 和 stream_socket_sendto 之类的函数不适用于 TLS/SSL(这对 PHP 专家来说可能是显而易见的……如果真是这样,我表示歉意,我现在有点不知所措)。

最后,我用 fread() 和 fwrite() 完成了所有 IO 操作,这解决了我的所有问题。
To Top