Socket 是網路通訊的基礎抽象。PHP 封裝 Berkeley Socket API,支援 TCP/UDP、IPv4/IPv6、Unix Domain Socket。本文系統講解 Socket 三要素(協定族、型別、協定)、核心函式流程,並逐步實作從單連線回聲伺服端到支援多客戶端的簡易 HTTP 伺服端。
| 層面 | 常數 | 說明 |
|---|---|---|
| 協定族 (Domain) | AF_INET | IPv4,最常用 |
| AF_INET6 | IPv6 | |
| AF_UNIX | 本地進程間通訊 (IPC),效能最高 | |
| Socket 型別 | SOCK_STREAM | 串流、可靠、有序、TCP |
| SOCK_DGRAM | 資料報、不可靠、無序、UDP | |
| SOCK_RAW | 原始封包、ICMP (ping/traceroute) | |
| 協定 | SOL_TCP / IPPROTO_TCP | TCP 協定 |
| SOL_UDP / IPPROTO_UDP | UDP 協定 | |
| SOL_ICMP | ICMP 協定 |
$proto = getprotobyname('tcp'); // 或 SOL_TCP
$socket = socket_create(AF_INET, SOCK_STREAM, $proto);
socket_bind($socket, '0.0.0.0', 1337); // 監聽所有介面
socket_listen($socket, SOMAXCONN); // 佇列長度
while (true) {
$client = socket_accept($socket); // 阻塞等待
// 處理 $client...
socket_close($client);
}
socket_close($socket);
單執行緒循環接受、處理、關閉,無法並發。改用 socket_select 多路複用:
<?php
// server_multi.php
set_time_limit(0);
$master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($master, '0.0.0.0', 1337);
socket_listen($master, SOMAXCONN);
socket_set_nonblock($master); // 關鍵:非阻塞
$clients = [$master => $master];
$buffers = [];
echo "多客戶端伺服端啟動於 0.0.0.0:1337" . PHP_EOL;
while (true) {
$read = $clients;
$write = $except = null;
$num = @socket_select($read, $write, $except, 1); // 1秒超時
if ($num === false) continue;
foreach ($read as $sock) {
if ($sock === $master) {
// 新連線
$client = socket_accept($master);
if ($client !== false) {
socket_set_nonblock($client);
$clients[(int)$client] = $client;
$buffers[(int)$client] = '';
echo "新客戶端連入: " . (int)$client . PHP_EOL;
}
} else {
// 資料到達
$data = @socket_read($sock, 4096, PHP_BINARY_READ);
if ($data === false || $data === '') {
// 斷線
$id = (int)$sock;
unset($clients[$id], $buffers[$id]);
socket_close($sock);
echo "客戶端 $id 斷開" . PHP_EOL;
} else {
$id = (int)$sock;
$buffers[$id] .= $data;
// 簡單協定:以 \n 為訊息界限
while (($pos = strpos($buffers[$id], "\n")) !== false) {
$msg = substr($buffers[$id], 0, $pos);
$buffers[$id] = substr($buffers[$id], $pos + 1);
echo "客戶端 $id: $msg" . PHP_EOL;
// 回聲
socket_write($sock, "Echo: $msg\n");
}
}
}
}
}
HTTP/1.0 簡單實作:解析請求行、標頭、回應靜態檔案/動態內容。
<?php
// http_server.php
require 'server_multi.php'; // 繼承上述多路複用架構
// 修改資料處理部分
function handleHttpRequest(string $request, string $docRoot): string {
// 1. 解析請求行
$lines = explode("\n", $request);
$requestLine = trim($lines[0]);
if (!preg_match('/^(GET|POST|HEAD)\s+(\S+)\s+HTTP\/(\d\.\d)$/', $requestLine, $m)) {
return "HTTP/1.0 400 Bad Request\r\n\r\n";
}
[$method, $uri, $version] = [$m[1], $m[2], $m[3]];
// 2. 解析標頭 (簡易)
$headers = [];
foreach (array_slice($lines, 1) as $line) {
$line = trim($line);
if ($line === '') break; // 標頭結束
if (strpos($line, ':') !== false) {
[$k, $v] = explode(':', $line, 2);
$headers[strtolower(trim($k))] = trim($v);
}
}
// 3. 路由與安全
$path = parse_url($uri, PHP_URL_PATH);
$path = $path === '/' ? '/index.html' : $path;
$realPath = realpath($docRoot . $path);
if ($realPath === false || strpos($realPath, realpath($docRoot)) !== 0) {
return "HTTP/1.0 404 Not Found\r\n\r\n404 Not Found
";
}
// 4. 回應檔案
if (is_dir($realPath)) {
$realPath = realpath($realPath . '/index.html');
if (!$realPath) return "HTTP/1.0 403 Forbidden\r\n\r\n";
}
$mime = mime_content_type($realPath) ?: 'application/octet-stream';
$content = file_get_contents($realPath);
$len = strlen($content);
return "HTTP/1.0 200 OK\r\n" .
"Content-Type: $mime\r\n" .
"Content-Length: $len\r\n" .
"Connection: close\r\n" .
"\r\n" .
$content;
}
// 在 server_multi.php 的資料處理處呼叫:
// $response = handleHttpRequest($msg, __DIR__ . '/public');
// socket_write($sock, $response);
# Telnet 測試回聲伺服端
telnet 127.0.0.1 1337
# 瀏覽器測試 HTTP 伺服端
https://127.0.0.1:1337/
# PHP 客戶端腳本
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($socket, '127.0.0.1', 1337);
socket_write($socket, "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n");
while ($data = socket_read($socket, 1024)) echo $data;
socket_close($socket);
PHP_BINARY_READ:讀取指定長度原始資料,預設。PHP_NORMAL_READ:讀取直到 `\n` 或 `\r\n`,適合行協定。tv_sec=0, tv_usec=0:非阻塞輪詢,CPU 佔用高。tv_sec=1, tv_usec=0:平衡回應速度與 CPU。tv_sec=null:永久阻塞,直到有事件 (信號會中斷)。
// 風格 1:檢查返回值
if (($socket = socket_create(...)) === false) { /* 錯誤 */ }
// 風格 2:@ 抑制警告 + socket_last_error()
$socket = @socket_create(...);
if ($socket === false) {
$err = socket_last_error();
echo socket_strerror($err);
}
| 層級 | 代表技術 | 特點 |
|---|---|---|
| 原生 Socket | ext-sockets | 完全控制、學習用、維護成本高 |
| Stream 封裝 | stream_socket_server/client | 內建、支援 SSL/TLS、資源型別統一 |
| 事件驅動庫 | ReactPHP, Amp, libevent | 非阻塞、Promise、生態豐富 |
| 協程框架 | Swoole, OpenSwoole, RoadRunner | C 擴展、協程、極致效能、生產就緒 |
| 應用框架 | Laravel Octane, Hyperf, Symfony Mercure | 整合業務框架、開發體驗佳 |
Socket 程式設計核心在於建立通訊端點、綁定位址、監聽/連接、資料收發、優雅關閉。PHP 提供完整的 Berkeley Socket API 封裝,配合 socket_select 可實現高併發單執行緒伺服端。理解阻塞/非阻塞、讀取模式、錯誤處理、協定解析細節,是進階網路程式設計的基石。生產系統強烈建議使用 Swoole 等成熟協程框架,專注業務創新。