Sockets #

A socket is a network communication endpoint — a door that can send and receive data between two programs, whether on the same machine or on different machines across a network. PHP has two ways to work with sockets: stream sockets via stream_socket_server/client, which use the standard stream interface, and ext-sockets via socket_* functions, which provide lower-level C-like control. This article covers both — when to use each, how to build correct TCP servers and clients, non-blocking sockets with multiplexing, Unix domain sockets for local inter-process communication, and real-world patterns like simple protocols over TCP and connection pooling.

Socket Types in PHP #

flowchart TD
    A[Sockets in PHP] --> B[Stream Sockets\nstream_socket_*]
    A --> C[Ext-Sockets\nsocket_*]

    B --> B1[Standard stream interface\nfread / fwrite / fgets]
    B --> B2[Easy integration\nwith stream filters]
    B --> B3[SSL/TLS built-in\nvia context]

    C --> C1[Low level\nfull control]
    C --> C2[Access to socket options\nsetsockopt / getsockopt]
    C --> C3[Easier UDP\nsendto / recvfrom]

    style B fill:#dcfce7
    style C fill:#dbeafe

For most needs, stream sockets are more recommended because they’re consistent with PHP’s familiar stream model and support TLS transparently. Use ext-sockets only when you need very low-level control or features unavailable in stream sockets.


TCP Client with Stream Sockets #

Making a TCP connection to a server is the most frequently used operation — talking to databases, custom APIs, or any service using TCP:

<?php
declare(strict_types=1);

// Open a TCP connection to the server
$socket = stream_socket_client(
    'tcp://api.example.com:8080',
    $errno,
    $errstr,
    timeout: 5.0, // connection timeout in seconds
);

if ($socket === false) {
    throw new \RuntimeException("Connection failed: [$errno] $errstr");
}

// Set a timeout for read/write operations (different from the connection timeout)
stream_set_timeout($socket, 10);

try {
    // Send a request
    $request = "GET /data HTTP/1.0\r\nHost: api.example.com\r\n\r\n";
    fwrite($socket, $request);

    // Read the response
    $response = '';
    while (!feof($socket)) {
        $chunk = fread($socket, 8192);
        if ($chunk === false) break;
        $response .= $chunk;
    }

    echo "Response:\n$response\n";

} finally {
    fclose($socket); // always close the connection
}

Client with TLS/SSL #

<?php
// TLS connection — just change 'tcp://' to 'ssl://'
$context = stream_context_create([
    'ssl' => [
        'verify_peer'       => true,
        'verify_peer_name'  => true,
        'peer_name'         => 'api.example.com',
        'cafile'            => '/etc/ssl/certs/ca-certificates.crt',
        'ciphers'           => 'HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5',
    ],
]);

$socket = stream_socket_client(
    'ssl://api.example.com:443',
    $errno,
    $errstr,
    timeout: 5.0,
    flags: STREAM_CLIENT_CONNECT,
    context: $context,
);

if ($socket === false) {
    throw new \RuntimeException("TLS connection failed: [$errno] $errstr");
}

// Usage is identical to a regular connection
fwrite($socket, "GET / HTTP/1.1\r\nHost: api.example.com\r\nConnection: close\r\n\r\n");

$response = stream_get_contents($socket);
fclose($socket);

TCP Server with Stream Sockets #

<?php
declare(strict_types=1);

// Create a server socket listening on port 8080
$server = stream_socket_server(
    'tcp://0.0.0.0:8080',
    $errno,
    $errstr,
    flags: STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
);

if ($server === false) {
    throw new \RuntimeException("Server creation failed: [$errno] $errstr");
}

echo "Server running on port 8080...\n";

// Main loop — accept incoming connections one by one (blocking)
while (true) {
    // stream_socket_accept() — wait for an incoming connection
    $client = stream_socket_accept($server, timeout: -1); // -1 = wait forever

    if ($client === false) {
        continue;
    }

    $clientAddr = stream_socket_get_name($client, true); // get the client IP:port
    echo "New connection from $clientAddr\n";

    // Handle the client in a separate function
    handleClient($client, $clientAddr);
}

