首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP Socket 程式設計完整指南:建立、監聽、連線、配對與非阻塞模式

     編輯:PHP 相關知識

前言:PHP Socket 擴展的功能與定位

PHP sockets 擴展提供了更底層、更接近 C 語言 BSD Socket API 的介面,支援 TCP/UDP、Unix Domain、原始 Socket 等,適合開發高效能網路服務、自定義協定伺服器/客戶端。本文系統整理 Socket 建立、綁定、監聽、接受、讀寫、關閉、非阻塞、配對等核心操作與實戰範例。

環境需求

# 安裝 sockets 擴展 (通常預設編譯包含)
php -m | grep sockets

# 或安裝
apt-get install php-sockets  # Debian/Ubuntu
yum install php-sockets      # CentOS/RHEL

核心函數速查表

函數用途關鍵參數
socket_create建立 Socketdomain, type, protocol
socket_bind綁定 IP/埠socket, address, port
socket_listen開始監聽socket, backlog
socket_accept接受連線socket
socket_read讀取資料socket, length, type
socket_write寫入資料socket, buffer, length
socket_close關閉 Socketsocket
socket_connect發起連線socket, address, port
socket_set_nonblock設定非阻塞socket
socket_set_block設定阻塞socket
socket_select多路複用read, write, except, tv_sec
socket_create_pair建立 Socket 配對domain, type, protocol, array
socket_strerror錯誤訊息errno
socket_last_error最後錯誤碼socket

範例一:基本 TCP 回聲伺服器

<?php
$ip = '127.0.0.1';
$port = 1000;

// 1. 建立 IPv4 TCP Socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (!$socket) die("建立失敗: " . socket_strerror(socket_last_error()));

// 2. 綁定位址
if (!socket_bind($socket, $ip, $port)) {
    die("綁定失敗: " . socket_strerror(socket_last_error($socket)));
}

// 3. 監聽 (backlog 5)
if (!socket_listen($socket, 5)) {
    die("監聽失敗: " . socket_strerror(socket_last_error($socket)));
}

echo "伺服器啟動於 $ip:$port\n";

// 4. 接受連線 (阻塞)
$client = socket_accept($socket);
if (!$client) die("接受失敗: " . socket_strerror(socket_last_error($socket)));

// 5. 寫入歡迎訊息
$welcome = "歡迎來到 PHP Socket 伺服器!\n";
socket_write($client, $welcome, strlen($welcome));

// 6. 讀取客戶端資料 (簡易回聲)
while (($buf = socket_read($client, 1024, PHP_NORMAL_READ)) !== false) {
    $buf = trim($buf);
    if ($buf === 'quit') break;
    socket_write($client, "Echo: $buf\n", strlen("Echo: $buf\n"));
}

// 7. 關閉
socket_close($client);
socket_close($socket);

範例二:錯誤處理與資源清理

<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (!$socket) die(socket_strerror(socket_last_error()));

// 清除錯誤狀態
socket_clear_error($socket);

// ... 正常操作 ...

// 確保關閉
socket_close($socket);

範例三:Socket Pair (進程間通訊)

<?php
$sockets = [];
// 建立一對相連的 Unix Stream Socket
if (!socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets)) {
    die("建立配對失敗: " . socket_strerror(socket_last_error()));
}

$parent = $sockets[0];
$child = $sockets[1];

// 父進程寫入
if (!socket_write($parent, "Hello Child\n", 12)) {
    die("寫入失敗: " . socket_strerror(socket_last_error($parent)));
}

// 子進程讀取
$data = socket_read($child, 1024, PHP_BINARY_READ);
if ($data === false) {
    die("讀取失敗: " . socket_strerror(socket_last_error($child)));
}

var_dump($data); // string(12) "Hello Child\n"

// 關閉
socket_close($parent);
socket_close($child);

範例四:非阻塞連線與 socket_select 多路複用

<?php
$ip = '127.0.0.1';
$port = 1000;

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (!$socket) die(socket_strerror(socket_last_error()));

// 設為非阻塞
socket_set_nonblock($socket);

// 發起連線 (立即返回,不等待)
$result = socket_connect($socket, $ip, $port);

// 連線狀態檢查
if ($result === false) {
    $err = socket_last_error($socket);
    if ($err !== EINPROGRESS && $err !== EALREADY && $err !== EWOULDBLOCK) {
        die("連線啟動失敗: " . socket_strerror($err));
    }
}

// 使用 socket_select 等待可寫 (連線完成)
$read = $write = $except = [$socket];
$tv_sec = 5;

$sel = socket_select($read, $write, $except, $tv_sec);
if ($sel === false) {
    die("select 錯誤: " . socket_strerror(socket_last_error()));
} elseif ($sel === 0) {
    die("連線逾時 (5秒)\n");
}

// 檢查連線結果
if (isset($write[0])) {
    // 連線成功,改回阻塞模式進行後續通訊
    socket_set_block($socket);
    echo "連線成功!\n";
    
    // 發送測試資料
    socket_write($socket, "TEST\n", 5);
    $resp = socket_read($socket, 1024);
    echo "回應: $resp\n";
} else {
    // 連線失敗
    $err = socket_last_error($socket);
    echo "連線失敗: " . socket_strerror($err) . "\n";
}

socket_close($socket);

範例五:SocketListener 類別封裝

<?php
class SocketListener {
    private $socket;
    private $port;
    private $clients = [];

    public function __construct(int $port = 8080) {
        $this->port = $port;
        $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if (!$this->socket) {
            throw new RuntimeException("建立 Socket 失敗: " . socket_strerror(socket_last_error()));
        }

        // 設定選項:地址重用
        socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1);

        if (!socket_bind($this->socket, '0.0.0.0', $this->port)) {
            throw new RuntimeException("綁定失敗: " . socket_strerror(socket_last_error($this->socket)));
        }
        if (!socket_listen($this->socket, SOMAXCONN)) {
            throw new RuntimeException("監聽失敗: " . socket_strerror(socket_last_error($this->socket)));
        }

        // 非阻塞模式
        socket_set_nonblock($this->socket);
        echo "SocketListener 啟動於 port {$this->port}\n";
    }

    public function run() {
        while (true) {
            $read = [$this->socket];
            $write = $except = [];

            // 加入所有客戶端到讀取集合
            foreach ($this->clients as $client) {
                $read[] = $client;
            }

            $ready = @socket_select($read, $write, $except, 1); // 1秒超時
            if ($ready === false) continue;

            // 新連線
            if (in_array($this->socket, $read)) {
                $client = socket_accept($this->socket);
                if ($client) {
                    socket_set_nonblock($client);
                    $this->clients[] = $client;
                    echo "新客戶端連線: " . count($this->clients) . "\n";
                }
            }

            // 處理客戶端資料
            foreach ($this->clients as $i => $client) {
                if (!in_array($client, $read)) continue;

                $data = @socket_read($client, 4096, PHP_NORMAL_READ);
                if ($data === false || $data === '') {
                    // 客戶端斷線
                    echo "客戶端斷線\n";
                    socket_close($client);
                    unset($this->clients[$i]);
                    continue;
                }

                $data = trim($data);
                echo "收到: $data\n";

                // 簡易回聲
                $response = "Server: $data\n";
                @socket_write($client, $response, strlen($response));

                if ($data === 'quit') {
                    socket_close($client);
                    unset($this->clients[$i]);
                }
            }
        }
    }

    public function __destruct() {
        foreach ($this->clients as $c) socket_close($c);
        if ($this->socket) socket_close($this->socket);
    }
}

// 使用
// $server = new SocketListener(9000);
// $server->run();

常見錯誤與除錯技巧

問題原因解決
Address already in use埠被佔用或 TIME_WAITSO_REUSEADDR、等待或換埠
Permission denied權限不足 (port < 1024)用 >1024 埠或 root/sudo
Connection refused伺服器未啟動/防火牆確認伺服器、防火牆規則
Resource temporarily unavailable非阻塞下無資料正常,繼續 select
Broken pipe寫入已關閉的連線檢查回傳值、捕獲 SIGPIPE

效能優化建議

  • SO_REUSEADDR:socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1)
  • 緩衝區調整:socket_set_option($sock, SOL_SOCKET, SO_RCVBUF, 65536)
  • TCP_NODELAY:socket_set_option($sock, SOL_TCP, TCP_NODELAY, 1) 關閉 Nagle 演算法
  • 事件驅動:生產環境建議用 ReactPHP、Swoole、AMPHP 等事件迴圈框架
  • 進程池:pcntl_fork 或 Swoole\Process\Pool 多核心處理

安全性檢查清單

  • 輸入長度限制,防止緩衝區溢位
  • 連線數限制,防止 DoS
  • TLS 加密 (需搭配 stream_socket 或 OpenSSL)
  • 驗證授權機制
  • 記錄稽核日誌

總結

PHP Socket 擴展提供了完整的 BSD Socket API 對應,能構建高效能網路應用。掌握 socket_create、bind、listen、accept、read/write、select、nonblock 等核心流程,並結合非阻塞 I/O 與多路複用,即可開發出支援高併發的伺服器程式。生產環境建議使用成熟的事件驅動框架 (Swoole/ReactPHP) 而非手寫迴圈。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved