reactphp / reactphp

Event-driven, non-blocking I/O with PHP.

Home Page:https://reactphp.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Same data in "on data" while cycle write

Tick007 opened this issue · comments

Hello!
I've got a server, that responds me very quickly. So i've made folowing code to ask server in cycle. Problem is, that no matter what server answer, I always get same $data in $conn->on('data', ....
The only way for me to make it work as i want - is to include "$connector2->connect(" in cycle, thus to re-establish connection. But that is too bad decision.

Request frames are located in the $arr, and they are not equal - i checked it on my server. It receives different requests, sends different answers, but $data is always same - equal to first answer.

What do i do wrong ?

$loop2 = React\EventLoop\Factory::create();
$connector2 = new React\Socket\Connector($loop2);

$cps=null;


echo '100 request'."\n\r";
$connector2->connect($dstip . ':' . $dstport)->then(
    function (React\Socket\ConnectionInterface $conn) use ($loop2, &$arr, &$cps) {
        foreach ($arr as $num => $frame)  {
            //time_nanosleep(0, 100000000);
            $conn->write($frame);
            $conn->on('data', function ($data) use ($conn, $loop2,  $frame, &$cps) {

                $cpr = new ContProtocol;
                $cpr->readMessage($data);
              
                $cps[$cpr->hdr['UnitNum'][2]] = $cpr;
                print_r($cpr->hdr['UnitNum'][2]);
                
                echo "\n\r";
                time_nanosleep(0, 100000000);
                
                $loop2->stop();
                        
            }, function (Exception $exception) use ($loop2) {
                echo "Cannot data connect to server: " . $exception->getMessage();
                $loop2->stop();
            });
        }
    }
);
$loop2->run();

[edit by @clue: code style]

@Tick007 Welcome to @reactphp! 👋

I agree ReactPHP can be confusing when you're starting out first, so let's simplify your example a bit:

$conn->write($frame);

$conn->on('data', function ($data) {
    $cpr = new ContProtocol;
    $cpr->readMessage($data);
});

$conn->on('data', function ($data) {
    $cpr = new ContProtocol;
    $cpr->readMessage($data);
});

This will send a single message ($frame) and will wait for the data twice. This means that every time your connection receives some data, both data handlers will be invoked with the same data from the stream.

The way I understand your problem, you only want to attach a single data handler. Then, parse this as a continuous stream with some delimiter between individual messages. For instance, you can use a newline delimiter between message so each single line can be read as a JSON structure with https://github.com/clue/reactphp-ndjson.

The same logic also applies to binary protocols (look up message framing for more details). If you can tell us more about the protocol you're using, we might be able to help you.

Additionally, the time_nanosleep() function must not be used in a truly non-blocking application. I suppose this was only left here for testing purposes, but keep in mind that this will block your entire loop. It looks like this can be removed, but if you want to "wait", use the $loop->addTimer() method instead.

I'll assume this is resolved and will close this for now, please feel free to report back otherwise 👍

Hello clue, thank you for your answer.
Doesn't $loop2->stop(); should stop wait for new data receiving on open socket connection ?
So my goal was to make analog of following code:
$pop_conn = socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp')); socket_connect ( $pop_conn , $dstip, $dstport ); foreach ($arr as $num => $frame) { socket_write($pop_conn, $frame ); time_nanosleep(0, 001000000); $data = socket_read ($pop_conn, 4048); $cpr = new ContProtocol; $cpr->readMessage($data); $cps[$cpr->hdr['UnitNum'][2]] = $cpr; } socket_close ($pop_conn);
ContProtocol, that I use - is some binary protocol, is not JSON. It has it own HEADER and END, and it is not very comfortable to separate single frame from common stream. Much easer to receive individual messages with single frame. Code above do that job. My project is some simulator, where one part receives binary data, repack it to json (with nessesary aditions) and sends to websocket. Websoket works fine, thanks, but another part is expiriences some trouble :)

@Tick007 I understand where you're coming from. The problem with streaming protocols is this: If you send "hello" and "world" as two "messages", there's no guarantee the receives sees these exact message boundaries, it may as well receive "helloworld" as a single message or "h" and "elloworld" or any combination thereof. That's why you need to employ message framing to avoid this situation.

This is nothing specific to this project, but how the underlying transport mechanisms work (for good reason).

You can also introduce a small delay between each message, which makes it more likely you'll receive each message after another, but depending on your load and/or message size, it's not something you can rely on and you're back to square one. Even if it does work, it's effectively limiting your throughput and increasing latency.

If your protocol is strictly following request/response semantics and you don't want to take advantage of pipelining (sending multiple request messages at once without having to wait for the first response), you may also design this to be strictly sequential. In this case, you can send a single request message, wait for the response and only then send the next request message. This guarantees you're receiving full messages between each step, but you're missing out on the huge I/O improvements pipelining can give you 👍

Looks like i got you:
` $loop2 = React\EventLoop\Factory::create();
$connector2 = new React\Socket\Connector($loop2);
$cps=null;
echo '100 request'."\n\r";
$connector2->connect($dstip . ':' . $dstport)->then(
function (React\Socket\ConnectionInterface $conn) use ($loop2, &$arr, &$cps) {

                $conn->on('data', function ($data) use ($conn, $loop2, &$cps) {
                    
                    $cpr = new ContProtocol;
                    $cpr->readMessage($data);
                    $cps[$cpr->hdr['UnitNum'][2]] = $cpr;
                    //echo mb_strlen($data);
                    //echo "\n\r";
                }, function (Exception $exception) use ($loop2) {
                    echo "Cannot data connect to server: " . $exception->getMessage();
                    $loop2->stop();
                });
                
                
                foreach ($arr as $num => $frame)  {
                    $loop2->addTimer(0.1, function () use ($conn, $frame) {
                        $conn->write($frame);
                    });
                }
                
                $loop2->addTimer(1, function () use ($loop2) {
                    $loop2->stop();
                 });
            });
        $loop2->run();`

Works way faster than socket_creatr etc...
Thank you.

May be you should add this to docs....

Happy to hear you're making some progress!

foreach ($arr as $num => $frame)  {
    $loop2->addTimer(0.1, function () use ($conn, $frame) {
        $conn->write($frame);
    });
}

Note that this piece of code will probably not work the way you may expect it to work. Assuming you have 100 entries in your array, it will start 100 timers which all trigger at around the same time in 100ms. This means a single timer would pretty much do the same thing. I would even question if this timer is needed at all (depends on your use case of course)?

Additionally, as per https://github.com/reactphp/event-loop#addtimer there is no guarantee that timers that are scheduled to trigger at the same time will actually execute in order. This means the writing the second chunk might as well happen before writing the first chunk from your array (which may or may not be a problem for your use case).

If you're sure you need timers in here (which I would question), I would suggest scheduling each timer with an interval of 0.1 * $num so that the array position is taken into account (assuming its a vector/list here).

$conn->on('data', function ($data) use ($conn, $loop2, &$cps) {
      // …
}, function (Exception $exception) use ($loop2) {
    echo "Cannot data connect to server: " . $exception->getMessage();
    $loop2->stop();
});

Note that as per https://github.com/reactphp/stream#data-event, this event handler only accepts a single function. The second function has no effect. It looks like this is supposed to handle failed connections attempts, so this should be added as a second function to the connection promise instead 👍

May be you should add this to docs....

We're happy to accept PRs if you feel there's anything that we can improve in our documentation! :shipit: