Socket

Socket #

Socket adalah endpoint komunikasi jaringan — pintu yang bisa mengirim dan menerima data antara dua program, baik di mesin yang sama maupun di mesin berbeda di jaringan. PHP memiliki dua cara bekerja dengan socket: stream socket via stream_socket_server/client yang menggunakan antarmuka stream standar, dan ext-sockets via fungsi socket_* yang memberikan kontrol lebih rendah level seperti C. Artikel ini membahas keduanya — kapan menggunakan masing-masing, cara membangun TCP server dan client yang benar, non-blocking socket dengan multiplexing, Unix domain socket untuk komunikasi antar proses lokal, dan pola-pola yang berguna di dunia nyata seperti simple protocol di atas TCP dan connection pooling.

Jenis Socket di PHP #

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

    B --> B1[Antarmuka stream standar\nfread / fwrite / fgets]
    B --> B2[Mudah diintegrasikan\ndengan stream filter]
    B --> B3[SSL/TLS built-in\nvia context]

    C --> C1[Level rendah\nkontrol penuh]
    C --> C2[Akses opsi socket\nsetsockopt / getsockopt]
    C --> C3[UDP lebih mudah\nsendto / recvfrom]

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

Untuk sebagian besar kebutuhan, stream socket lebih direkomendasikan karena konsisten dengan model stream PHP yang sudah familiar dan mendukung TLS secara transparan. Gunakan ext-sockets hanya jika butuh kontrol sangat rendah level atau fitur yang tidak tersedia di stream socket.


TCP Client dengan Stream Socket #

Membuat koneksi TCP ke server adalah operasi yang paling sering digunakan — berbicara ke database, API custom, atau service apapun yang menggunakan TCP:

<?php
declare(strict_types=1);

// Buka koneksi TCP ke server
$socket = stream_socket_client(
    'tcp://api.example.com:8080',
    $errno,
    $errstr,
    timeout: 5.0, // timeout koneksi dalam detik
);

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

// Set timeout untuk operasi baca/tulis (berbeda dari timeout koneksi)
stream_set_timeout($socket, 10);

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

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

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

} finally {
    fclose($socket); // selalu tutup koneksi
}

Client dengan TLS/SSL #

<?php
// Koneksi TLS — cukup ganti 'tcp://' dengan '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("Gagal koneksi TLS: [$errno] $errstr");
}

// Penggunaan identik dengan koneksi biasa
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 dengan Stream Socket #

<?php
declare(strict_types=1);

// Buat server socket yang listen di 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("Gagal buat server: [$errno] $errstr");
}

echo "Server berjalan di port 8080...\n";

// Loop utama — terima koneksi masuk satu per satu (blocking)
while (true) {
    // stream_socket_accept() — tunggu koneksi masuk
    $client = stream_socket_accept($server, timeout: -1); // -1 = tunggu selamanya

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

    $clientAddr = stream_socket_get_name($client, true); // dapatkan IP:port client
    echo "Koneksi baru dari $clientAddr\n";

    // Tangani client dalam fungsi terpisah
    tanganiClient($client, $clientAddr);
}

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

        // Baca request dari client
        $request = fgets($socket, 4096);
        if ($request === false) {
            echo "$addr: koneksi ditutup tanpa data\n";
            return;
        }

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

        // Proses dan kirim response
        $response = prosesRequest($request);
        fwrite($socket, $response . "\n");

    } finally {
        fclose($socket);
        echo "$addr: koneksi ditutup\n";
    }
}

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

Non-Blocking Socket dan Multiplexing #

Server yang menangani satu client pada satu waktu tidak praktis. Dengan stream_select(), satu loop bisa memantau banyak socket bersamaan dan hanya memproses yang siap:

<?php
declare(strict_types=1);

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

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

$clients   = [];  // daftar koneksi client aktif
$buffer    = [];  // buffer per client

echo "Server multi-client berjalan di :8080\n";

while (true) {
    // Susun daftar socket yang perlu dipantau
    $baca    = array_merge([$server], $clients);
    $tulis   = null;
    $kecuali = null;

    // stream_select() — blokir sampai minimal satu socket siap
    // timeout 0.1 detik agar loop tidak berjalan terlalu cepat
    $aktif = stream_select($baca, $tulis, $kecuali, 0, 100_000); // 100ms

    if ($aktif === false) {
        throw new \RuntimeException("stream_select gagal");
    }

    if ($aktif === 0) {
        // Tidak ada activity — bisa lakukan pekerjaan background di sini
        continue;
    }

    // Periksa apakah server socket siap (ada koneksi masuk)
    if (in_array($server, $baca, true)) {
        $client = stream_socket_accept($server, timeout: 0);
        if ($client !== false) {
            $id             = (int) $client;
            $clients[$id]   = $client;
            $buffer[$id]    = '';
            $addr           = stream_socket_get_name($client, true);
            stream_set_blocking($client, false); // client juga non-blocking
            echo "[$id] Koneksi baru dari $addr\n";
        }
    }

    // Periksa client yang siap dibaca
    foreach ($clients as $id => $client) {
        if (!in_array($client, $baca, true)) {
            continue;
        }

        $data = fread($client, 4096);

        if ($data === false || $data === '') {
            // Koneksi ditutup client
            echo "[$id] Koneksi terputus\n";
            fclose($client);
            unset($clients[$id], $buffer[$id]);
            continue;
        }

        $buffer[$id] .= $data;

        // Proses jika ada baris lengkap (delimiter \n)
        while (str_contains($buffer[$id], "\n")) {
            $pos    = strpos($buffer[$id], "\n");
            $pesan  = substr($buffer[$id], 0, $pos);
            $buffer[$id] = substr($buffer[$id], $pos + 1);

            echo "[$id] Terima: " . trim($pesan) . "\n";

            $resp = prosesRequest(trim($pesan)) . "\n";
            fwrite($client, $resp);

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

UDP Socket #

UDP (User Datagram Protocol) tidak memiliki koneksi — setiap pesan dikirim secara independen tanpa handshake. Cocok untuk DNS, logging, game, dan data yang tidak masalah jika sesekali hilang:

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

echo "UDP Server berjalan di :9999\n";

while (true) {
    // stream_socket_recvfrom — baca datagram UDP beserta alamat pengirim
    $data   = stream_socket_recvfrom($server, 65535, 0, $from);
    if ($data === false) continue;

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

    // Kirim reply ke pengirim
    stream_socket_sendto($server, "OK: $data", 0, $from);
}

// UDP Client — kirim pesan tanpa koneksi dulu
$client = stream_socket_client('udp://127.0.0.1:9999');

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

fclose($client);

Protokol Kustom di atas TCP #

Saat membangun service TCP kustom, kamu perlu mendefinisikan protokol — aturan bagaimana data diformat dan dipisahkan. Dua pendekatan paling umum:

Length-Prefix Protocol #

Kirim panjang pesan dulu (4 byte), lalu pesan itu sendiri. Server tahu persis berapa byte harus dibaca:

<?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("Gagal konek: [$errno] $errstr");
        }
    }

    public function kirim(string $pesan): void
    {
        $panjang = strlen($pesan);
        // Pack panjang sebagai 4-byte unsigned integer (big-endian)
        $header = pack('N', $panjang);
        fwrite($this->socket, $header . $pesan);
    }

    public function terima(): string
    {
        // Baca 4 byte header
        $header = $this->bacaPersis(4);
        // Unpack panjang dari 4 byte big-endian
        ['panjang' => $panjang] = unpack('Npanjang', $header);

        // Baca persis sebanyak panjang yang dinyatakan
        return $this->bacaPersis($panjang);
    }

    private function bacaPersis(int $n): string
    {
        $data = '';
        while (strlen($data) < $n) {
            $chunk = fread($this->socket, $n - strlen($data));
            if ($chunk === false || $chunk === '') {
                throw new \RuntimeException("Koneksi terputus saat baca data");
            }
            $data .= $chunk;
        }
        return $data;
    }

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

// Penggunaan
$client = new ProtocolClient('127.0.0.1', 8080);
$client->kirim('{"action":"ping"}');
$reply = $client->terima();
echo "Reply: $reply\n"; // {"status":"pong"}
$client->tutup();

Delimiter-Based Protocol #

Gunakan karakter khusus (newline, null byte) sebagai pemisah pesan — lebih sederhana tapi tidak cocok jika pesan bisa mengandung delimiter:

<?php
// Kirim pesan diakhiri newline
fwrite($socket, json_encode($data) . "\n");

// Baca sampai newline
$baris = fgets($socket, 65536); // max 64KB per baris
$pesan = json_decode(rtrim($baris, "\n"), true);

Unix Domain Socket #

Unix domain socket (UDS) adalah socket yang menggunakan path file sistem sebagai alamat alih-alih IP:port. Jauh lebih cepat dari TCP loopback karena tidak melalui network stack:

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

// Hapus socket lama jika ada
if (file_exists($socketPath)) {
    unlink($socketPath);
}

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

echo "UDS Server berjalan di $socketPath\n";

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

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

// UDS Client — koneksi ke socket file
$client = stream_socket_client("unix://$socketPath", $errno, $errstr, 2.0);
if ($client === false) {
    throw new \RuntimeException("Gagal konek UDS: [$errno] $errstr");
}

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

UDS sangat berguna untuk komunikasi antara PHP-FPM dan web server (Nginx menggunakan UDS untuk berkomunikasi dengan PHP-FPM secara default di banyak konfigurasi).


Connection Pool — Reuse Koneksi #

Membuka koneksi TCP baru untuk setiap operasi mahal (handshake, DNS lookup). Connection pool menyimpan koneksi yang sudah dibuka dan menggunakannya kembali:

<?php
class ConnectionPool
{
    private array $koneksiIdle    = [];
    private array $koneksiAktif  = [];
    private int   $maxKoneksi;

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

    public function pinjam(): mixed
    {
        // Coba gunakan koneksi idle yang sudah ada
        while (!empty($this->koneksiIdle)) {
            $socket = array_pop($this->koneksiIdle);

            // Periksa apakah koneksi masih hidup
            if ($this->masihHidup($socket)) {
                $id = spl_object_id((object) $socket);
                $this->koneksiAktif[$id] = $socket;
                return $socket;
            }

            fclose($socket); // koneksi mati, buang
        }

        // Buat koneksi baru jika belum mencapai maksimum
        $total = count($this->koneksiIdle) + count($this->koneksiAktif);
        if ($total >= $this->maxKoneksi) {
            throw new \RuntimeException("Pool penuh — tidak ada koneksi tersedia");
        }

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

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

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

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

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

    private function masihHidup(mixed $socket): bool
    {
        // Cek apakah socket masih terbuka dengan non-blocking peek
        stream_set_blocking($socket, false);
        $data = fread($socket, 1);
        stream_set_blocking($socket, true);

        // Jika fread mengembalikan false atau '' (dan bukan karena non-blocking), koneksi mati
        $info = stream_get_meta_data($socket);
        return !$info['eof'];
    }

    public function tutupSemua(): void
    {
        foreach ([...$this->koneksiIdle, ...$this->koneksiAktif] as $socket) {
            fclose($socket);
        }
        $this->koneksiIdle   = [];
        $this->koneksiAktif  = [];
    }
}

// Penggunaan
$pool = new ConnectionPool('127.0.0.1', 8080, maxKoneksi: 5);

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

$pool->tutupSemua();

ext-sockets — Level Rendah #

Untuk kasus yang butuh kontrol lebih dalam (misalnya setsockopt untuk mengatur TCP_NODELAY, SO_KEEPALIVE, atau raw socket), gunakan ext-sockets:

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

// Opsi socket
socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);    // izinkan reuse port
socket_set_option($sock, SOL_SOCKET, SO_KEEPALIVE, 1);    // keepalive
socket_set_option($sock, IPPROTO_TCP, TCP_NODELAY, 1);    // matikan Nagle algorithm

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

echo "Server ext-socket berjalan di :9090\n";

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

    socket_getpeername($client, $ip, $port);
    echo "Koneksi dari $ip:$port\n";

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

socket_close($sock);

Ringkasan #

  • Stream socket vs ext-sockets — gunakan stream socket (stream_socket_*) untuk sebagian besar kebutuhan karena konsisten dengan model stream PHP dan mendukung TLS secara transparan. Gunakan ext-sockets (socket_*) hanya untuk kontrol level rendah seperti setsockopt.
  • stream_socket_client untuk membuat koneksi TCP/UDP/Unix; stream_socket_server untuk membuat server. TLS cukup ganti tcp:// dengan ssl:// dan tambahkan context SSL.
  • stream_select() adalah kunci server multi-client — pantau banyak socket sekaligus dan hanya proses yang siap. Kombinasikan dengan stream_set_blocking(false) agar tidak ada socket yang memblok loop.
  • Length-prefix protocol (pack('N', $panjang)) lebih andal dari delimiter-based karena tidak ada ambiguitas jika pesan mengandung karakter delimiter.
  • Unix Domain Socket (UDS) jauh lebih cepat dari TCP loopback untuk komunikasi antar proses di mesin yang sama — tidak melalui network stack sama sekali.
  • Connection pool menghemat overhead TCP handshake dengan menyimpan dan menggunakan kembali koneksi yang sudah terbuka.
  • Selalu tutup socket dengan fclose() atau socket_close() — open socket adalah resource OS yang terbatas. Gunakan try/finally untuk memastikan ini terjadi.

← Sebelumnya: I/O   Berikutnya: Web Socket →

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