PHP Socket 編程是實現**服務間通訊、即時推送、物聯網設備對接、自定義協議網關**的核心技能。雖然 PHP 定位為 Web 腳本語言,但其 ext-sockets 擴展提供了完整的 Berkeley Socket API 封裝,支援 TCP/UDP、阻塞/非阻塞、SSL/TLS 加密、多路復用。本文將從基礎 API、同步客戶端封裝、異步非阻塞模型、SSL 安全通訊、應用層協議解析、生產級錯誤處理六大維度,構建完整的 PHP Socket 客戶端開發知識體系。
| 分類 | 函數 | 用途 |
|---|---|---|
| 創建/連接 | socket_create($domain, $type, $protocol) | 創建 Socket 資源 (AF_INET/AF_INET6, SOCK_STREAM/SOCK_DGRAM, SOL_TCP/SOL_UDP) |
socket_connect($socket, $address, $port) | 發起連接 (阻塞直到成功/失敗/超時) | |
socket_bind/listen/accept | 服務端綁定/監聽/接受 (本文專注客戶端) | |
| 讀寫 | socket_write($socket, $buffer, $length) | 發送數據 (可能部分發送,需循環) |
socket_read($socket, $length, $type) | 讀取數據 (PHP_BINARY_READ/PHP_NORMAL_READ) | |
socket_recv/send/recvfrom/sendto | 進階收發 (旗標 MSG_DONTWAIT, MSG_WAITALL) | |
| 控制/狀態 | socket_set_option($socket, $level, $name, $value) | 設置選項 (SO_RCVTIMEO, SO_SNDTIMEO, SO_REUSEADDR, TCP_NODELAY) |
socket_get_option | 獲取選項 | |
socket_last_error/strerror | 錯誤碼/描述 | |
| 非阻塞/多路復用 | socket_set_nonblock($socket) | 設置非阻塞模式 |
socket_select($read, $write, $except, $tv_sec, $tv_usec) | I/O 多路復用 (select 系統調用) | |
| SSL/TLS | stream_socket_client + stream_socket_enable_crypto | **推薦用 Stream API 實現 SSL** (sockets 擴展 SSL 支援有限) |
| 關閉 | socket_close($socket) / socket_shutdown($socket, $how) | 關閉/半關閉 (SHUT_RD/SHUT_WR/SHUT_RDWR) |
<?php
class TcpClient {
private $host;
private $port;
private $timeout; // 連接/讀寫超時 (秒,浮點支援毫秒)
private $socket; // Socket 資源
private $connected = false;
private $persistent = false; // 長連接模式
public function __construct(string $host, int $port, float $timeout = 5.0) {
$this->host = $host;
$this->port = $port;
$this->timeout = $timeout;
}
public function connect(bool $persistent = false): bool {
if ($this->connected && $this->persistent) return true;
$this->close();
$this->persistent = $persistent;
$flags = $persistent ? STREAM_CLIENT_PERSISTENT : 0;
// 使用 Stream API (更現代、支援 SSL、超時語義清晰)
$this->socket = @stream_socket_client(
"tcp://{$this->host}:{$this->port}",
$errno, $errstr,
$this->timeout,
$flags | STREAM_CLIENT_CONNECT,
stream_context_create(['socket' => ['so_keepalive' => true]])
);
if (!$this->socket) {
$this->lastError = "連接失敗 [$errno]: $errstr";
return false;
}
// 設置讀寫超時 (流級別,影響 fread/fwrite)
stream_set_timeout($this->socket, (int)$this->timeout, ($this->timeout - (int)$this->timeout) * 1e6);
// 禁用 Nagle 算法 (減少小包延遲,適合即時通訊)
stream_set_option($this->socket, STREAM_OPTION_WRITE_BUFFER, STREAM_BUFFER_NONE, 0);
$this->connected = true;
return true;
}
// 發送數據 (確保全部發送)
public function write(string $data): bool {
if (!$this->connected) return false;
$written = 0;
$len = strlen($data);
while ($written < $len) {
$n = @fwrite($this->socket, substr($data, $written), $len - $written);
if ($n === false || $n === 0) {
$this->connected = false;
return false;
}
$written += $n;
}
return true;
}
// 讀取固定長度
public function readExact(int $length): ?string {
$buf = '';
while (strlen($buf) < $length) {
$chunk = @fread($this->socket, $length - strlen($buf));
if ($chunk === '' || $chunk === false) {
$this->connected = false;
return null; // 連接斷開或錯誤
}
$buf .= $chunk;
}
return $buf;
}
// 讀取直到分隔符 (換行、NULL、自定義)
public function readUntil(string $delimiter, int $maxLength = 65536): ?string {
$buf = '';
$delimLen = strlen($delimiter);
while (strlen($buf) < $maxLength) {
$chunk = @fread($this->socket, 4096);
if ($chunk === '' || $chunk === false) {
$this->connected = false;
return null;
}
$buf .= $chunk;
$pos = strpos($buf, $delimiter);
if ($pos !== false) {
return substr($buf, 0, $pos + $delimLen); // 含分隔符
}
}
return null; // 超過最大長度未找到分隔符
}
// 通用請求-響應 (發送後讀取響應)
public function request(string $request, string $responseDelimiter = "\n", float $readTimeout = null): ?string {
if (!$this->write($request)) return null;
if ($readTimeout) stream_set_timeout($this->socket, (int)$readTimeout, ($readTimeout - (int)$readTimeout) * 1e6);
return $this->readUntil($responseDelimiter);
}
public function close(): void {
if ($this->socket) {
@fclose($this->socket);
$this->socket = null;
$this->connected = false;
}
}
public function isConnected(): bool { return $this->connected; }
public function getLastError(): ?string { return $this->lastError ?? null; }
public function __destruct() { $this->close(); }
}
// 使用範例:簡單文本協議 (換行符結尾)
$client = new TcpClient('127.0.0.1', 9501, 3.0);
if ($client->connect()) {
$resp = $client->request("GET_TIME\n");
echo "服務器時間: $resp";
$client->close();
} else {
echo "連接失敗: " . $client->getLastError();
}
?>
ext-sockets 原生 SSL 支援極弱且棄用**,**必須使用 stream_socket_client + stream_socket_enable_crypto** 或直接 ssl:///tls:// 協議。
<?php
class SecureTcpClient extends TcpClient {
private $sslOptions = [];
public function __construct(string $host, int $port, array $sslOptions = [], float $timeout = 5.0) {
parent::__construct($host, $port, $timeout);
$this->sslOptions = array_merge([
'verify_peer' => true, // 生產環境必須 true
'verify_peer_name' => true, // 驗證 CN/SAN
'allow_self_signed' => false, // 生產禁用自簽名
'cafile' => null, // CA 證書路徑 (驗證私有 CA)
'local_cert' => null, // 客戶端證書 (雙向認證)
'local_pk' => null, // 客戶端私鑰
'ciphers' => 'HIGH:!aNULL:!MD5:!3DES', // 加密套件
'disable_compression' => true, // 防 CRIME 攻擊
], $sslOptions);
}
public function connect(bool $persistent = false): bool {
$this->close();
$flags = $persistent ? STREAM_CLIENT_PERSISTENT : 0;
// 先建立 TCP 連接
$this->socket = @stream_socket_client(
"tcp://{$this->host}:{$this->port}",
$errno, $errstr,
$this->timeout,
$flags | STREAM_CLIENT_CONNECT
);
if (!$this->socket) { /* 錯誤處理 */ return false; }
// 設置超時
stream_set_timeout($this->socket, (int)$this->timeout, ($this->timeout - (int)$this->timeout) * 1e6);
// 啟用 SSL/TLS 加密 (非阻塞握手需配合 select,此處阻塞模式)
$cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT; // 或 TLSv1_2_CLIENT 等
$success = @stream_socket_enable_crypto($this->socket, true, $cryptoMethod, $this->sslOptions);
if (!$success) {
$err = error_get_last();
$this->lastError = "SSL 握手失敗: " . ($err['message'] ?? 'Unknown');
$this->close();
return false;
}
$this->connected = true;
return true;
}
}
// 使用:連接 HTTPS API (驗證系統 CA)
$client = new SecureTcpClient('api.example.com', 443, [
'cafile' => '/etc/ssl/certs/ca-certificates.crt', // Linux 系統 CA 捆綁包
]);
if ($client->connect()) {
$client->write("GET /v1/data HTTP/1.1\r\nHost: api.example.com\r\nConnection: close\r\n\r\n");
$response = '';
while (!feof($client->socket)) $response .= fread($client->socket, 8192);
echo $response;
}
?>
<?php
class NonBlockingClient {
private $sockets = []; // fd => ['socket'=>, 'host'=>, 'port'=>, 'buffer'=>'', 'callbacks'=>[]]
public function addConnection(string $host, int $port, callable $onConnect, callable $onData, callable $onClose, float $timeout = 5.0): int {
$socket = stream_socket_client(
"tcp://$host:$port",
$errno, $errstr,
$timeout,
STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT // 關鍵:非阻塞連接
);
if (!$socket) throw new RuntimeException("非阻塞連接啟動失敗: $errstr");
stream_set_blocking($socket, false); // 確保非阻塞
$fd = (int)$socket;
$this->sockets[$fd] = [
'socket' => $socket,
'host' => $host,
'port' => $port,
'state' => 'connecting',
'write_buffer' => '',
'read_buffer' => '',
'on_connect' => $onConnect,
'on_data' => $onData,
'on_close' => $onClose,
'timeout' => $timeout,
'last_activity' => microtime(true),
];
return $fd;
}
public function send(int $fd, string $data): bool {
if (!isset($this->sockets[$fd])) return false;
$this->sockets[$fd]['write_buffer'] .= $data;
return true;
}
public function runOnce(float $selectTimeout = 0.1): void {
$read = $write = $except = [];
$now = microtime(true);
foreach ($this->sockets as $fd => &$conn) {
$sock = $conn['socket'];
// 連接中/有數據待發送 -> 關注可寫
if ($conn['state'] === 'connecting' || $conn['write_buffer'] !== '') {
$write[] = $sock;
}
// 已連接 -> 關注可讀
if ($conn['state'] === 'connected') {
$read[] = $sock;
}
$except[] = $sock; // 關注異常
// 空閒超時檢查
if ($now - $conn['last_activity'] > $conn['timeout']) {
$this->closeConnection($fd, 'timeout');
continue;
}
}
if (empty($read) && empty($write) && empty($except)) return;
// select 系統調用 (微秒級超時)
$tv_sec = (int)$selectTimeout;
$tv_usec = ($selectTimeout - $tv_sec) * 1e6;
$num = @stream_select($read, $write, $except, $tv_sec, $tv_usec);
if ($num === false) return; // 信號中斷等
// 處理可寫 (連接建立/發送緩衝)
foreach ($write as $sock) {
$fd = (int)$sock;
if (!isset($this->sockets[$fd])) continue;
$conn = &$this->sockets[$fd];
if ($conn['state'] === 'connecting') {
// 檢查連接是否完成 (getsockopt SO_ERROR)
$err = socket_get_option($sock, SOL_SOCKET, SO_ERROR); // 需 ext-sockets 或 stream_socket_get_name 技巧
// Stream API 簡易判斷:嘗試寫入空串或獲取 peer name
if (@stream_socket_get_name($sock, true) !== false) {
$conn['state'] = 'connected';
$conn['last_activity'] = $now;
($conn['on_connect'])($fd, true);
} else {
$this->closeConnection($fd, 'connect_failed');
}
} elseif ($conn['write_buffer'] !== '') {
$written = @fwrite($sock, $conn['write_buffer']);
if ($written > 0) {
$conn['write_buffer'] = substr($conn['write_buffer'], $written);
$conn['last_activity'] = $now;
} elseif ($written === false) {
$this->closeConnection($fd, 'write_error');
}
}
}
// 處理可讀
foreach ($read as $sock) {
$fd = (int)$sock;
if (!isset($this->sockets[$fd])) continue;
$conn = &$this->sockets[$fd];
$data = @fread($sock, 8192);
if ($data === '' || $data === false) { // EOF 或錯誤
$this->closeConnection($fd, $data === false ? 'read_error' : 'eof');
} else {
$conn['read_buffer'] .= $data;
$conn['last_activity'] = $now;
// 簡易:直接回調原始數據,實際應按協議幀解析
($conn['on_data'])($fd, $data);
}
}
// 處理異常
foreach ($except as $sock) {
$this->closeConnection((int)$sock, 'exception');
}
}
private function closeConnection(int $fd, string $reason): void {
if (isset($this->sockets[$fd])) {
$conn = $this->sockets[$fd];
@fclose($conn['socket']);
($conn['on_close'])($fd, $reason);
unset($this->sockets[$fd]);
}
}
}
// 使用:同時連接 100 個服務
$nb = new NonBlockingClient();
for ($i = 1; $i <= 100; $i++) {
$nb->addConnection("10.0.0.$i", 9501,
fn($fd, $ok) => $ok ? $nb->send($fd, "PING\n") : null,
fn($fd, $data) => echo "FD$fd: $data",
fn($fd, $reason) => echo "FD$fd closed: $reason"
);
}
// 事件循環
while (!empty($nb->sockets)) { $nb->runOnce(0.5); }
?>
TCP 是流式協議,**無消息邊界**。必須在應用層定義幀格式:
\n, \r\n, \0, | 等。[Len:4bytes][Body:Len bytes] (最通用、高效)。<?php
class ProtocolParser {
// 解析「長度前綴(4字節大端)+JSON體」協議
public static function parseLengthPrefixedJson(string &$buffer): ?array {
if (strlen($buffer) < 4) return null; // Header 不全
$len = unpack('N', substr($buffer, 0, 4))[1]; // 大端無符號 32位
if ($len > 10 * 1024 * 1024) throw new RuntimeException("封包過大: $len");
if (strlen($buffer) < 4 + $len) return null; // Body 不全
$json = substr($buffer, 4, $len);
$buffer = substr($buffer, 4 + $len); // 消費掉
$data = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) throw new RuntimeException("JSON 解析失敗");
return $data;
}
// 編碼
public static function encodeLengthPrefixedJson(array $data): string {
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$len = strlen($json);
return pack('N', $len) . $json; // 大端長度 + JSON
}
}
// 在 NonBlockingClient 中集成:
$conn['parser'] = function(string &$buf) {
while (($msg = ProtocolParser::parseLengthPrefixedJson($buf)) !== null) {
($conn['on_message'])($fd, $msg); // 回調完整消息對象
}
};
// on_data 回調改為:
'on_data' => function($fd, $data) use ($nb) {
$nb->sockets[$fd]['read_buffer'] .= $data;
$nb->sockets[$fd]['parser']($nb->sockets[$fd]['read_buffer']);
}
| 場景 | 錯誤碼/現象 | 處理策略 |
|---|---|---|
| 連接超時 | errno=110 (ETIMEDOUT) | 重試 (指數退避)、熔斷、報警 |
| 連接拒絕 | errno=111 (ECONNREFUSED) | 服務未啟動/端口錯誤、檢查防火牆、服務發現 |
| 連接重置 | errno=104 (ECONNRESET) / 讀寫返回 false | 服務端崩潰/主動關閉/超時踢出,**必須重連** |
| 斷管 | errno=32 (EPIPE) / SIGPIPE 信號 | 寫入已關閉連接,**忽略 SIGPIPE** (pcntl_signal(SIGPIPE, SIG_IGN)) |
| SSL 握手失敗 | 證書過期/域名不匹配/CA 不信任/協議版本 | 嚴格驗證、記錄詳情、降級告警、不降級到不安全 |
| 發送緩衝區滿 | fwrite 返回 0/false / stream_select 可寫 | 背壓控制:暫停上層業務、丟棄非關鍵數據、斷開慢客戶端 |
| 接收緩衝區滿 | 內核接收緩衝區滿導致丟包/窗口為 0 | 應用層及時讀取、增大 SO_RCVBUF、流控 |
| 空閒超時 | 長連接無數據交換被防火牆/負載均衡器切斷 | 心跳機制 (Ping/Pong),間隔 < 空閒超時 (通常 30-60s) |
PHP Socket 客戶端開發的核心層次:
stream_socket_client),原生支援超時、SSL、非阻塞連接、上下文選項,語義清晰、跨平台一致。掌握上述體系,即可用 PHP 構建高性能、安全、可維護的網絡客戶端,對接微服務、物聯網、即時通訊、金融交易等多元場景。