function handleClient($socket, string $addr): void
{
    try {
        stream_set_timeout($socket, 30);

        // Read the request from the client
        $request = fgets($socket, 4096);
        if ($request === false) {
            echo "$addr: connection closed without data\n";
            return;
        }

        $request = trim($request);
        echo "$addr: '$request'\n";

        // Process and send a response
        $response = processRequest($request);
        fwrite($socket, $response . "\n");

    } finally {
        fclose($socket);
        echo "$addr: connection closed\n";
    }
}

function processRequest(string $req): string
{
    return match(strtoupper($req)) {
        'PING'  => 'PONG',
        'TIME'  => date('Y-m-d H:i:s'),
        'QUIT'  => 'BYE',
        default => "UNKNOWN: $req",
    };
}

Non-Blocking Sockets and Multiplexing #

A server that handles one client at a time is impractical. With stream_select(), a single loop can monitor many sockets at once and only process the ones that are ready:

<?php
declare(strict_types=1);

$server = stream_socket_server('tcp://0.0.0.0:8080', $errno, $errstr);
if ($server === false) {
    throw new \RuntimeException("Failed: [$errno] $errstr");
}

stream_set_blocking($server, false); // non-blocking server

$clients   = [];  // list of active client connections
$buffers   = [];  // per-client buffer

echo "Multi-client server running on :8080\n";

while (true) {
    // Build the list of sockets to monitor
    $read    = array_merge([$server], $clients);
    $write   = null;
    $except  = null;

    // stream_select() — block until at least one socket is ready
    // 0.1 second timeout so the loop doesn't spin too fast
    $active = stream_select($read, $write, $except, 0, 100_000); // 100ms

    if ($active === false) {
        throw new \RuntimeException("stream_select failed");
    }

    if ($active === 0) {
        // No activity — could do background work here
        continue;
    }

    // Check whether the server socket is ready (incoming connection)
    if (in_array($server, $read, true)) {
        $client = stream_socket_accept($server, timeout: 0);
        if ($client !== false) {
            $id             = (int) $client;
            $clients[$id]   = $client;
            $buffers[$id]   = '';
            $addr           = stream_socket_get_name($client, true);
            stream_set_blocking($client, false); // the client is also non-blocking
            echo "[$id] New connection from $addr\n";
        }
    }

    // Check clients ready to be read
    foreach ($clients as $id => $client) {
        if (!in_array($client, $read, true)) {
            continue;
        }

        $data = fread($client, 4096);

        if ($data === false || $data === '') {
            // Connection closed by the client
            echo "[$id] Connection dropped\n";
            fclose($client);
            unset($clients[$id], $buffers[$id]);
            continue;
        }

        $buffers[$id] .= $data;

        // Process if there's a complete line (delimiter \n)
        while (str_contains($buffers[$id], "\n")) {
            $pos    = strpos($buffers[$id], "\n");
            $message = substr($buffers[$id], 0, $pos);
            $buffers[$id] = substr($buffers[$id], $pos + 1);

            echo "[$id] Received: " . trim($message) . "\n";

            $resp = processRequest(trim($message)) . "\n";
            fwrite($client, $resp);

            if (strtoupper(trim($message)) === 'QUIT') {
                fclose($client);
                unset($clients[$id], $buffers[$id]);
                break;
            }
        }
    }
}

UDP Sockets #

UDP (User Datagram Protocol) has no connection — each message is sent independently without a handshake. Suitable for DNS, logging, games, and data where occasional loss is acceptable:

<?php
// UDP Server
$server = stream_socket_server(
    'udp://0.0.0.0:9999',
    $errno,
    $errstr,
    flags: STREAM_SERVER_BIND,
);

echo "UDP Server running on :9999\n";

while (true) {
    // stream_socket_recvfrom — read a UDP datagram along with the sender address
    $data   = stream_socket_recvfrom($server, 65535, 0, $from);
    if ($data === false) continue;

    echo "From $from: $data\n";

    // Send a reply to the sender
    stream_socket_sendto($server, "OK: $data", 0, $from);
}

// UDP Client — send a message without connecting first
$client = stream_socket_client('udp://127.0.0.1:9999');

stream_socket_sendto($client, "Hello UDP Server!");
$reply = stream_socket_recvfrom($client, 65535);
echo "Reply: $reply\n";

