HardТеория11 min

Сокеты и HTTP

Потоковые сокеты, HTTP через stream context, DNS функции, сетевые утилиты

PHP предоставляет низкоуровневый доступ к сетевому взаимодействию через потоковые обертки (stream sockets) и расширение Sockets. Это позволяет создавать TCP/UDP клиенты и серверы, работать с HTTP без cURL, управлять DNS и выполнять сетевую диагностику.

stream_socket_client() -- TCP/UDP подключения

<?php
declare(strict_types=1);

// Connect to TCP server
$socket = stream_socket_client(
    address: 'tcp://example.com:80',
    error_code: $errorCode,
    error_message: $errorMessage,
    timeout: 10.0,
    flags: STREAM_CLIENT_CONNECT,
);

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

// Send HTTP request manually
$request = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n";
fwrite($socket, $request);

// Read response
$response = '';
while (!feof($socket)) {
    $response .= fread($socket, 8192);
}
fclose($socket);

echo $response;

// Connect with SSL/TLS
$sslSocket = stream_socket_client(
    address: 'ssl://example.com:443',
    error_code: $errorCode,
    error_message: $errorMessage,
    timeout: 10.0,
);

if ($sslSocket === false) {
    throw new RuntimeException("SSL connection failed: $errorMessage");
}

fwrite($sslSocket, "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n");
$response = stream_get_contents($sslSocket);
fclose($sslSocket);

// Connect with context options
$context = stream_context_create([
    'ssl' => [
        'verify_peer'       => true,
        'verify_peer_name'  => true,
        'allow_self_signed' => false,
    ],
    'socket' => [
        'tcp_nodelay' => true, // Disable Nagle's algorithm
    ],
]);

$socket = stream_socket_client(
    'tls://api.example.com:443',
    $errorCode,
    $errorMessage,
    10.0,
    STREAM_CLIENT_CONNECT,
    $context,
);

// Set stream timeout after connection
stream_set_timeout($socket, 30);

// Non-blocking mode
stream_set_blocking($socket, false);

stream_socket_server() -- создание серверов

<?php
declare(strict_types=1);

// Create a TCP server
$server = stream_socket_server(
    address: 'tcp://0.0.0.0:8080',
    error_code: $errorCode,
    error_message: $errorMessage,
    flags: STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
);

if ($server === false) {
    throw new RuntimeException("Server creation failed: $errorMessage");
}

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

// Accept connections in a loop
while (true) {
    // Block until a client connects (with 60s timeout)
    $client = @stream_socket_accept($server, 60, $peerName);

    if ($client === false) {
        continue; // Timeout or error — try again
    }

    echo "Connection from: $peerName\n";

    // Read request
    $request = '';
    while (($line = fgets($client)) !== false) {
        $request .= $line;
        if (trim($line) === '') {
            break; // Empty line = end of HTTP headers
        }
    }

    // Send response
    $body = "<h1>Hello from PHP!</h1><p>Your IP: $peerName</p>";
    $response = "HTTP/1.1 200 OK\r\n"
        . "Content-Type: text/html\r\n"
        . "Content-Length: " . strlen($body) . "\r\n"
        . "Connection: close\r\n"
        . "\r\n"
        . $body;

    fwrite($client, $response);
    fclose($client);
}

fclose($server);

// UDP server (connectionless)
$udpServer = stream_socket_server(
    'udp://0.0.0.0:9000',
    $errorCode,
    $errorMessage,
    STREAM_SERVER_BIND,
);

while (true) {
    $data = stream_socket_recvfrom($udpServer, 65535, 0, $peer);
    echo "Received from $peer: $data\n";
    stream_socket_sendto($udpServer, "Echo: $data", 0, $peer);
}

file_get_contents() с HTTP контекстом

<?php
declare(strict_types=1);

// Simple GET request (simplest HTTP client in PHP!)
$html = file_get_contents('https://example.com');

// GET with custom headers
$context = stream_context_create([
    'http' => [
        'method'  => 'GET',
        'header'  => "Accept: application/json\r\nAuthorization: Bearer my-token\r\n",
        'timeout' => 10,
    ],
]);

$response = file_get_contents('https://api.example.com/users', context: $context);
$data = json_decode($response, true);

// POST with JSON body
$postData = json_encode(['name' => 'John', 'email' => '[email protected]']);

$context = stream_context_create([
    'http' => [
        'method'  => 'POST',
        'header'  => "Content-Type: application/json\r\nAccept: application/json\r\n",
        'content' => $postData,
        'timeout' => 30,
    ],
]);

$response = file_get_contents('https://api.example.com/users', context: $context);

// Access response headers via $http_response_header (magic variable)
// Populated after file_get_contents() with HTTP wrapper
echo $http_response_header[0]; // 'HTTP/1.1 200 OK'

// Parse status code from response headers
function getStatusCode(array $headers): int
{
    // $headers[0] = 'HTTP/1.1 200 OK'
    if (preg_match('/HTTP\/[\d.]+\s+(\d+)/', $headers[0], $matches)) {
        return (int) $matches[1];
    }
    return 0;
}

$statusCode = getStatusCode($http_response_header);

// Handle errors — file_get_contents returns false on HTTP errors by default
// Use ignore_errors to get response body even on 4xx/5xx
$context = stream_context_create([
    'http' => [
        'method'        => 'GET',
        'ignore_errors' => true, // Don't trigger warning on 4xx/5xx
        'timeout'       => 10,
    ],
]);

$response = file_get_contents('https://api.example.com/not-found', context: $context);
$statusCode = getStatusCode($http_response_header);

if ($statusCode === 404) {
    echo "Resource not found\n";
}

// PUT request
$context = stream_context_create([
    'http' => [
        'method'  => 'PUT',
        'header'  => "Content-Type: application/json\r\n",
        'content' => json_encode(['status' => 'active']),
    ],
]);

$response = file_get_contents('https://api.example.com/users/42', context: $context);

stream_context_create() -- полные опции

<?php
declare(strict_types=1);

$context = stream_context_create([
    // HTTP/HTTPS options
    'http' => [
        'method'           => 'POST',
        'header'           => "Content-Type: application/json\r\nX-Custom: value\r\n",
        'content'          => '{"key":"value"}',
        'user_agent'       => 'MyApp/1.0',
        'timeout'          => 30,        // Read timeout (seconds, float)
        'max_redirects'    => 5,         // Follow redirects (default: 20)
        'follow_location'  => 1,         // Follow redirects (default: 1)
        'ignore_errors'    => true,      // Fetch body on 4xx/5xx
        'protocol_version' => '1.1',     // HTTP version
        'proxy'            => 'tcp://proxy.example.com:8080',
        'request_fulluri'  => true,      // Send full URI in request line (for proxies)
    ],

    // SSL/TLS options
    'ssl' => [
        'verify_peer'       => true,
        'verify_peer_name'  => true,
        'allow_self_signed' => false,
        'cafile'            => '/etc/ssl/certs/ca-certificates.crt',
        'local_cert'        => '/path/to/client-cert.pem',  // Client certificate
        'local_pk'          => '/path/to/client-key.pem',   // Client private key
        'peer_name'         => 'example.com',               // Expected CN
        'crypto_method'     => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT,
    ],

    // Socket options
    'socket' => [
        'bindto'      => '0.0.0.0:0',    // Local address to bind
        'tcp_nodelay' => true,             // Disable Nagle
    ],
]);

fsockopen() -- низкоуровневые соединения

<?php
declare(strict_types=1);

// TCP connection
$socket = fsockopen('example.com', 80, $errorCode, $errorMessage, 10);

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

// Send raw HTTP
fwrite($socket, "GET /api/data HTTP/1.1\r\n");
fwrite($socket, "Host: example.com\r\n");
fwrite($socket, "Connection: close\r\n");
fwrite($socket, "\r\n");

// Read response
$response = '';
while (!feof($socket)) {
    $response .= fgets($socket, 4096);
}
fclose($socket);

// SSL connection via fsockopen
$sslSocket = fsockopen('ssl://example.com', 443, $errorCode, $errorMessage, 10);

// Persistent connection (reused across requests in same process)
$persistent = pfsockopen('example.com', 80, $errorCode, $errorMessage, 10);

// Check if connection is still alive
$meta = stream_get_meta_data($persistent);
if ($meta['timed_out'] || $meta['eof']) {
    fclose($persistent);
    $persistent = pfsockopen('example.com', 80, $errorCode, $errorMessage, 10);
}

Socket extension -- полный контроль

<?php
declare(strict_types=1);

// Low-level socket API (ext-sockets)
// More control than stream functions, but more complex

// Create TCP socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

if ($socket === false) {
    throw new RuntimeException('Socket creation failed: ' . socket_strerror(socket_last_error()));
}

// Set options
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 10, 'usec' => 0]);
socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, ['sec' => 10, 'usec' => 0]);
socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);

// Connect
$result = socket_connect($socket, 'example.com', 80);

if ($result === false) {
    throw new RuntimeException('Connection failed: ' . socket_strerror(socket_last_error($socket)));
}

// Send data
$request = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n";
socket_write($socket, $request, strlen($request));

// Receive data
$response = '';
while ($chunk = socket_read($socket, 8192)) {
    $response .= $chunk;
}

socket_close($socket);

// === TCP server with socket extension ===
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '0.0.0.0', 8080);
socket_listen($server, 5); // Backlog of 5 connections

echo "Listening on :8080\n";

while (true) {
    $client = socket_accept($server);

    socket_getpeername($client, $clientIp, $clientPort);
    echo "Client connected: $clientIp:$clientPort\n";

    $data = socket_read($client, 4096);
    echo "Received: $data\n";

    socket_write($client, "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK");
    socket_close($client);
}

socket_close($server);

// === Select — multiplex I/O on multiple sockets ===
$read = [$socket1, $socket2, $socket3];
$write = null;
$except = null;

// Wait for any socket to become readable (up to 5 seconds)
$ready = socket_select($read, $write, $except, 5);

if ($ready > 0) {
    foreach ($read as $readySocket) {
        $data = socket_read($readySocket, 8192);
        // Process data from whichever socket is ready
    }
}

DNS функции

<?php
declare(strict_types=1);

// Resolve hostname to IP
$ip = gethostbyname('example.com');
echo $ip; // '93.184.216.34'

// If resolution fails, returns the hostname itself (!)
$result = gethostbyname('nonexistent.invalid');
if ($result === 'nonexistent.invalid') {
    echo "DNS resolution failed\n";
}

// Resolve IP to hostname (reverse DNS)
$hostname = gethostbyaddr('8.8.8.8');
echo $hostname; // 'dns.google'

// Get all IPs for hostname
$ips = gethostbynamel('google.com');
// ['142.250.80.14', '142.250.80.46', ...]

// Full DNS records
$records = dns_get_record('example.com', DNS_ALL);

foreach ($records as $record) {
    match ($record['type']) {
        'A'     => printf("A     %s -> %s (TTL: %d)\n", $record['host'], $record['ip'], $record['ttl']),
        'AAAA'  => printf("AAAA  %s -> %s\n", $record['host'], $record['ipv6']),
        'MX'    => printf("MX    %s -> %s (pri: %d)\n", $record['host'], $record['target'], $record['pri']),
        'CNAME' => printf("CNAME %s -> %s\n", $record['host'], $record['target']),
        'TXT'   => printf("TXT   %s -> %s\n", $record['host'], $record['txt']),
        'NS'    => printf("NS    %s -> %s\n", $record['host'], $record['target']),
        'SOA'   => printf("SOA   %s (mname: %s, serial: %d)\n", $record['host'], $record['mname'], $record['serial']),
        default => printf("%s record for %s\n", $record['type'], $record['host']),
    };
}

// Query specific record types
$mxRecords = dns_get_record('example.com', DNS_MX);
$txtRecords = dns_get_record('example.com', DNS_TXT);
$nsRecords = dns_get_record('example.com', DNS_NS);

// Check if DNS record exists (useful for email validation)
$hasMx = checkdnsrr('example.com', 'MX');
$hasA = checkdnsrr('example.com', 'A');

// Email domain validation
function isValidEmailDomain(string $email): bool
{
    $parts = explode('@', $email);
    if (count($parts) !== 2) {
        return false;
    }

    $domain = $parts[1];
    return checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A');
}

Сетевые функции

<?php
declare(strict_types=1);

// IP address manipulation
$long = ip2long('192.168.1.1');
echo $long; // 3232235777

$ip = long2ip(3232235777);
echo $ip; // '192.168.1.1'

// IP range check
function isInSubnet(string $ip, string $subnet): bool
{
    [$network, $bits] = explode('/', $subnet);
    $ipLong = ip2long($ip);
    $networkLong = ip2long($network);
    $mask = -1 << (32 - (int) $bits);

    return ($ipLong & $mask) === ($networkLong & $mask);
}

echo isInSubnet('192.168.1.100', '192.168.1.0/24') ? 'Yes' : 'No'; // Yes
echo isInSubnet('10.0.0.1', '192.168.1.0/24') ? 'Yes' : 'No';      // No

// IPv6 support
$packed = inet_pton('::1');                // Convert to binary format
$readable = inet_ntop($packed);            // Convert back to readable
echo $readable; // '::1'

$packedV4 = inet_pton('192.168.1.1');      // Also works with IPv4
$readableV4 = inet_ntop($packedV4);        // '192.168.1.1'

// Get network interfaces (PHP 8.3+ socket ext)
// Useful for finding local IP addresses

// Check if port is open
function isPortOpen(string $host, int $port, float $timeout = 2.0): bool
{
    $socket = @fsockopen($host, $port, $errorCode, $errorMessage, $timeout);

    if ($socket === false) {
        return false;
    }

    fclose($socket);
    return true;
}

echo isPortOpen('example.com', 443) ? 'Port 443 open' : 'Port 443 closed';
echo isPortOpen('example.com', 8080) ? 'Port 8080 open' : 'Port 8080 closed';

// Get service name by port
$service = getservbyport(443, 'tcp'); // 'https'
$service = getservbyport(80, 'tcp');  // 'http'
$service = getservbyport(22, 'tcp');  // 'ssh'

// Get port by service name
$port = getservbyname('https', 'tcp'); // 443
$port = getservbyname('ssh', 'tcp');   // 22

Практический пример: HTTP клиент без cURL

<?php
declare(strict_types=1);

final class StreamHttpClient
{
    public function __construct(
        private readonly int $timeout = 30,
        private readonly string $userAgent = 'StreamHTTP/1.0',
    ) {}

    public function get(string $url, array $headers = []): array
    {
        return $this->request('GET', $url, $headers);
    }

    public function post(string $url, string $body, array $headers = []): array
    {
        return $this->request('POST', $url, $headers, $body);
    }

    private function request(string $method, string $url, array $headers = [], ?string $body = null): array
    {
        $headerString = '';
        foreach ($headers as $name => $value) {
            $headerString .= "$name: $value\r\n";
        }

        $options = [
            'http' => [
                'method'        => $method,
                'header'        => $headerString . "User-Agent: {$this->userAgent}\r\n",
                'timeout'       => $this->timeout,
                'ignore_errors' => true,
            ],
            'ssl' => [
                'verify_peer'      => true,
                'verify_peer_name' => true,
            ],
        ];

        if ($body !== null) {
            $options['http']['content'] = $body;
        }

        $context = stream_context_create($options);
        $responseBody = @file_get_contents($url, false, $context);

        if ($responseBody === false) {
            $error = error_get_last();
            throw new RuntimeException("HTTP request failed: " . ($error['message'] ?? 'unknown'));
        }

        // Parse status code
        $statusLine = $http_response_header[0] ?? '';
        preg_match('/HTTP\/[\d.]+\s+(\d+)\s+(.*)/', $statusLine, $matches);

        return [
            'status'  => (int) ($matches[1] ?? 0),
            'reason'  => $matches[2] ?? '',
            'headers' => $this->parseHeaders($http_response_header),
            'body'    => $responseBody,
        ];
    }

    private function parseHeaders(array $rawHeaders): array
    {
        $headers = [];
        foreach ($rawHeaders as $header) {
            $parts = explode(':', $header, 2);
            if (count($parts) === 2) {
                $headers[strtolower(trim($parts[0]))] = trim($parts[1]);
            }
        }
        return $headers;
    }
}

// Usage
$client = new StreamHttpClient(timeout: 10);

$response = $client->get('https://api.example.com/users', [
    'Accept'        => 'application/json',
    'Authorization' => 'Bearer my-token',
]);

if ($response['status'] === 200) {
    $users = json_decode($response['body'], true);
}

Практический пример: TCP Echo-сервер

<?php
declare(strict_types=1);

function runEchoServer(string $host, int $port): never
{
    $server = stream_socket_server(
        "tcp://$host:$port",
        $errorCode,
        $errorMessage,
    );

    if ($server === false) {
        throw new RuntimeException("Cannot start server: $errorMessage");
    }

    echo "Echo server running on $host:$port\n";

    $clients = [$server];

    while (true) {
        $read = $clients;
        $write = null;
        $except = null;

        // Wait for activity on any socket
        $ready = stream_select($read, $write, $except, 1);

        if ($ready === false) {
            break;
        }

        foreach ($read as $socket) {
            if ($socket === $server) {
                // New connection
                $client = stream_socket_accept($server, 0, $peerName);
                if ($client !== false) {
                    $clients[] = $client;
                    stream_set_blocking($client, false);
                    echo "Connected: $peerName\n";
                    fwrite($client, "Welcome to Echo Server!\n");
                }
            } else {
                // Data from existing client
                $data = fread($socket, 8192);

                if ($data === '' || $data === false) {
                    // Client disconnected
                    $key = array_search($socket, $clients, true);
                    unset($clients[$key]);
                    fclose($socket);
                    echo "Client disconnected\n";
                } else {
                    // Echo data back
                    fwrite($socket, "Echo: $data");
                }
            }
        }
    }

    fclose($server);
}

// runEchoServer('0.0.0.0', 9000);

Практический пример: DNS Lookup утилита

<?php
declare(strict_types=1);

function dnsLookup(string $domain): void
{
    echo "=== DNS Lookup: $domain ===\n\n";

    // A records (IPv4)
    $aRecords = dns_get_record($domain, DNS_A);
    echo "A Records:\n";
    foreach ($aRecords as $record) {
        echo "  {$record['ip']} (TTL: {$record['ttl']}s)\n";
    }

    // AAAA records (IPv6)
    $aaaaRecords = dns_get_record($domain, DNS_AAAA);
    echo "\nAAAA Records:\n";
    foreach ($aaaaRecords as $record) {
        echo "  {$record['ipv6']} (TTL: {$record['ttl']}s)\n";
    }

    // MX records
    $mxRecords = dns_get_record($domain, DNS_MX);
    usort($mxRecords, fn($a, $b) => $a['pri'] <=> $b['pri']);
    echo "\nMX Records:\n";
    foreach ($mxRecords as $record) {
        echo "  [{$record['pri']}] {$record['target']}\n";
    }

    // NS records
    $nsRecords = dns_get_record($domain, DNS_NS);
    echo "\nNS Records:\n";
    foreach ($nsRecords as $record) {
        echo "  {$record['target']}\n";
    }

    // TXT records (SPF, DKIM, etc.)
    $txtRecords = dns_get_record($domain, DNS_TXT);
    echo "\nTXT Records:\n";
    foreach ($txtRecords as $record) {
        echo "  {$record['txt']}\n";
    }

    // Reverse DNS
    foreach ($aRecords as $record) {
        $reverse = gethostbyaddr($record['ip']);
        echo "\nReverse DNS ({$record['ip']}): $reverse\n";
    }
}

// dnsLookup('google.com');

Тесты

Вопросы с экзамена ZCE

Проверь себя

5 из 16

Какой из следующих протоколов используется для передачи почты?

Что возвращает gethostbyname() если DNS-разрешение не удалось?

Какие из следующих являются методами создания веб-сервисов?

Выберите все правильные варианты

Для чего используется stream_select()?

Как получить HTTP заголовки ответа при использовании file_get_contents()?