Using Sockets in PHP

Published Updated

Most PHP applications should not hand-write HTTP requests over raw sockets. Use an HTTP client, cURL, a provider SDK, or framework tooling for ordinary web API calls. Use sockets when the protocol is the point: WHOIS, Redis-style line protocols, a private TCP service, a Unix socket, a UDP message, or a long-running service that really owns its event loop.

Think of a socket as an open phone line. The protocol is the agreed script, each write speaks part of it, each read waits for the reply, and the timeout is the rule for hanging up. Leaving a learning exercise in a real request path means the whole page can wait on that line.

Sockets expose timeouts, blocking reads, TLS choices, partial writes, connection limits, and errors that an HTTP library normally owns. One careless blocking call can hold a PHP worker until another server responds.

Guide Path

Read this after building a mini chat app with PHP if you need to understand the lower transport layer underneath polling, SSE, WebSockets, and custom protocols. Use build your own API with PHP when the contract should stay at HTTP and JSON instead.

Streams First, Raw Sockets Later

For this topic, PHP has two related layers:

  • Streams generalize readable and writable resources: files, network connections, compression wrappers, and protocol wrappers.
  • The sockets extension exposes a lower-level BSD sockets interface for client and server work.

For most client-side network work, start with streams. PHP's own sockets manual points readers toward stream_socket_client(), stream_socket_server(), fsockopen(), and pfsockopen() for a more generic client-side socket interface, which is the practical hint hiding in the manual.

fsockopen() still exists and still opens Internet or Unix domain socket connections. stream_socket_client() gives you the same basic shape with richer options, including contexts and asynchronous connection flags.

A Small TCP Client

The shape of a client is always the same: connect, set a read timeout, write the protocol request, read the response, then close the stream.

<?php

function writeAll($stream, string $data): void
{
    $offset = 0;
    $length = strlen($data);

    while ($offset < $length) {
        $written = fwrite($stream, substr($data, $offset));

        if ($written === false || $written === 0) {
            throw new RuntimeException("The socket write did not make progress.");
        }

        $offset += $written;
    }
}

$errorCode = 0;
$errorMessage = "";

$socket = stream_socket_client(
    "tcp://whois.iana.org:43",
    $errorCode,
    $errorMessage,
    timeout: 5,
);

if ($socket === false) {
    throw new RuntimeException("Connection failed: {$errorMessage}", $errorCode);
}

stream_set_timeout($socket, 5);

writeAll($socket, "example.com\r\n");

$response = stream_get_contents($socket);
$meta = stream_get_meta_data($socket);

fclose($socket);

if ($meta["timed_out"]) {
    throw new RuntimeException("WHOIS lookup timed out.");
}

