PHP. 将请求原封不动地转发到另一台服务器

4

服务器A正在向服务器B发送消息。我需要使用PHP脚本在它们之间插入服务器C。

所以我希望它能够像这样工作:

服务器A -> 服务器C(对数据进行一些处理并将请求转发下一步)-> 服务器B。

我需要服务器B接收完全相同的请求,就像从服务器A发送的那样。我不需要服务器C充当代理,只需按原样发送请求即可。

我尝试了类似于以下内容,但它没有起作用:

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'Server B URL');  
curl_setopt($ch, CURLOPT_HEADER, 1);  
curl_setopt($ch, CURLOPT_POST, 1);  
curl_setopt($ch, CURLOPT_VERBOSE, 1);  
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);  
curl_setopt($ch, CURLOPT_FRESH_CONNECT, 1);  
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);  
curl_setopt($ch, CURLINFO_HEADER_OUT, 1);  
curl_setopt($ch, CURLOPT_POSTFIELDS, $_POST);
$response = curl_exec($ch);

服务器B返回200,但没有响应任何操作。我无法访问它,所以不知道是什么阻止了请求。


这不是 curl 的任务,这是 socket_* 的任务。 - hanshenrik
2个回答

6
确保用curl精确地复制任何请求的细节?那不是curl的工作,而是socket_*的工作。我敢打赌有些人会说这不是PHP的工作,但是PHP确实能够以单线程的方式做到这一点。使用socket API接受来自服务器A的连接,读取请求,进行处理,按原样将请求转发到服务器B,读取来自服务器B的响应,并将该响应发送回服务器A。
示例(可以称之为ServerC.php):
<?php
declare(strict_types = 1);
if(php_sapi_name() !== 'cli'){die('this script must run in cli mode.');}
$port = 9999;
$targetIP = gethostbyname ( 'example.org' );
$targetPort = 80;
assert ( false !== filter_var ( $targetIP, FILTER_VALIDATE_IP ) );
var_dump ( $targetIP, $targetPort );

y ( ($listen = socket_create ( AF_INET, SOCK_STREAM, SOL_TCP )) );
register_shutdown_function ( function () use (&$listen) {
    socket_close ( $listen );
} );
y ( socket_bind ( $listen, '0.0.0.0', $port ) );
y ( socket_listen ( $listen ) );
y ( socket_set_block ( $listen ) );
echo 'listening for connections... ', PHP_EOL;
while ( false !== ($newconn = socket_accept ( $listen )) ) {
    echo 'new connection from ';
    socket_getpeername ( $newconn, $peername, $peerport );
    echo $peername . ':' . $peerport . PHP_EOL;
    y ( socket_set_block ( $newconn ) );
    $data = '';
    echo 'reading request... ';
    $data = socket_read_all ( $newconn, 2 );
    var_dump ( $data );
    echo 'done.' . PHP_EOL;
    // client didnt send any bytes in a while, assume we got the whole request...
    {
        // do whatever processing you need to do between requests in here.
    }
    {
        // now to forward the request.
        y ( ($targetSock = socket_create ( AF_INET, SOCK_STREAM, SOL_TCP )) );
        y ( socket_set_block ( $targetSock ) );
        y ( socket_connect ( $targetSock, $targetIP, $targetPort ) );
        echo 'connected to target. forwarding request... ';
        socket_write_all ( $targetSock, $data );
        echo 'done.', PHP_EOL;
        unset ( $data, $newdata );
        $data = '';
        echo 'reading response... ';
        $data = socket_read_all ( $targetSock, 2 );
        var_dump ( $data );
        echo 'done.', PHP_EOL;
        socket_close ( $targetSock );
    }
    echo 'sending response back to client... ';
    socket_write_all ( $newconn, $data );
    echo 'done.', PHP_EOL;
    socket_close ( $newconn );
}
function y($in) {
    if (! $in) {
        $str = hhb_return_var_dump ( socket_last_error (), socket_strerror ( socket_last_error () ) );
        throw new \Exception ( $str );
    }
    return $in;
}
function n($in) {
    if (! ! $in) {
        throw new \Exception ();
    }
    return $in;
}
function hhb_return_var_dump(): string // works like var_dump, but returns a string instead of printing it.
{
    $args = func_get_args ();
    ob_start ();
    call_user_func_array ( 'var_dump', $args );
    return ob_get_clean ();
}
function socket_write_all($sock, string $data) {
    $len = strlen ( $data );
    while ( $len > 0 ) {
        $written = socket_write ( $sock, $data );
        if ($written === false) {
            throw new RuntimeException ( 'socket_write failed. errno: ' . socket_last_error ( $sock ) . '. error: ' . socket_strerror ( socket_last_error ( $sock ) ) );
        }
        $len -= $written;
        $data = substr ( $data, $written );
    }
    return; // all data written
}
function socket_read_all($sock, int $sleep_sec = 2): string {
    $ret = '';
    while ( true ) {
        sleep ( $sleep_sec ); // ...
        $buf = '';
        $read = socket_recv ( $sock, $buf, 9999, MSG_DONTWAIT );
        if ($read === false || $read < 1) {
            return $ret;
        }
        $ret .= $buf;
    }
}

(这个脚本很慢,一次只能处理1个请求(尽管并发请求通过socket_accept被操作系统缓冲),而且是同步的。但是,如果值得优化,它可以通过socket_select&co进行优化和完全异步化)。


如果请求发送到唯一的URL而不仅仅是域名(例如example.org/test),那怎么办? - justaguy
@justaguy 这种方法与URL无关,服务器不关心域名/ URL是什么,它只是将所有内容转发。但是,它将被转发到源请求的相同URL...如果您请求proxy.com/blabla,则转发请求也将转到/blabla。 - hanshenrik
非常感谢!!! 现在我会让它变得更难一些... :) 如果我想让服务器A将所有请求发送到服务器C(例如example.com/forwarder.php),但要添加一个唯一的参数,以允许我(作为服务器C)将他的请求发送到不同的第三方服务器(例如example.com/forwarder.php?server=test将发送请求到test.com/api.php)。 - justaguy

-2

尝试更改这一行

curl_setopt($ch, CURLOPT_POSTFIELDS, $_POST);

to:

curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($_POST));

当您进行常规的cURL请求时,不能直接将数组($_POST是一个数组)作为CURLOPT_POSTFIELDS的值使用。您必须将该数组转换为postdata字符串,这正是http_build_query()所做的。例如,它会将其转换为:
["name" => "my first name", "email" => "my@email.com"]

转换为:

name=my%20first%20name&email=my@email.com

该方法不会转发GET数据、cookie数据或http头(如用户代理),也不能保证POST数据被编码相同,甚至不能保证使用相同的编码方式(例如,您无法从$_POST中确定是使用了application/x-www-form-urlencoded还是multipart/form-data,PHP会为您抽象和解码它。您的http_build_query方法始终会使用application/x-www-form-urlencoded,而不管服务器A使用了什么编码方式)。 - hanshenrik
此外,当服务器A实际上使用application/x-www-form-urlencoded进行编码时,您将无法确定/复制服务器A是否按照RFC1738、RFC3986或其他方案对数据进行编码(因为PHP会将它们都解码为相同的结果)。 - hanshenrik
此外,至少application/x-www-form-urlencoded支持多次重新定义同一变量,例如Content-Type: application/x-www-form-urlencoded foo=9&foo=8(我不知道为什么人们会想这样做,但我在实际应用中看到过,并且Firefox和Chrome都支持它)。在这种情况下,PHP将只把$_POST['foo']设置为最后一个提供的foo值,并且会丢弃任何先前的定义。这意味着当您使用http_build_query时,被覆盖的foo的先前定义不会被转发到ServerB。 - hanshenrik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接