Code Monkey home page Code Monkey logo

socket's Introduction

amphp/socket

AMPHP is a collection of event-driven libraries for PHP designed with fibers and concurrency in mind. amphp/socket is a library for establishing and encrypting non-blocking sockets. It provides a socket abstraction for clients and servers. It abstracts the really low levels of non-blocking streams in PHP.

Latest Release MIT License

Installation

This package can be installed as a Composer dependency.

composer require amphp/socket

Requirements

amphp/socket heavily relies on amphp/byte-stream, specifically its ReadableStream and WritableStream interfaces.

Connecting to a Server

amphp/socket allows clients to connect to servers via TCP, UDP, or Unix domain sockets. You can establish a socket connection using Amp\Socket\connect(). It will automatically resolve DNS names and retries other IPs if a connection fails and multiple IPs are available.

// You can customize connect() options using ConnectContext
$connectContext = (new Amp\Socket\ConnectContext)
        ->withConnectTimeout(5);

// You can optionally pass a Cancellation object to cancel a pending connect() operation
$deferredCancellation = new Amp\DeferredCancellation();

$socket = connect('amphp.org:80', $connectContext, $deferredCancellation->getCancellation());

Encrypted Connections / TLS

If you want to connect via TLS, use Amp\Socket\connectTls() instead or call $socket->setupTls() on the returned socket.

Handling Connections

Socket implements ReadableStream and WritableStream, so everything from amphp/byte-stream applies for receiving and sending data.

#!/usr/bin/env php
<?php // basic (and dumb) HTTP client

require __DIR__ . '/../vendor/autoload.php';

// This is a very simple HTTP client that just prints the response without parsing.
// league/uri required for this example.

use Amp\ByteStream;
use Amp\Socket\ClientTlsContext;
use Amp\Socket\ConnectContext;
use League\Uri\Http;
use function Amp\Socket\connect;
use function Amp\Socket\connectTls;

$stdout = ByteStream\getStdout();

if (\count($argv) !== 2) {
    $stdout->write('Usage: examples/simple-http-client.php <url>' . PHP_EOL);
    exit(1);
}

$uri = Http::createFromString($argv[1]);
$host = $uri->getHost();
$port = $uri->getPort() ?? ($uri->getScheme() === 'https' ? 443 : 80);
$path = $uri->getPath() ?: '/';

$connectContext = (new ConnectContext)
        ->withTlsContext(new ClientTlsContext($host));

$socket = $uri->getScheme() === 'http'
        ? connect($host . ':' . $port, $connectContext)
        : connectTls($host . ':' . $port, $connectContext);

$socket->write("GET {$path} HTTP/1.1\r\nHost: $host\r\nConnection: close\r\n\r\n");

ByteStream\pipe($socket, $stdout);

Server

amphp/socket allows listening for incoming TCP connections as well as connections via Unix domain sockets. It defaults to secure TLS settings if you decide to enable TLS.

Listening and Accepting Connections

Use Amp\Socket\Socket\listen() to listen on a port or unix domain socket. It's a wrapper around stream_socket_server that gives useful error message on failures via exceptions.

Once you're listening, accept clients using Server::accept(). It returns a Socket that returns once a new client has been accepted. It's usually called within a while loop:

$server = Socket\listen("tcp://127.0.0.1:1337");

while ($client = $server->accept()) {
    // You shouldn't spend too much time here, because that blocks accepting another client, so we use async():
    async(function () use ($client) {
        // Handle client connection here
    });
}

Handling Connections

Socket implements ReadableStream and WritableStream, so everything from amphp/byte-stream applies for receiving and sending data. It's best to handle clients in their own coroutine, while letting the server accept all clients as soon as there are new clients.

#!/usr/bin/env php
<?php // basic (and dumb) HTTP server

require __DIR__ . '/../vendor/autoload.php';

// This is a very simple HTTP server that just prints a message to each client that connects.
// It doesn't check whether the client sent an HTTP request.

// You might notice that your browser opens several connections instead of just one,
// even when only making one request.

use Amp\Socket;
use function Amp\async;

$server = Socket\listen('127.0.0.1:0');

echo 'Listening for new connections on ' . $server->getAddress() . ' ...' . PHP_EOL;
echo 'Open your browser and visit http://' . $server->getAddress() . '/' . PHP_EOL;

while ($socket = $server->accept()) {
    async(function () use ($socket) {
        $address = $socket->getRemoteAddress();
        $ip = $address->getHost();
        $port = $address->getPort();

        echo "Accepted connection from {$address}." . PHP_EOL;

        $body = "Hey, your IP is {$ip} and your local port used is {$port}.";
        $bodyLength = \strlen($body);

        $socket->write("HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: {$bodyLength}\r\n\r\n{$body}");
        $socket->end();
    });
}

Closing Connections

Once you're done with a client, close the connection using Socket::close(). If you want to wait for all data to be successfully written before closing the connection, use Socket::end(). See above for an example.

Server Address