fclose($client);

Custom Protocols over TCP #

When building a custom TCP service, you need to define a protocol — rules for how data is formatted and delimited. The two most common approaches:

Length-Prefix Protocol #

Send the message length first (4 bytes), then the message itself. The server knows exactly how many bytes to read:

<?php
class ProtocolClient
{
    private $socket;

    public function __construct(string $host, int $port)
    {
        $this->socket = stream_socket_client(
            "tcp://$host:$port",
            $errno, $errstr, 5.0
        );

        if ($this->socket === false) {
            throw new \RuntimeException("Connection failed: [$errno] $errstr");
        }
    }

    public function send(string $message): void
    {
        $length = strlen($message);
        // Pack the length as a 4-byte unsigned integer (big-endian)
        $header = pack('N', $length);
        fwrite($this->socket, $header . $message);
    }

    public function receive(): string
    {
        // Read the 4-byte header
        $header = $this->readExactly(4);
        // Unpack the length from 4 big-endian bytes
        ['length' => $length] = unpack('Nlength', $header);

        // Read exactly as many bytes as declared
        return $this->readExactly($length);
    }

    private function readExactly(int $n): string
    {
        $data = '';
        while (strlen($data) < $n) {
            $chunk = fread($this->socket, $n - strlen($data));
            if ($chunk === false || $chunk === '') {
                throw new \RuntimeException("Connection dropped while reading data");
            }
            $data .= $chunk;
        }
        return $data;
    }

    public function close(): void
    {
        fclose($this->socket);
    }
}

// Usage
$client = new ProtocolClient('127.0.0.1', 8080);
$client->send('{"action":"ping"}');
$reply = $client->receive();
echo "Reply: $reply\n"; // {"status":"pong"}
$client->close();

Delimiter-Based Protocol #

Use a special character (newline, null byte) as the message separator — simpler but unsuitable if messages can contain the delimiter:

<?php
// Send a message terminated by a newline
fwrite($socket, json_encode($data) . "\n");

// Read until the newline
$line = fgets($socket, 65536); // max 64KB per line
$message = json_decode(rtrim($line, "\n"), true);

Unix Domain Sockets #

A Unix domain socket (UDS) is a socket that uses a file system path as its address instead of an IP:port. Much faster than TCP loopback because it doesn’t go through the network stack:

<?php
$socketPath = '/tmp/myapp.sock';

// Remove an old socket if present
if (file_exists($socketPath)) {
    unlink($socketPath);
}

// UDS Server
$server = stream_socket_server("unix://$socketPath", $errno, $errstr);
chmod($socketPath, 0660); // set permissions

echo "UDS Server running at $socketPath\n";

while (true) {
    $client = stream_socket_accept($server, -1);
    if ($client === false) continue;

    $request   = fgets($client, 4096);
    $response  = processRequest(trim($request));
    fwrite($client, $response . "\n");
    fclose($client);
}

// UDS Client — connect to the socket file
$client = stream_socket_client("unix://$socketPath", $errno, $errstr, 2.0);
if ($client === false) {
    throw new \RuntimeException("UDS connection failed: [$errno] $errstr");
}

fwrite($client, "PING\n");
$reply = fgets($client, 1024);
echo trim($reply); // PONG
fclose($client);

UDS is very useful for communication between PHP-FPM and the web server (Nginx uses UDS to communicate with PHP-FPM by default in many configurations).


Connection Pools — Reusing Connections #

Opening a new TCP connection for every operation is expensive (handshake, DNS lookup). A connection pool stores already-opened connections and reuses them:

<?php
class ConnectionPool
{
    private array $idleConnections   = [];
    private array $activeConnections = [];
    private int   $maxConnections;

    public function __construct(
        private string $host,
        private int    $port,
        int            $maxConnections = 10,
    ) {
        $this->maxConnections = $maxConnections;
    }

    public function borrow(): mixed
    {
        // Try to reuse an existing idle connection
        while (!empty($this->idleConnections)) {
            $socket = array_pop($this->idleConnections);

            // Check whether the connection is still alive
            if ($this->isAlive($socket)) {
                $id = spl_object_id((object) $socket);
                $this->activeConnections[$id] = $socket;
                return $socket;
            }

            fclose($socket); // dead connection, discard it
        }

        // Create a new connection if the maximum hasn't been reached
        $total = count($this->idleConnections) + count($this->activeConnections);
        if ($total >= $this->maxConnections) {
            throw new \RuntimeException("Pool full — no connections available");
        }

        $socket = stream_socket_client(
            "tcp://{$this->host}:{$this->port}",
            $errno, $errstr, 5.0
        );

        if ($socket === false) {
            throw new \RuntimeException("Connection creation failed: [$errno] $errstr");
        }

        $id = (int) $socket;
        $this->activeConnections[$id] = $socket;
        return $socket;
    }

    public function return(mixed $socket): void
    {
        $id = (int) $socket;
        unset($this->activeConnections[$id]);

        if ($this->isAlive($socket)) {
            $this->idleConnections[] = $socket;
        } else {
            fclose($socket);
        }
    }

    private function isAlive(mixed $socket): bool
    {
        // Check whether the socket is still open with a non-blocking peek
        stream_set_blocking($socket, false);
        $data = fread($socket, 1);
        stream_set_blocking($socket, true);

        // If fread returns false or '' (and not because of non-blocking), the connection is dead
        $info = stream_get_meta_data($socket);
        return !$info['eof'];
    }

    public function closeAll(): void
    {
        foreach ([...$this->idleConnections, ...$this->activeConnections] as $socket) {
            fclose($socket);
        }
        $this->idleConnections   = [];
        $this->activeConnections = [];
    }
}

// Usage
$pool = new ConnectionPool('127.0.0.1', 8080, maxConnections: 5);

for ($i = 0; $i < 20; $i++) {
    $socket = $pool->borrow();
    try {
        fwrite($socket, "PING\n");
        $reply = fgets($socket, 1024);
        echo "Reply $i: " . trim($reply) . "\n";
    } finally {
        $pool->return($socket); // return to the pool
    }
}

$pool->closeAll();

ext-sockets — Low Level #

For cases needing deeper control (for example, setsockopt to set TCP_NODELAY, SO_KEEPALIVE, or raw sockets), use ext-sockets:

<?php
// Create a TCP socket
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($sock === false) {
    throw new \RuntimeException("socket_create failed: " . socket_strerror(socket_last_error()));
}

// Socket options
socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);    // allow port reuse
socket_set_option($sock, SOL_SOCKET, SO_KEEPALIVE, 1);    // keepalive
socket_set_option($sock, IPPROTO_TCP, TCP_NODELAY, 1);    // disable Nagle's algorithm

// Bind and listen
socket_bind($sock, '0.0.0.0', 9090);
socket_listen($sock, backlog: 128);

echo "ext-socket server running on :9090\n";

while (true) {
    $client = socket_accept($sock);
    if ($client === false) continue;

    socket_getpeername($client, $ip, $port);
    echo "Connection from $ip:$port\n";

    $data = socket_read($client, 4096, PHP_NORMAL_READ);
    socket_write($client, "Echo: " . trim($data) . "\n");
    socket_close($client);
}

socket_close($sock);

Summary #

  • Stream sockets vs ext-sockets — use stream sockets (stream_socket_*) for most needs because they’re consistent with PHP’s stream model and support TLS transparently. Use ext-sockets (socket_*) only for low-level control like setsockopt.
  • stream_socket_client to create TCP/UDP/Unix connections; stream_socket_server to create a server. For TLS, just change tcp:// to ssl:// and add an SSL context.
  • stream_select() is the key to multi-client servers — monitor many sockets at once and only process the ready ones. Combine it with stream_set_blocking(false) so no socket blocks the loop.
  • Length-prefix protocols (pack('N', $length)) are more reliable than delimiter-based ones because there’s no ambiguity if a message contains the delimiter character.
  • Unix Domain Sockets (UDS) are much faster than TCP loopback for inter-process communication on the same machine — they don’t go through the network stack at all.
  • Connection pools save TCP handshake overhead by storing and reusing already-open connections.
  • Always close sockets with fclose() or socket_close() — open sockets are a limited OS resource. Use try/finally to guarantee this happens.

← Previous: I/O   Next: Web Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact