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:#dbeafeUntuk 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 sepertisetsockopt.stream_socket_clientuntuk membuat koneksi TCP/UDP/Unix;stream_socket_serveruntuk membuat server. TLS cukup gantitcp://denganssl://dan tambahkan context SSL.stream_select()adalah kunci server multi-client — pantau banyak socket sekaligus dan hanya proses yang siap. Kombinasikan denganstream_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()atausocket_close()— open socket adalah resource OS yang terbatas. Gunakantry/finallyuntuk memastikan ini terjadi.