react/socket-client
Async, event-driven TCP/IP socket client for ReactPHP. Provides promise-based connectors for plain TCP, secure TLS, and Unix domain sockets, plus DNS resolution, connection timeouts, and a flexible ConnectorInterface for custom transports.
react/socket-client is the client half of what is now react/socket — it gives you non-blocking TcpConnector, SecureConnector (TLS), and the resolver/timeout wrappers used to open outbound connections on a ReactPHP event loop. There is no server here; if you need to accept connections you want react/socket, not this package. The package is flagged abandoned by its own README ("[Legacy]") and was last tagged in 2017, so treat any adoption as an explicit, time-boxed decision — plan to migrate to react/socket (which exposes the equivalent React\Socket\Connector::connect($uri) API).
Minimal steps:
composer require react/socket-client:^0.6 react/event-loop react/dns
Look first at:
README.md — the only maintained documentation; the usage example is still accurate.examples/ — small runnable scripts showing DNS + connector wiring.src/Connector.php, src/DnsConnector.php, src/TimeoutConnector.php — the three classes you will actually compose in a Laravel app.First use case: an Artisan command that dials an upstream service and streams bytes back. Do not try to run this inside a normal HTTP request — the loop blocks the request until run() returns, and nothing else can happen in the same PHP process.
// app/Console/Commands/TcpPing.php
$loop = React\EventLoop\Factory::create();
$resolver = (new React\Dns\Resolver\Factory())->create('8.8.8.8');
$tcp = new React\SocketClient\TcpConnector($loop);
$dns = new React\SocketClient\DnsConnector($tcp, $resolver);
$secure = new React\SocketClient\SecureConnector($dns, $loop);
$connector = new React\SocketClient\TimeoutConnector($secure, $loop, 10.0);
$connector->create('api.example.com', 443)->then(
function (React\SocketClient\Stream $stream) {
$stream->write("HEAD / HTTP/1.1\r\nHost: api.example.com\r\n\r\n");
$stream->on('data', function ($chunk) {
$this->output->write($chunk);
});
$stream->on('close', fn () => $this->output->writeln('<info>closed</info>'));
},
fn (Throwable $e) => $this->error($e->getMessage())
);
$loop->run();
Bind the connector as a container singleton. Construct the loop, resolver, and connector stack once in a service provider and inject it. Building a new Factory::create() per call leaks file descriptors and creates loop conflicts if two instances are ever run().
$this->app->singleton(React\SocketClient\ConnectorInterface::class, function () {
$loop = React\EventLoop\Factory::create();
$resolver = (new React\Dns\Resolver\Factory())->create(config('services.dns.resolver'));
$connector = new React\SocketClient\TimeoutConnector(
new React\SocketClient\DnsConnector(
new React\SocketClient\TcpConnector($loop),
$resolver
),
$loop,
10.0
);
return $connector;
});
Remember the singleton also owns the loop — expose it separately ($this->app->instance(LoopInterface::class, $loop)) so commands, timers, and Ctrl-C handling can reach it.
Use the promise composition operators, not nested then(). React\Promise\all(), race(), and any() let you fan out to many hosts concurrently and aggregate results. This is the real reason to reach for ReactPHP over curl_multi — you get a uniform Promise interface.
use function React\Promise\all;
$checks = collect($hosts)->map(fn ($h) =>
$connector->create($h, 443)->then(fn () => 'up', fn ($e) => 'down: '.$e->getMessage())
);
all($checks)->then(fn ($results) => $this->table(['host','status'], $results));
Treat the socket as a duplex stream, not a request/response object. Stream extends React\Stream\Stream, so you get write(), end(), close(), pipe(), and data/drain/end/close/error events. There are no message boundaries — implement framing yourself (newline-delimited for LINE protocols, length-prefix for binary).
Always wrap with TimeoutConnector. TcpConnector alone will leave a promise pending indefinitely on a filtered port; the promise never rejects and your command hangs with no output. TimeoutConnector rejects on connection establishment only — you still need your own $loop->addTimer() for idle-read timeouts.
Wire graceful shutdown explicitly. In long-running commands, pcntl_signal + $loop->addSignal(SIGTERM, ...) and close all streams before $loop->stop(), otherwise Octane/supervisord restarts will leave half-open sockets.
Bridge into Laravel logging/config, not facades. The loop callbacks run outside the request lifecycle; Log::info() is fine, but anything that touches the DB can block the loop (see gotchas). Use config for the resolver and TLS context rather than hard-coded 8.8.8.8.
Package react/socket-client is abandoned, you should avoid using it. Use react/socket instead. Migration is mechanical: (new React\Socket\Connector($loop))->connect("tcp://host:80") returns Promise<ConnectionInterface> with the same write/data semantics. The Stream class is gone; you get ConnectionInterface directly.ConnectorInterface::create($host, $port) vs ConnectionInterface::connect($uri) — the API changed across the 0.4 → 0.5 → 0.6 line. Pin the version you actually install and read the constructor of SecureConnector in that tag: older releases take (ConnectorInterface $connector, LoopInterface $loop), 0.5+ adds a third array $context for TLS options (verify_peer, cafile, SNI).TcpConnector gives you plaintext. SecureConnector wraps and performs the handshake, but it resolves after the TCP connect — a certificate failure rejects the promise, not an exception. Attach an otherwise/second then argument or the rejection is silently swallowed (React/Promise v2 has no unhandled-rejection reporting).then() will just… stop. Wrap fan-out code with a top-level ->then(null, fn ($e) => Log::error(...)).file_get_contents, sleep, and synchronous Redis will stall every in-flight stream in the process. Do I/O through ReactPHP clients or hand off to a queue.create() is a fresh socket; there's no keep-alive manager. If you're hitting an upstream in a tight loop, build a small pool keyed by host:port and guard against reusing closed streams ($stream->isWritable()).Stream::getRemoteAddress() is unreliable across versions — verify in your installed tag before logging it; the canonical implementation lives in ConnectionInterface on react/socket.React\EventLoop. Run the connector in a dedicated php artisan daemon under supervisord and speak to it over Redis/queue, rather than trying to embed it in an Octane request.Throwable-only signature changes and will emit deprecations under PHP 8.2+ if you pass Exception-typed handlers. Test on the exact PHP version you deploy; if you must stay on this package, dev-master is usually a bit healthier than the last tag.How can I help you explore Laravel packages today?