具有本地文件访问问题的PHP stream_socket_server / client。
我正在使用此脚本的修改:php: How to save the client socket (not closed), so a further script may retrieve it to send an answer?,但是我无法使本地文件部分正常工作。
我想做的实际上是通过使用一个文件作为中间人,在PHP进程/脚本之间流式传输数据,本质上是流式传输数据。
我在打开/添加到现有文件中的现有脚本时遇到麻烦。
在stream_socket_server
端,它将工作一次(文件不存在),但是随后在任何随后的尝试运行时都会在下面抛出错误;
PHP警告:stream_socket_server():无法连接到unix://./temp.sock(未知错误)
[似乎stream_socket_server
创建文件时,它将使它成为只读文件,并在下面的代码段中提供详细信息;
rwxrwxr-x 1 xxx xxx 0 Jun 13 20:05 temp.sock
我曾尝试将权限调整为更宽容的方式,但是没有运气。
在套接字客户端,我无法获取它来打开文件,无论是否存在。
$socket = stream_socket_server('unix://./temp.sock', $errno, $errstr);
$sock = stream_socket_client('unix:///./temp.sock', $errno, $errstr);
PHP警告:stream_socket_server():无法连接到unix://./temp.sock(未知错误)(文件已存在时服务器)
PHP警告:stream_socket_client():无法连接到unix://./temp.sock(连接被拒绝)(客户端)
实际上有很多原因导致您无法实现这一目标。
unlink()
或手动rm temp.sock
在脚本中执行此操作如果不这样做,则无法创建套接字服务器,因为它已经存在。您可能认为它不起作用,但实际上它起作用:-!preg_match('/\r?\n\r?\n/', $buffer)
此条件阻止缓冲区在持久脚本中输出,因为它等待此双回车符到达套接字以打印所有内容。因此,数据可能会进入套接字并以持久脚本读取,但不会回显到响应。
不能花太多时间在上面,但这是两个文件的版本。确保先运行persist.php,然后再发送senddata.php
persist.php
<?php
$socket = stream_socket_server('unix://unique.sock', $errno, $errstr);
if (!$socket) {
echo "$errstr ($errno)<br />\n";
} else {
while ($conn = stream_socket_accept($socket)) {
$buffer = "";
while (false === strpos($buffer, 'QUIT')) {
$buffer .= fread($conn, 2046);
}
echo $buffer;
flush();
// Respond to socket client
fwrite($conn, "200 OK HTTP/1.1\r\n\r\n");
fclose($conn);
break;
}
fclose($socket);
unlink('unique.sock');
}
senddata.php
<?php
$sock = stream_socket_client('unix://unique.sock', $errno, $errstr);
if (false == $sock) {
die('error');
}
while ($data = fgets(STDIN)) {
fwrite($sock, $data);
fflush($sock);
}
fclose($sock);
不确定要在哪种情况下使用此功能。但这可以帮助您了解如何使用套接字。如果您不需要疯狂的快速性能,或者如果您更多地是在Web环境中,则建议您切换和使用WebSockets。
这里有一个很棒的图书馆:http://socketo.me/
这是现代且面向对象的。希望对您有所帮助。
让我以以下开头:您确定需要Unix套接字吗?您确定proc_open()的管道不足以达到您的目标吗? proc_open()比unix套接字更易于使用。
腔室:不要信任fread()一次性读取所有数据,尤其是
在发送更大的数据量(例如兆字节)时,您将需要某种方式来传达消息的大小,可以通过以消息长度标题(例如,little-endian uint64字符串)开头的所有消息来实现,您可以使用生成该消息/** * convert a native php int to a little-endian uint64_t (binary) string * * @param int $i * @return string */ function to_little_uint64_t(int $i): string { return pack('P', $i); }
您可以用它解析
/** * convert a (binary) string containing a little-endian uint64_t * to a native php int * * @param string $i * @return int */ function from_little_uint64_t(string $i): int { $arr = unpack('Puint64_t', $i); return $arr['uint64_t']; }
有时fread()不会在第一次调用中返回所有数据,并且您必须继续调用fread()并追加数据以获得完整的消息,这是这样的fread()循环的实现:
/** * read X bytes from $handle, * or throw an exception if that's not possible. * * @param mixed $handle * @param int $bytes * @throws \RuntimeException * @return string */ function fread_all($handle, int $bytes): string { $ret = ""; if ($bytes < 1) { // ... return $ret; } $bytes_remaining = $bytes; for (;;) { $read_now = fread($handle, $bytes_remaining); $read_now_bytes = (is_string($read_now) ? strlen($read_now) : 0); if ($read_now_bytes > 0) { $ret .= $read_now; if ($read_now_bytes === $bytes_remaining) { return $ret; } $bytes_remaining -= $read_now_bytes; } else { throw new \RuntimeException("could only read " . strlen($ret) . "/{$bytes} bytes!"); } } }
此外,当发送大量数据时,您也不能信任fwrite(),有时您需要调用fwrite,查看它写入了多少字节,然后substr()切断了实际写入的字节,然后将其余的内容发送到第二个fwrite()中,依此类推,这是fwrite()循环的一种实现,该循环一直保持写入,直到所有内容都写完为止(或者,如果无法全部写入,则会抛出异常):
/** * write the full string to $handle, * or throw a RuntimeException if that's not possible * * @param mixed $handle * @param string $data * @throws \RuntimeException */ function fwrite_all($handle, string $data): void { $len = $original_len = strlen($data); $written_total = 0; while ($len > 0) { $written_now = fwrite($handle, $data); if ($written_now === $len) { return; } if ($written_now <= 0) { throw new \RuntimeException("could only write {$written_total}/{$original_len} bytes!"); } $written_total += $written_now; $data = substr($data, $written_now); $len -= $written_now; assert($len > 0); } }
..通过这种方式,您可以像创建服务器一样
$server_errno = null; $server_errstr = ""; $server_path = __FILE__ . ".socket"; $server = stream_socket_server("unix://" . $server_path, $server_errno, $server_errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN); if (! $server || ! ! $server_errno) { throw new \RuntimeException("failed to create server {$server_path} - errno: {$server_errno} errstr: {$server_errstr}"); } register_shutdown_function(function () use (&$server_path, &$server) { // cleanup fclose($server); unlink($server_path); }); var_dump("listening on {$server_path}", $server);
现在,如果您只需要支持与一位客户交谈,只需要一条消息,一个人就可以做到
echo "waiting for connection..."; $client = stream_socket_accept($server); echo "connection!\n"; echo "reading message size header.."; stream_set_blocking($client, true); // size header is a little-endian 64-bit (8-byte) unsigned integer $size_header = fread_all($client, 8); $size_header = from_little_uint64_t($size_header); echo "got size header, message size: {$size_header}\n"; echo "reading message..."; $message = fread_all($client, $size_header); echo "message recieved: "; var_dump($message); $reply = "did you know that the hex-encoded sha1-hash of your message is " . bin2hex(hash("sha1", $message, true)) . " ?"; echo "sending reply: {$reply}\n"; fwrite_all($client, to_little_uint64_t(strlen($reply)) . $reply); echo "reply sent!\n";
然后客户端看起来像
$unix_socket_path = __DIR__ . "/unixserver.php.socket"; $conn_errno = 0; $conn_errstr = ""; echo "connecting to unix socket.."; $conn = stream_socket_client("unix://" . $unix_socket_path, $conn_errno, $conn_errstr, (float) ($timeout ?? ini_get("default_socket_timeout")), STREAM_CLIENT_CONNECT); if (! $conn || ! ! $conn_errno) { throw new \RuntimeException("unable to connect to unix socket path at {$unix_socket_path} - errno: {$conn_errno} errstr: {$conn_errstr}"); } stream_set_blocking($conn, true); echo "connected!\n"; $message = "Hello World"; echo "sending message: {$message}\n"; fwrite_all($conn, to_little_uint64_t(strlen($message)) . $message); echo "message sent! waitinf for reply.."; $reply_length_header = fread_all($conn, 8); $reply_length_header = from_little_uint64_t($reply_length_header); echo "got reply header, length: {$reply_length_header}\n"; echo "reciving reply.."; $reply = fread_all($conn, $reply_length_header); echo "recieved reply: "; var_dump($reply);
现在运行服务器,我们得到:
hans@dev2020:~/projects/misc$ php unixserver.php string(59) "listening on /home/hans/projects/misc/unixserver.php.socket" resource(5) of type (stream) waiting for connection...
然后运行客户端,
hans@dev2020:~/projects/misc$ php unixclient.php connecting to unix socket..connected! sending message: Hello World message sent! waitinf for reply..got reply header, length: 105 reciving reply..recieved reply: string(105) "did you know that the hex-encoded sha1-hash of your message is 0a4d55a8d778e5022fab701977c5d840bbc486d0 ?"
现在回望我们的服务器,我们将看到:
hans@dev2020:~/projects/misc$ php unixserver.php string(59) "listening on /home/hans/projects/misc/unixserver.php.socket" resource(5) of type (stream) waiting for connection...connection! reading message size header..got size header, message size: 11 reading message...message recieved: string(11) "Hello World" sending reply: did you know that the hex-encoded sha1-hash of your message is 0a4d55a8d778e5022fab701977c5d840bbc486d0 ? reply sent!
这一次仅适用于一个客户端,只有一个回复/响应,但至少它正确地使用了fread / fwrite循环,并确保始终发送/接收整个消息,无论它有多大全部..
让我们做一些更有趣的事情:创建一个可以与无限数量的客户端异步通信的服务器
// clients key is the client-id, and the value is the client socket $clients = []; stream_set_blocking($server, false); $check_for_client_activity = function () use (&$clients, &$server): void { $select_read_arr = $clients; $select_read_arr[] = $server; $select_except_arr = []; $empty_array = []; $activity_count = stream_select($select_read_arr, $empty_array, $empty_array, 0, 0); if ($activity_count < 1) { // no activity. return; } foreach ($select_read_arr as $sock) { if ($sock === $server) { echo "new connections! probably.."; // stream_set_blocking() has no effect on stream_socket_accept, // and stream_socket_accept will still block when the socket is non-blocking, // unless timeout is 0, but if timeout is 0 and there is no waiting connections, // php will throw PHP Warning: stream_socket_accept(): accept failed: Connection timed // so it seems using @ to make php stfu is the easiest way here $peername = ""; while ($new_connection = @stream_socket_accept($server, 0, $peername)) { socket_set_blocking($new_connection, true); $clients[] = $new_connection; echo "new client! id: " . array_key_last($clients) . " peername: {$peername}\n"; } } else { $client_id = array_search($sock, $clients, true); assert(! ! $client_id); echo "new message from client id {$client_id}\n"; try { $message_length_header = fread_all($sock, 8); $message_length_header = from_little_uint64_t($message_length_header); $message = fread_all($sock, $message_length_header); echo "message: "; var_dump($message); } catch (Throwable $ex) { echo "could not read the full message, probably means the client has been disconnected. removing client..\n"; // removing client stream_socket_shutdown($sock, STREAM_SHUT_RDWR); fclose($sock); unset($clients[$client_id]); } } } }; for (;;) { // pretend we're doing something else.. sleep(1); echo "checking for client activity again!\n"; $check_for_client_activity(); }
现在只需调用$ check_for_client_activity();只要方便,看看您是否有任何客户的来信。如果您无事可做,想等到收到任何客户的来信,可以这样做
,如果您没有获得任何新的连接并且任何客户端都没有任何反应。 (您可以设置另一个超时(例如1秒或其他时间)来设置超时。null表示“永远等待”)$empty_array = []; $select_read_arr=$clients; $select_read_arr[]=$server; $activity_count = stream_select($select_read_arr, $empty_array, $empty_array, null, null);
但是警告,stream_select()的最后两个参数为null,stream_select可以无限期阻止