Sometimes you don't know the address the server is listening on, e.g. because you listed to tcp://127.0.0.1:0, which assigns a random free port. You can use Server::getAddress() to get the address the server is bound to.

Server Shutdown

Once you're done with the server socket, close the socket. That means, the server won't listen on the specified location anymore. Use Server::close() to close the server socket.

Encrypted Connections / TLS

As already mentioned in the documentation for Amp\Socket\Socket\listen(), you need to enable TLS manually after accepting connections. For a TLS server socket, you listen on the tcp:// protocol on a specified address. After accepting clients, call $socket->setupTls() where $socket is the socket returned from SocketServer::accept().

Warning Any data transmitted before Socket::setupTls() completes will be transmitted in clear text. Don't attempt to read from the socket or write to it manually. Doing so will read the raw TLS handshake data that's supposed to be read by OpenSSL.

Self-Signed Certificates

There's no option to allow self-signed certificates in ClientTlsContext since it is no more secure than disabling peer verification. To safely use a self-signed certificate, disable peer verification and require fingerprint verification of the certificate using ClientTlsContext::withPeerFingerprint().

Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker.

License

The MIT License (MIT). Please see LICENSE for more information.

socket's People

Contributors

61-6c-69 avatar bilge avatar brstgt avatar bwoebi avatar camspiers avatar danog avatar fedott avatar iankberry avatar joelwurtz avatar kelunik avatar lt avatar nicolas-grekas avatar psafarov avatar rdlowrey avatar smatyas avatar staabm avatar trowski avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

socket's Issues

Write to closed stream

code:

\Amp\Loop::run(function () {
    list($l, $r) = \Amp\Socket\pair();

    $left = new Amp\Socket\ClientSocket($l, 1024);
    $right = new Amp\Socket\ClientSocket($r, 1024);


    // setup read from right stream
    \Amp\asyncCall(function () use ($right) {
        while (($chunk = yield $right->read()) !== null) {
            echo "readed -> " . strlen($chunk) . PHP_EOL;
        }

        echo "right stream was closed" . PHP_EOL;
    });

    // setup read from left stream
    \Amp\Loop::delay(5000, function () use ($left) {
        echo "start read" . PHP_EOL;

        \Amp\asyncCall(function () use ($left) {
            while (($chunk = yield $left->read()) !== null) {
                echo "readed -> " . strlen($chunk) . PHP_EOL;

                \Amp\Loop::defer(function () use ($left) {
                    echo "close left stream -> " . PHP_EOL;

                    $left->close();
                });
            }

            echo "left stream was closed" . PHP_EOL;
        });
    });

    try {
        echo "write -> 1" . PHP_EOL;
        $written = yield $right->write(str_repeat("1", 1024 * 2));
        echo "written -> $written" . PHP_EOL;

        echo "write -> 2" . PHP_EOL;
        $written = yield $right->write(str_repeat("2", 1024 * 1024));
        echo "written -> $written" . PHP_EOL;

        echo "write -> 3" . PHP_EOL;
        $written = yield $right->write(str_repeat("3", 1024 * 1024));
        echo "written -> $written" . PHP_EOL;

    } catch (Throwable $exception) {
        echo "$exception";
    }
});

result:

write -> 1
written -> 2048
write -> 2
start read
readed -> 1024
close left stream ->
left stream was closed
right stream was closed
written -> 1048576
write -> 3
written -> 1048576

I expect that at least write -> 3 will fail

Expand examples

Currently there's only one example for client / server, maybe there should be more examples. There should probably be a socket pool example.

Use separate interfaces for TLS management and TLS state inspection

shutdownTLS and setupTLS indicate that a sockets TLS can be setup or shutdown.

However, an encrypted socket may want to expose that information without offering to change the TLS state - use cases are when wrapping another socket or also concretely, QUIC, which is forcibly using TLS, setup and shutdown not possible.

Replacing stream_socket_client with amphp/socket

I currently have an array of options defined -

$options = [
    'ssl' => [
        'verify_peer' => false,
        'verify_peer_name' => false,
        'allow_self_signed' => true,
    ],
];

I use the options to create a socket context -

$socket_context = stream_context_create($options);

And then I open a connection -

$connection = @stream_socket_client(
        "tcp://www.example.com:80",
        $errno,
        $errstr,
        0.5,
        STREAM_CLIENT_CONNECT,
        $socket_context
);

And I get a new resource that I can do stuff with. The above code works.

I want to port this code over to use amphp socket. So far I have this -

$clientContext = new ClientTlsContext("tcp://www.example.com");
$clientContext = $clientContext->withoutPeerVerification();
$connectContext = (new ConnectContext)->withTlsContext($clientContext)->withConnectTimeout(500);

$socket = connect("tcp://www.example.com:80", $connectContext);
$socket->onResolve(function ($fail, $success) {
    echo $fail->getMessage();
});

However I always get this error from the echo above -

All query attempts failed for www.example.com: Reading from the server failed, Reading from the server failed

What am I doing wrong?

Process ends in case of deferred promise is used

