PHP stream_socket_server/客户端存在本地文件访问问题。
我正在使用此脚本的修改: php:如何保存客户端套接字(未关闭),以便进一步的脚本可以检索它以发送答案? 但我无法让本地文件部分正常工作。
我试图做的本质上是通过使用文件作为中间人来在 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 (连接被拒绝) (客户端)
让我先说一下: 你确定需要Unix套接字吗?你确定proc_open()的管道不足以实现你的目标吗? proc_open() 比 Unix 套接字更容易使用。 继续前进,
警告: 不要相信 fread() 在 1 次读取所有数据,尤其是在发送大量数据(如兆字节)时,您需要某种方法来传达您的消息将有多大,这可以通过以消息长度标头开始所有消息来实现,例如小端 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);
现在,如果您只需要支持与 1 个客户端对话,只需一条消息,就可以
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!
这一次仅适用于 1 个客户端,只有一个回复/响应,但至少它正确地使用了 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(); 只要方便,看看你是否有来自任何客户的消息。 如果你无事可做,想等到你收到任何客户的消息,你可以这样做
$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() 的最后 2 个参数为 null,如果您没有获得任何新连接并且您的任何客户端都没有发生任何事情,stream_select可以无限期地阻止。(您可以设置另一个超时,例如 1 秒或其他什么,以设置超时。 null 表示"永远等待")
实际上有很多原因导致您无法实现这一目标。
- 首先,为了使事情变得更容易,请使用不以"."开头的文件,这样它就不会隐藏在您的查找器/终端中。
- 然后,请确保在每次运行脚本后删除套接字文件。 您可以在脚本中使用
unlink()
或手动执行此操作rm temp.sock
如果不这样做,则无法创建套接字服务器,因为它已经存在。
您可能认为它不起作用,但实际上它确实有效: -!preg_match('/r?nr?n/', $buffer)
这种情况会阻止缓冲区在持久脚本中输出,因为它会等待此双回车符到达套接字以打印所有内容。因此,数据可能会进入套接字并在持久脚本中读取,但不会回显到响应。
不能花太多时间在上面,但这是两个文件的一个版本。确保在发送数据之前运行持久.php.php
坚持.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.1rnrn");
fclose($conn);
break;
}
fclose($socket);
unlink('unique.sock');
}
发送数据.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);
不确定您希望在哪种上下文中使用它。但这可以帮助您了解如何使用套接字。 如果你不需要疯狂的快速性能,或者如果你更喜欢网络环境,我建议你切换和使用WebSockets。
你可以在这里找到一个很棒的图书馆:http://socketo.me/
这是现代的和面向对象的。 希望对您有所帮助。