echo htmlspecialchars($response, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");

The parts that matter are all visible in that WHOIS lookup: an explicit timeout, a checked failure path, output escaping, and no extract($_POST) anywhere near the network call.

The connection timeout only covers the connection attempt. PHP's documentation calls out that read and write timeouts need stream_set_timeout(), which is the kind of detail old socket examples often skipped.

The phone line is open once the connection succeeds, but that does not guarantee a timely reply. The separate read timeout supplies the hang-up rule for the conversation that follows.

Manual HTTP

Building a GET or HEAD request by hand and looping over the result explains the wire, while production HTTP belongs in an HTTP client.

If you do write the request manually, make the boundaries visible:

<?php

function writeAll($stream, string $data): void
{
    $offset = 0;
    $length = strlen($data);

    while ($offset < $length) {
        $written = fwrite($stream, substr($data, $offset));

        if ($written === false || $written === 0) {
            throw new RuntimeException("The socket write did not make progress.");
        }

        $offset += $written;
    }
}

$host = "example.com";
$path = "/";

$socket = stream_socket_client("tls://{$host}:443", $errorCode, $errorMessage, 5);

if ($socket === false) {
    throw new RuntimeException("Connection failed: {$errorMessage}", $errorCode);
}

stream_set_timeout($socket, 5);

$request = implode("\r\n", [
    "GET {$path} HTTP/1.1",
    "Host: {$host}",
    "User-Agent: CodeWalkersSocketExample/1.0",
    "Connection: close",
    "",
    "",
]);

writeAll($socket, $request);

$response = stream_get_contents($socket);

fclose($socket);

That example deliberately uses tls:// on port 443. Plain port 80 examples are still easy to find, and they are a bad default. If credentials, cookies, tokens, phone numbers, or user content cross the connection, the transport has to be encrypted and verified.

For application work, the better answer is usually simpler: do not build the HTTP request yourself. If you are calling an SMS provider, read sending SMS with HTTP APIs. If you are exposing your own service, read build your own API with PHP and keep the contract at the HTTP level.

Blocking Is the Hidden Cost

Socket code often looks small because the waiting is invisible. A blocking read can hold the PHP worker until the remote server responds, the timeout fires, or the process runs out of patience. On a low-traffic admin script that may be acceptable. On a request path that many users hit, it can quietly tie up the whole application.

Use short timeouts and treat remote calls as unreliable. If the feature can run later, put it behind a queue or background job. If many sockets have to be watched at once, you are moving toward stream_select(), non-blocking mode, or an event loop. At that point, the application is no longer a normal request-response PHP page.

A command-line socket script can behave differently under PHP-FPM, behind a proxy, or inside a container. Test the same network path and timeout behavior in deployment.

Raw Sockets

Use the lower-level sockets extension when you need control over the socket itself: domain, type, protocol, bind address, datagram behavior, server accept loops, Unix sockets, or protocol details that streams do not expose cleanly.

PHP streams are part of the core runtime, while the socket_* functions require the optional sockets extension compiled with --enable-sockets. Check function_exists("socket_create") before using those lower-level functions.

The socket_create() API makes those choices explicit:

<?php

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

if ($socket === false) {
    throw new RuntimeException(socket_strerror(socket_last_error()));
}

That code moves below the stream abstraction. The PHP manual describes SOCK_STREAM as the reliable, connection-based byte stream that TCP uses, while SOCK_DGRAM is the fixed-length datagram shape that UDP uses. Features that depend on those distinctions belong at this layer.

Raw sockets make sense when PHP is acting more like infrastructure than a page renderer: a small protocol client, a local Unix-socket adapter, a test client for a private service, or a long-running worker. They are the wrong shape for ordinary JSON APIs, basic web scraping, or provider integrations that already publish an HTTP contract.

Be Careful with Long-running Servers

PHP can create listening sockets, and the resulting server process has a long-running lifecycle. It has to survive past one request, clean up memory, handle signals, watch file descriptors, and keep logs that explain what happened after the browser is gone.

If the goal is browser real-time behavior, do not confuse three different jobs:

  • Polling uses ordinary HTTP requests.
  • Server-Sent Events keep a one-way server-to-browser stream open.
  • WebSockets keep a persistent two-way connection open.

The mini chat app covers that product-level choice. Raw PHP sockets sit underneath those browser-facing transports.

Common Pitfalls & Debugging

stream_socket_client() Returns False

Symptom: the client never obtains a stream resource. Cause: DNS, the host, the port, the transport, outbound network policy, or the remote listener failed before the protocol exchange began. Fix: capture both error arguments, log the destination without secrets, and test connectivity from the deployed runtime.

The Read Hangs After Connection

Symptom: the connection succeeds but the request waits until the worker limit is reached. Cause: the connection timeout covered only the initial call, while the later read remained blocking. Fix: call stream_set_timeout(), inspect stream_get_meta_data(), and handle timed_out as a named remote-service failure.

The Manual HTTP Response Is Incomplete

Symptom: the response appears truncated, redirects are missed, or a valid server reply is parsed incorrectly. Cause: the hand-written client assumes connection close is the only message boundary and ignores HTTP behavior. Fix: use an HTTP client for application requests and keep manual HTTP as a bounded protocol exercise.

Frequently Asked Questions

Does a socket close on its own when the script ends?

Yes. PHP releases open stream and socket resources when a script finishes, even after a fatal error. Relying on that instead of closing explicitly makes it harder to control exactly when the other side sees the connection go.

Can PHP stream sockets use TLS?

Yes, a stream_socket_client address can use a TLS transport such as tls://host:443, with a stream context when certificate or connection options need configuration. Encryption still requires correct peer verification, hostname handling, timeouts, and an application protocol that both endpoints understand.

Can a PHP script listen on a port below 1024?

Only with the privilege to bind one, normally root or an explicitly granted capability. PHP web workers run unprivileged, which is another reason application-level listeners are unusual on ordinary hosting.

What is the difference between fsockopen and stream_socket_client?

stream_socket_client is the newer interface and accepts a stream context, so timeouts, TLS options, and bind settings are configurable. fsockopen is the older, simpler wrapper that covers the common case with fewer options.

Is a long-running raw PHP socket server suitable for production?

It can run in production, but it needs process supervision, memory-leak monitoring, and a restart strategy that ordinary request-response PHP never requires. For most applications a dedicated tool such as a message queue, WebSocket server, or reverse proxy handles the long-running concern more safely.

Should PHP build raw HTTP requests manually, or use a library like cURL?

Use cURL or a well-maintained HTTP client for ordinary requests; both handle redirects, timeouts, TLS verification, and header parsing correctly. Building a manual HTTP request over a raw socket is worth understanding for what it teaches about the protocol, but production code should not depend on it.

Next Steps

Sockets are worth understanding in their own right. A socket is where the polite abstractions end and the network starts showing you what is really happening.

The recommendation is to use that power deliberately: use HTTP tools for ordinary HTTP, start with streams for custom protocols, and reach for the sockets extension only when low-level control is the feature. At that point, clear timeouts, explicit TLS, bounded reads, and logs that name the slow remote system define the implementation.

That is the line between learning the wire and accidentally rebuilding half a network client inside a controller.

This example has been updated from the original CodeWalkers sockets tutorial, which hand-built HTTP requests over fsockopen() on port 80 with no read timeout. Both halves of that pattern are why this page starts with an HTTP client and keeps stream_set_timeout() next to every connection.

Sources

  1. [1]
    PHP Sockets
    (php.net)
  2. [2]
    PHP Streams
    (php.net)
  3. [3]
  4. [4]
    PHP fsockopen
    (php.net)
  5. [5]
  6. [6]
    fwrite
    (php.net)
  7. [7]
    Installation
    (php.net)