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:#dbeafeFor 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 likesetsockopt.stream_socket_clientto create TCP/UDP/Unix connections;stream_socket_serverto create a server. For TLS, just changetcp://tossl://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 withstream_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()orsocket_close()— open sockets are a limited OS resource. Usetry/finallyto guarantee this happens.