Having issue where in case of deferred promise used in repeat loop, program completely exists everything. After handling single connection, I would expect it to go back to while loop, waiting for new connection, but instead it exists handleConnection and afterwards the while loop. No exception, no errors. Only happens if deferred is used.

Loop::run(function () use ($host) {
    $server = Server::listen($host, (new BindContext())->withTcpNoDelay());
    
    while ($socket = yield $server->accept()) {
        $this->handleConnection($socket);
    }
}

private function handleConnection(ResourceSocket $socket): void
{
        $counter = 0;
        $deferred = new Deferred();
        
        Loop::repeat(100, function ($watcherId) use (&$counter, $deferred) {
            if($counter === 3) {
                $deferred->resolve();
                Loop::cancel($watcherId);

            }
            $counter++;
        });

        wait($deferred->promise());
        
        return;
}

Amp v2 function names

Currently we have listen, rawListen, connect, rawConnect, cryptoConnect, enableCrypto, ...

Maybe we should change to listenRaw, connect, connectCrypto, enableCrypto? Not sure... but cryptoConnect vs. enableCrypto feels weird.

Distrust SHA-1 certificates

NIST recommends that SHA-1 should no longer be used for digital signatures. As of 2016-01-01, the CA/B Forum forbids issuing new SHA-1 certificates. The CA/B has advised CAs starting 2015-01-16 to issue no SHA-1 certificates with an expiration date greater than 2017-01-01, as browsers had already announced to deprecate and remove SHA-1. Starting with Java 9, Java will also no longer accept SHA-1 starting 2017-01-01 by default.

I think PHP doesn't provide a mechanism for that yet, at least I couldn't find anything in the options. Therefore we probably have to capture the certificate and chain and check it ourselves. I'm looking forward to adding such a possibility to PHP 7.2 and defaulting to not accept SHA-1 there, too.

Socket renegotiation fails

$promise = \Amp\socket\cryptoConnect('github.com:443', []);
$sock = \Amp\wait($promise);
$promise = \Amp\socket\cryptoEnable($sock, ["verify_peer" => false]); // force renegotiation by different option...
$sock = \Amp\wait($promise);

That shouldn't cause any exceptions etc....

Memory leak if try to write in close socket

Example

Loop::run(function () {
    $uri = "tcp://127.0.0.1:1337";

    Loop::repeat(500, function () {
        echo 'Counters: '. Test::$constructInc . ' - ' . Test::$destructInc . PHP_EOL;
        echo 'Memory: ' . round(memory_get_usage() / 1024, 2) . ' KB' . PHP_EOL;
    });

    $server = Amp\Socket\Server::listen($uri);

    while ($socket = yield $server->accept()) {
        $obj = new Test($socket);
        yield $obj->run();
        unset($obj);
    }
});

class Test
{
    public static int $constructInc = 0;
    public static int $destructInc = 0;

    public string $content;

    public function __construct(private \Amp\Socket\ResourceSocket $socket)
    {
        $this->content = str_repeat('a', 2048);
        self::$constructInc++;
    }

    public function __destruct()
    {
        echo 'destruct' . PHP_EOL;
        self::$destructInc++;
    }

    public function run(): \Amp\Promise
    {
        return \Amp\call(function () {
            try {
                $this->socket->close();
                yield $this->socket->write('test');
            } catch (Throwable $e) {
                echo 'catch exception' . PHP_EOL;
            }
        });
    }
}

Run script and try to connect by telnet several times

telnet 127.0.0.1 1337

Result in console

Counters: 0 - 0
Memory: 1284.35 KB
catch exception
Counters: 1 - 0
Memory: 1488.39 KB
catch exception
Counters: 2 - 0
Memory: 1522.4 KB
catch exception
Counters: 3 - 0
Memory: 1556.41 KB
catch exception
Counters: 4 - 0
Memory: 1590.41 KB
catch exception
Counters: 5 - 0
Memory: 1624.42 KB
catch exception
Counters: 6 - 0
Memory: 1658.43 KB
catch exception
Counters: 7 - 0
Memory: 1692.44 KB
catch exception
Counters: 8 - 0
Memory: 1726.45 KB
catch exception
Counters: 9 - 0
Memory: 1760.45 KB
catch exception
Counters: 10 - 0
Memory: 1794.46 KB
catch exception
Counters: 11 - 0
Memory: 1828.47 KB
catch exception
Counters: 12 - 0
Memory: 1862.48 KB

Test instances only created but not destructed. Memory consumption increases

If change vendor\amphp\byte-stream\lib\ResourceOutputStream.php:177 from

return new Failure(new ClosedException("The stream is not writable"));

to

throw new ClosedException("The stream is not writable");

got these results

Counters: 0 - 0
Memory: 1284.35 KB
catch exception
destruct
Counters: 1 - 1
Memory: 1449.94 KB
catch exception
destruct
Counters: 2 - 2
Memory: 1449.94 KB
catch exception
destruct
Counters: 3 - 3
Memory: 1449.94 KB
catch exception
destruct
Counters: 4 - 4
Memory: 1449.94 KB
catch exception
destruct
Counters: 5 - 5
Memory: 1449.94 KB
catch exception
destruct
Counters: 6 - 6
Memory: 1449.94 KB
catch exception
destruct
Counters: 7 - 7
Memory: 1449.94 KB
catch exception
destruct
Counters: 8 - 8
Memory: 1449.94 KB
catch exception
destruct
Counters: 9 - 9
Memory: 1449.94 KB
catch exception
destruct
Counters: 10 - 10
Memory: 1449.94 KB

Instances destructed, memory is stable.

PHP 8.1, amp 2.6.2, amphp/socket 1.2.0.

Socket connector retry logic

Let's re-examine how error codes (including EAGAIN, which is not handled currently) and retries are handled by DnsConnector before tagging the next major.

bug: on connect

<?php
require_once __DIR__ . "/../vendor/autoload.php";

\Amp\Loop::run(function()
{
    \Amp\Socket\connect("tcp://127.0.0.1:1")->onResolve(function (Throwable $error = null, \Amp\Socket\Socket $socket = null) {
//        var_dump($error);

        assert($error !== null); //FAIL
    });

    \Amp\Socket\connect("tcp://8.8.8.8:53")->onResolve(function (Throwable $error = null, \Amp\Socket\Socket $socket = null) {
//        var_dump($error);

        assert($error === null);
    });

    \Amp\Socket\connect("tcp://8.8.8.8:1234")->onResolve(function (Throwable $error = null, \Amp\Socket\Socket $socket = null) {
//        var_dump($error);

        assert($error !== null);
    });

    \Amp\Socket\connect("unix:///tmp/invalid")->onResolve(function (Throwable $error = null, \Amp\Socket\Socket $socket = null) {
//        var_dump($error);

        assert($error !== null);
    });

    \Amp\Socket\connect("unix:///tmp/11001")->onResolve(function (Throwable $error = null, \Amp\Socket\Socket $socket = null) {
//        var_dump($error);

        assert($error === null);
    });
});

Can't get examples to work

I have websockets with different scripts working but really like the usability the lib implies. but i can't get any of the samples work.

<?php
require __DIR__ . "/vendor/autoload.php";
// Non-blocking server implementation based on amphp/socket keeping track of connections.
use Amp\Loop;
use Amp\Socket\ServerSocket;
use function Amp\asyncCall;
Loop::run(function () {
    $server = new class {
        private $uri = "tcp://127.0.0.1:9002";
        // We use a property to store a map of $clientAddr => $client
        private $clients = [];
        public function listen() {
            asyncCall(function () {
                $server = Amp\Socket\listen($this->uri);
                print "Listening on " . $server->getAddress() . " ..." . PHP_EOL;
                while ($socket = yield $server->accept()) {
                    $this->handleClient($socket);
                }
            });
        }
        private function handleClient(ServerSocket $socket) {
            asyncCall(function () use ($socket) {
                $remoteAddr = $socket->getRemoteAddress();
                // We print a message on the server and send a message to each client
                print "Accepted new client: {$remoteAddr}". PHP_EOL;
                $this->broadcast($remoteAddr . " joined the chat." . PHP_EOL);
                // We only insert the client afterwards, so it doesn't get its own join message
                $this->clients[$remoteAddr] = $socket;
                while (null !== $chunk = yield $socket->read()) {
                    $this->broadcast($remoteAddr . " says: " . trim($chunk) . PHP_EOL);
                }
                // We remove the client again once it disconnected.
                // It's important, otherwise we'll leak memory.
                unset($this->clients[$remoteAddr]);
                // Inform other clients that that client disconnected and also print it in the server.
                print "Client disconnected: {$remoteAddr}" . PHP_EOL;
                $this->broadcast($remoteAddr . " left the chat." . PHP_EOL);
            });
        }
        private function broadcast(string $message) {
            foreach ($this->clients as $client) {
                // We don't yield the promise returned from $client->write() here as we don't care about
                // other clients disconnecting and thus the write failing.
                $client->write($message);
            }
        }
    };
    $server->listen();
});

when i make connections from a simple js ws connection i consistantly get errors.

the browser gives me this error:

Uncaught (in promise) {message: "Could not establish connection. Receiving end does not exist."}
callbackArgs @ browser-polyfill.js:588
sendResponseAndClearCallback @ VM35 extensions::messaging:415
disconnectListener @ VM35 extensions::messaging:433
EventImpl.dispatchToListener @ VM28 extensions::event_bindings:403
publicClassPrototype.(anonymous function) @ VM34 extensions::utils:140
EventImpl.dispatch_ @ VM28 extensions::event_bindings:387
EventImpl.dispatch @ VM28 extensions::event_bindings:409
publicClassPrototype.(anonymous function) @ VM34 extensions::utils:140
dispatchOnDisconnect @ VM35 extensions::messaging:376
index.html:13 WebSocket connection to 'ws://127.0.0.1:9002/' failed: Error during WebSocket handshake: net::ERR_INVALID_HTTP_RESPONSE
WebSocketTest @ index.html:13
(anonymous) @ VM74:1
browser-polyfill.js:588 Uncaught (in promise) {message: "Could not establish connection. Receiving end does not exist."}
callbackArgs @ browser-polyfill.js:588
sendResponseAndClearCallback @ VM35 extensions::messaging:415
disconnectListener @ VM35 extensions::messaging:433
EventImpl.dispatchToListener @ VM28 extensions::event_bindings:403
publicClassPrototype.(anonymous function) @ VM34 extensions::utils:140
EventImpl.dispatch_ @ VM28 extensions::event_bindings:387
EventImpl.dispatch @ VM28 extensions::event_bindings:409
publicClassPrototype.(anonymous function) @ VM34 extensions::utils:140
dispatchOnDisconnect @ VM35 extensions::messaging:376
browser-polyfill.js:588 Uncaught (in promise) {message: "Could not establish connection. Receiving end does not exist."}
callbackArgs @ browser-polyfill.js:588
sendResponseAndClearCallback @ VM35 extensions::messaging:415
disconnectListener @ VM35 extensions::messaging:433
EventImpl.dispatchToListener @ VM28 extensions::event_bindings:403
publicClassPrototype.(anonymous function) @ VM34 extensions::utils:140
EventImpl.dispatch_ @ VM28 extensions::event_bindings:387
EventImpl.dispatch @ VM28 extensions::event_bindings:409
publicClassPrototype.(anonymous function) @ VM34 extensions::utils:140
dispatchOnDisconnect @ VM35 extensions::messaging:376
browser-polyfill.js:588 Uncaught (in promise) {message: "Could not establish connection. Receiving end does not exist."}1

whiel the server log says accepted new client with an immediate client disconnected before the ws.send() that is wrapped int he ws.open callback can even do anything.

Im just looking for a bit of help getting started with this lib

thanks

"SNI_server_certs array requires string host name keys"

I'm attempting to start a TLS encrypted socket server with amphp/amp and amphp/socket.

I create a \Amp\Socket\Certificate using a self-signed certificate and pass that into a \Amp\Socket\ServerTlsContext, which is then passed into \Amp\Socket\listen().

After a client connects I have yield $socket->enableCrypto() to enable TLS.

When I try to connect with a client I receive the following:

PHP Warning: stream_socket_enable_crypto():
        SNI_server_certs array requires string host name keys
        in /foo/bar/vendor/amphp/socket/lib/Internal/functions.php on line 95
PHP Fatal error: Uncaught Amp\Socket\CryptoException:
        Crypto negotation failed: stream_socket_enable_crypto(): SNI_server_certs array requires string host name keys
        in /foo/bar/vendor/amphp/socket/lib/Internal/functions.php on line 101
<stacktrace here>

The only documentation I could find about enabling TLS for sockets is inside the docblock for \Amp\Socket\listen():

If you want to accept TLS connections, you have to use yield $socket->enableCrypto() after accepting new clients.

Is the SNI_server_certs a bug or am I doing something wrong? Are there any good guides on how to use TLS with Amp sockets?

My PHP version is 7.1.9 and I'm running the application on Ubuntu 14.04. The TLS cert I'm attempting to use is a single-domain self-signed certificate which I created with OpenSSL tooling.

To explain better here is the main gist of what my application code is doing in terms of sockets and TLS and clients:

<?php

$cert = new \Amp\Socket\Certificate('/path/to/cert.crt');

$tls_context = (new \Amp\Socket\ServerTlsContext())->withCertificate($cert);

\Amp\Loop::run(function () use ($tls_context) {
    $socket_server = \Amp\Socket\listen('host.app:8080', null, $tls_context);

    \Amp\asyncCall(function () use ($socket_server) {
        
        while ($socket = yield $socket_server->accept()) {
            yield $socket->enableCrypto();

            \Amp\asyncCall(function () use ($socket) {
                
                while (null !== $chunk = yield $socket->read()) {
                    $socket->write('you said : ' . $chunk);
                }
                
            });
        }
        
    });
});

I checked the code for stream context options relating to SNI_server_certs and the array is created with 0-based integer indices from what I'm seeing. Should they be hostname based indices or not?

Refactor certificates in context option

There should be an object for that instead of using paths to allow for different files for key and certs without relying on the socket option names of PHP.

Timeout exceeded on multiple request

I am sending multiple requests to a TCP client, using the below code

$promises = [];
foreach ($packets as $packet) {
    $promises[] = call(function () use ($packet, $uri) {

        /** @var \Amp\Socket\ClientSocket $socket */
        $socket = yield connect($uri);
        try {
            yield $socket->write($packet);

            $chunk = yield $socket->read(); // modbus packet is so small that one read is enough
            if ($chunk === null) {
                return null;
            }
            return ResponseFactory::parseResponse($chunk);
        } finally {
            $socket->close();
        }
    });
}

try {
    // will run multiple request in parallel using non-blocking php stream io
    $responses = wait(all($promises));
    // print_r($responses);
} catch (Throwable $e) {
    print_r($e);
}

This works fine, but if I have more than 100 packages, I am getting the below error message

Exception 'Amp\Socket\ConnectException' with message 'Connecting to tcp://192.168.0.220:502 failed: timeout exceeded (10000 ms)'

in /vagrant/vendor/amphp/socket/src/DnsConnector.php:82

Is there a limit on how many requests can I send using socket?
Or is this a memory limitation?
Or a network limitation?

Legacy SSL when using as Phar

https://github.com/amphp/socket/blob/master/lib/functions.php#L176 doesn't work when using it as Phar, because OpenSSL can't read files inside a Phar.

Warning: stream_socket_enable_crypto(): Unable to set verify locations 'phar://C:/le/acme-client.phar/vendor/amphp/socket/lib/../var/ca-bundle.crt' '(null)' in phar://C:/le/acme-client.phar/vendor/amphp/socket/lib/functions.php on line 269

Warning: stream_socket_enable_crypto(): failed to create an SSL handle in phar://C:/le/acme-client.phar/vendor/amphp/socket/lib/functions.php on line 269 exception 'Amp\Socket\CryptoException' with message 'Crypto negotiation failed: stream_socket_enable_crypto(): failed to create an SSL handle' in phar://C:/le/acme-client.phar/vendor/amphp/socket/lib/functions.php:273

Next exception 'Kelunik\Acme\AcmeException' with message 'Could not obtain directory.' in phar://C:/le/acme-client.phar/vendor/kelunik/acme/lib/AcmeClient.php:189

Unable to read from socket after timeout

Using such code:

$data = yield timeout($socket->read(), 5000)

In case timeout is reached, there is no possibility to read from the socker once again due to ResourceInputStream variable $deferred being defined thus:

 /** @inheritdoc */
    public function read(): Promise
    {
        if ($this->deferred !== null) {
            throw new PendingReadError; // Always exists here
        }
...

Is this intentional, or am I doing something wrong? I have tried calling $socket->unreference() after the timeout, but the use case for that seems completely different.

Question about a pipeline design pattern

Hi!

I'm looking for a pipeline-based design pattern where each side-effect is run through a yield. Something like

function doSomething()
{
  if ($whatever) {
    yield $io->db->query('UPDATE bla ...');
  } else {
    yield $io->file->write($filename, $data);
  }
  // More logic...
}

(Please disregard undefined variables.)

The point would be that all side-effects that do not depend on the result of the side-effect can be executed concurrently. So IO concurrency would be the default behaviour. Is this something that can be achieved with Amphp?

The samples provided in the documentation don't work

The documentation needs to be updated. None of the provided examples seem to be working.

The code sample provided here https://amphp.org/getting-started/tcp-chat/basic-echo-server throws this error:
PHP Fatal error: Uncaught TypeError: Argument 1 passed to {closure}() must be an instance of Amp\Socket\ServerSocket, instance of Amp\Socket\ResourceSocket given, called in [REDACTED]vendor/amphp/amp/lib/functions.php on line 60 and defined in [REDACTED]/test.php:13

Same thing here: https://amphp.org/getting-started/tcp-chat/broadcasting - i only tried running the first code sample
PHP Fatal error: Uncaught TypeError: Argument 1 passed to class@anonymous::handleClient() must be an instance of Amp\Socket\ServerSocket, instance of Amp\Socket\ResourceSocket given, called in [REDACTED]/index.php on line 20 and defined in [REDACTED]/index.php:25

I think the documentation needs to be updated. None of the provided examples seem to be working.

The code sample provided here https://amphp.org/getting-started/tcp-chat/basic-echo-server throws this error:

PHP Fatal error: Uncaught TypeError: Argument 1 passed to {closure}() must be an instance of Amp\Socket\ServerSocket, instance of Amp\Socket\ResourceSocket given, called in [REDACTED]vendor/amphp/amp/lib/functions.php on line 60 and defined in [REDACTED]/test.php:13

Same thing here: https://amphp.org/getting-started/tcp-chat/broadcasting - i only tried running the first code sample

PHP Fatal error: Uncaught TypeError: Argument 1 passed to class@anonymous::handleClient() must be an instance of Amp\Socket\ServerSocket, instance of Amp\Socket\ResourceSocket given, called in [REDACTED]/index.php on line 20 and defined in [REDACTED]/index.php:25

Steps to reproduce:

  1. Create a new test directory:
    mkdir test; cd test
  2. Within that test directory download amphp/socket:
    composer require amphp/socket
  3. Create a test.php file, copy and paste one of the samples then execute the file with this command:
    php test.php
    This will start the script and wait for connections
  4. From another terminal try to connect with "nc":
    nc localhost 1337

As soon as nc attempts to connect the script will throw the errors mentioned above.

Tested on:

  • Ubuntu 18.04 with PHP 7.2.19 (installed from the distro's official repositories)
  • MacOS 10.14.6 with PHP 7.3.4 (installed with homebrew)

Getting a response as NULL after 4 minutes

I am sending multiple requests to one or more TCP client, using the below code

$promises = [];
foreach ($packets as $packet) {
    $promises[] = call(function () use ($packet, $uri) {

        /** @var \Amp\Socket\ClientSocket $socket */
        $socket = yield connect($uri);
        try {
            yield $socket->write($packet);

            $chunk = yield $socket->read(); // modbus packet is so small that one read is enough
            if ($chunk === null) {
                return null;
            }
            return ResponseFactory::parseResponse($chunk);
        } finally {
            $socket->close();
        }
    });
}

try {
    // will run multiple request in parallel using non-blocking php stream io
    $responses = wait(all($promises));
    // print_r($responses);
} catch (Throwable $e) {
    print_r($e);
}

One of the clients is delaying too much and after approximately 4 minutes it returns "null".

Can you help me understand what happens after 4 minutes?
Is this something like a timeout for the read command?
Can I change it for example to 1 minute?

Thank you in advance

Connection fails with AAAA records on IPv4 only networks

Working on an IPv4 only network and trying to connect to whois.donuts.co I found I was getting Address family for hostname not supported errors. I tracked this down to the connection attempt using the AAAA record from the DNS lookup which was then not being caught because it's a (fatal) Error, not an Exception.

I've created a PR (#34) to allow limiting of the resolve type in the DNS request via a parameter in the ClientConnectionContext.

Loop exceptionally stopped without resolving the promise + Trying to access array offset on value of type null

Hey,

recently (after moving to php 7.4) we've started experiencing this issue where we queue a bunch of promises and at some point when resolving one of the promises we get Trying to access array offset on value of type null, which then interrupts the whole process.

This is the full (inner) exception:

[previous exception] [object] (ErrorException(code: 0): Trying to access array offset on value of type null at vendor/amphp/socket/src/Internal/functions.php:130)
[stacktrace]
#0 vendor/amphp/socket/src/Internal/functions.php(130): Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError()
#1 vendor/amphp/amp/lib/Loop/NativeDriver.php(201): Amp\\Socket\\Internal\\{closure}()
#2 vendor/amphp/amp/lib/Loop/NativeDriver.php(104): Amp\\Loop\\NativeDriver->selectStreams()
#3 vendor/amphp/amp/lib/Loop/Driver.php(138): Amp\\Loop\\NativeDriver->dispatch()
#4 vendor/amphp/amp/lib/Loop/Driver.php(72): Amp\\Loop\\Driver->tick()
#5 vendor/amphp/amp/lib/Loop.php(95): Amp\\Loop\\Driver->run()
#6 vendor/amphp/amp/lib/functions.php(229): Amp\\Loop::run()
#7 app/Console/Commands/Ccc.php(159): Amp\\Promise\\wait()
#8 [internal function]: App\\Console\\Commands\\Ccc->handle()

Which looks like it is related to trying to get the error from an internal php function, but failing.

Any ideas why that would be failing?


EDIT: I am trying in conjunction with amphp/http-client and the https://samsung.com domain.

Performance degradation of stream handling on Windows OS

Here's simple test script

<?php
require 'vendor/autoload.php';

$t1 = time();

\Amp\Loop::run(function () use ($t1) {
    \Amp\ByteStream\pipe(
        new \Amp\ByteStream\ResourceInputStream(fopen("C:\Downloads\Rogue One A Star Wars Story 2016 x264 HDTS AAC(Deflickered)-DDR\sample-Rogue One A Star Wars Story 2016 x264 HDTS AAC(Deflickered)-DDR.mkv", 'r')),
        new \Amp\ByteStream\ResourceOutputStream(fopen('file', 'w'))
    )->onResolve(function() use ($t1) {
        echo 'Done in '.(time() - $t1);
    });

    $server = Amp\Socket\listen("127.0.0.1:0");

    while ($socket = yield $server->accept()) {
//         do nothing
    }
});

Input is 19MB file. Code as is takes 241s to finish. If I comment out the while loop, file is copied in ~1 second!

Windows 7, PHP 7.2.0 (cli) (built: Nov 29 2017 00:17:00) ( ZTS MSVC15 (Visual C++ 2017) x86 )

Socket->reference()

\Amp\Loop::run(function () {
    echo "Driver -> " . get_class(\Amp\Loop::get()) . "\n";

    /**
     * @var $client \Amp\Socket\ClientSocket
     */
    $client = yield connect("127.0.0.1:12001");
    $client->reference();
});

I expect that loop will not close, but it is closed :(
Or reference() is used in some other scope ?

I try to connect a ClientSocket to server and do nothing :), is it possible ?

Too much RAM is used

\Amp\Loop::run(function () {
    \Amp\Loop::repeat(5 * 1000, function () {
        echo "current RAM: " . round((memory_get_usage() / 1024 / 1024), 2) . "MB, pick RAM: " . round((memory_get_peak_usage() / 1024 / 1024), 2) . "MB\n";
    });

    $server = \Amp\Socket\listen("127.0.0.1:12001");

    \Amp\call(function () use ($server) {
        /**
         * @var $client \Amp\Socket\ServerSocket
         */
        while ($client = yield $server->accept()) {
            \Amp\call(function () use ($client) {
                echo "client connected" . PHP_EOL;

                while (($chunk = yield $client->read()) !== null) {
                    echo "client data -> " . strlen($chunk) . PHP_EOL;
                }

                echo "client disonnected" . PHP_EOL;
            });
        }
    });
});

What I am doing wrong here ?
If I connect 1000 users I get 100MB of RAM used
Some time ago I already have connected 30K and I have ~100MB of RAM

UPDATE

\Amp\Loop::run(function () {
    \Amp\Loop::repeat(5 * 1000, function () {
        echo "current RAM: " . round((memory_get_usage() / 1024 / 1024), 2) . "MB, pick RAM: " . round((memory_get_peak_usage() / 1024 / 1024), 2) . "MB\n";
    });

    $clientHandler = asyncCoroutine(function (ServerSocket $client) {
        echo "client connected" . PHP_EOL;

        while (($chunk = yield $client->read()) !== null) {
            echo "client data -> " . strlen($chunk) . PHP_EOL;
        }

        echo "client disonnected" . PHP_EOL;
    });

    $server = \Amp\Socket\listen("127.0.0.1:12001");

    while ($client = yield $server->accept()) {
        $clientHandler($client);
    }
});

I try to use code from example, and I get the same problem

I think problem is here:

        while (($chunk = yield $client->read()) !== null) {
            echo "client data -> " . strlen($chunk) . PHP_EOL;
        }

or I am doing something wrong ?

Buffer sizes, too much RAM is used

After days of debugging I find that default chunkSize is using too much RAM, here is simple examples to check this.

Why you put by default 65536 ? Are there any reasons for this ?

After some tests I find that best settings are:

  1. For stream_get_contents -> -1
    1.1 If 65536 is used: for 1000 connections and 1 byte written you get ~73MB of RAM
    1.2 If -1 is used: for 1000 connections and 1 byte written you get ~6MB of RAM
    1.3 If -1 is used: for 1000 connections and 65537 byte written you get ~27MB of RAM
    1.4 If 65536 is used: for 1000 connections and 65537 byte written you get ~73MB of RAM

  2. For fwrite -> depends on data size
    2.1 If your $chunkSize is small ex: 1024 and you send big data, you will spend more time to receive it all, but less RAM
    2.2 vise versa 2.1

PS: Sorry for my formatting, looks like this is not my best skill :)

DNS get IPv6 in IPv4 Only network sometimes.

Sometimes socket client get IPv6, but the machine is IPv4 only network.

E_WARNING: stream_socket_client(): unable to connect to tcp://[240e:f7:a060:201::f0]:443 (Network is unreachable) in /../vendor/amphp/socket/src/DnsConnector.php:71

[..@localhost current]$ ifconfig
eth0      Link encap:Ethernet  HWaddr 00:0C:29:7A:4B:E7  
          inet addr:xxx.xxx.65.11  Bcast:xxx.xxx.65.31  Mask:255.255.255.224
          inet6 addr: fe80::20c:29ff:fe7a:4be7/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:29137558696 errors:0 dropped:0 overruns:0 frame:0
          TX packets:29440968631 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:15914042207653 (14.4 TiB)  TX bytes:19437442358278 (17.6 TiB)

eth1      Link encap:Ethernet  HWaddr 00:0C:29:7A:4B:F1  
          inet addr:192.168.1.11  Bcast:192.168.1.255  Mask:255.255.255.0
          inet6 addr: fe80::20c:29ff:fe7a:4bf1/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:150108186344 errors:0 dropped:0 overruns:0 frame:0
          TX packets:144534502376 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:77223366179658 (70.2 TiB)  TX bytes:35375308031429 (32.1 TiB)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:16436  Metric:1
          RX packets:756353882 errors:0 dropped:0 overruns:0 frame:0
          TX packets:756353882 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:242943102821 (226.2 GiB)  TX bytes:242943102821 (226.2 GiB)

PHP Version is 7.4.10.
OS is CentOS 6.2 (Linux localhost.localdomain 2.6.32-220.17.1.el6.x86_64).
Packages version:

amphp/amp                     v2.5.2  A non-blocking concurrency framework for PHP applications.
amphp/dns                     v1.2.3  Async DNS resolution for Amp.
amphp/socket                  v1.1.3  Async socket connection / server tools for Amp.

Proxy Socket

Actually working on amphp/ssh#3 and it would be nice to support encryption.

I'm wondering it there is a possiblity to provide some sort of ProxyClientSocket to avoid reimplementing tls ?

Basically, for the client part, it would spawn a "fake" server where the client connects to (so using basic php resource) and the server would then Proxy all incoming read / write from this stream to another Socket.

So an user would manipulate this ProxyClient and could enable encryption which will be transfered to the underlying socket.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.