Skip to content

Socket 编程

Socket(套接字)是网络通信的底层接口。PHP 提供了完整的 Socket 扩展,允许开发者直接操作 TCP/UDP 套接字,构建自定义网络协议的服务器和客户端。本节将系统讲解 PHP Socket 编程的核心概念、函数用法和实战案例。

前置知识

阅读本节前,建议先了解:PHP 基础语法cURL 详解

基础概念

什么是 Socket

Socket 是网络通信的端点,提供了进程间通过网络进行数据交换的机制。一个 Socket 由 IP 地址和端口号唯一标识。

Socket 通信模型

  • TCP(传输控制协议):面向连接、可靠传输、有序到达
  • UDP(用户数据报协议):无连接、不保证可靠、速度快
  • Unix Domain Socket:本地进程间通信(IPC),不走网络栈

PHP Socket 扩展

PHP 的 Socket 扩展基于 BSD Socket API,主要函数以 socket_* 开头。安装方式:

bash
# 编译安装
./configure --enable-sockets

# Ubuntu/Debian
sudo apt-get install php-sockets

基本语法

TCP 服务器

php
<?php
declare(strict_types=1);

/**
 * 简单 TCP 服务器
 */
class TcpServer
{
    private \Socket $socket;
    private bool $running = false;
    private array $clients = [];

    public function __construct(
        private readonly string $host = '0.0.0.0',
        private readonly int $port = 9501
    ) {}

    public function start(): void
    {
        // 创建 TCP Socket
        $this->socket = socket_create(
            AF_INET,       // IPv4
            SOCK_STREAM,   // TCP
            SOL_TCP
        );

        if ($this->socket === false) {
            throw new RuntimeException("创建 Socket 失败: " . socket_strerror(socket_last_error()));
        }

        // 设置地址复用(避免 TIME_WAIT 状态阻止重启)
        socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1);

        // 绑定地址和端口
        if (!socket_bind($this->socket, $this->host, $this->port)) {
            throw new RuntimeException("绑定失败: " . socket_strerror(socket_last_error($this->socket)));
        }

        // 开始监听(最大等待队列)
        if (!socket_listen($this->socket, 128)) {
            throw new RuntimeException("监听失败: " . socket_strerror(socket_last_error($this->socket)));
        }

        // 设置非阻塞模式
        socket_set_nonblock($this->socket);

        $this->running = true;
        echo "服务器启动: tcp://{$this->host}:{$this->port}" . PHP_EOL;

        $this->loop();
    }

    public function stop(): void
    {
        $this->running = false;
    }

    private function loop(): void
    {
        while ($this->running) {
            $read = array_merge([$this->socket], $this->clients);
            $write = [];
            $except = [];

            // select 多路复用
            if (socket_select($read, $write, $except, 0, 200000) === false) {
                break;
            }

            // 处理新连接
            if (in_array($this->socket, $read)) {
                $newClient = socket_accept($this->socket);
                if ($newClient !== false) {
                    socket_set_nonblock($newClient);
                    $this->clients[] = $newClient;
                    $peer = socket_getpeername($newClient, $addr, $port);
                    echo "新连接: {$addr}:{$port}" . PHP_EOL;
                    $this->send($newClient, "欢迎连接服务器!\n");
                }
            }

            // 处理客户端数据
            foreach ($read as $client) {
                if ($client === $this->socket) {
                    continue;
                }

                $data = socket_read($client, 1024, PHP_NORMAL_READ);
                if ($data === false || $data === '') {
                    // 连接断开
                    $this->disconnect($client);
                } else {
                    $data = trim($data);
                    echo "收到消息: {$data}" . PHP_EOL;
                    $this->broadcast($data, $client);
                }
            }
        }

        // 清理
        foreach ($this->clients as $client) {
            socket_close($client);
        }
        socket_close($this->socket);
        echo "服务器已停止" . PHP_EOL;
    }

    private function send(\Socket $client, string $message): bool
    {
        return socket_write($client, $message, strlen($message)) !== false;
    }

    private function broadcast(string $message, \Socket $exclude): void
    {
        foreach ($this->clients as $client) {
            if ($client !== $exclude) {
                $this->send($client, "广播: {$message}\n");
            }
        }
    }

    private function disconnect(\Socket $client): void
    {
        $index = array_search($client, $this->clients, true);
        if ($index !== false) {
            unset($this->clients[$index]);
            $this->clients = array_values($this->clients);
        }
        socket_close($client);
        echo "客户端断开连接" . PHP_EOL;
    }
}

// 启动服务器
// $server = new TcpServer('0.0.0.0', 9501);
// $server->start();

TCP 客户端

php
<?php
declare(strict_types=1);

/**
 * TCP 客户端
 */
class TcpClient
{
    private \Socket $socket;

    public function connect(string $host, int $port, int $timeout = 10): void
    {
        $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

        if ($this->socket === false) {
            throw new RuntimeException("创建 Socket 失败: " . socket_strerror(socket_last_error()));
        }

        // 设置发送/接收超时
        $timeoutSec = (int) ($timeout);
        $timeoutUsec = ($timeout - $timeoutSec) * 1000000;
        socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, [
            'sec' => $timeoutSec, 'usec' => $timeoutUsec,
        ]);
        socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, [
            'sec' => $timeoutSec, 'usec' => $timeoutUsec,
        ]);

        // 连接服务器
        $connected = @socket_connect($this->socket, $host, $port);

        if (!$connected) {
            $errno = socket_last_error($this->socket);
            if ($errno !== SOCKET_EINPROGRESS && $errno !== SOCKET_EWOULDBLOCK) {
                throw new RuntimeException(
                    "连接失败: " . socket_strerror($errno)
                );
            }
        }

        echo "已连接到 {$host}:{$port}" . PHP_EOL;
    }

    public function send(string $message): int|false
    {
        return socket_write($this->socket, $message, strlen($message));
    }

    public function receive(int $length = 4096): string|false
    {
        return socket_read($this->socket, $length, PHP_BINARY_READ);
    }

    public function receiveLine(): string|false
    {
        return socket_read($this->socket, 1024, PHP_NORMAL_READ);
    }

    public function close(): void
    {
        if (isset($this->socket)) {
            socket_close($this->socket);
        }
    }

    public function __destruct()
    {
        $this->close();
    }
}

// 使用示例
// $client = new TcpClient();
// $client->connect('127.0.0.1', 9501);
// $client->send("Hello, Server!\n");
// echo $client->receiveLine();
// $client->close();

UDP 通信

php
<?php
declare(strict_types=1);

/**
 * UDP 服务器
 */
class UdpServer
{
    private \Socket $socket;

    public function __construct(private readonly string $host = '0.0.0.0', private readonly int $port = 9502)
    {
    }

    public function start(): void
    {
        $this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
        if ($this->socket === false) {
            throw new RuntimeException("创建 Socket 失败");
        }

        socket_bind($this->socket, $this->host, $this->port);
        echo "UDP 服务器启动: udp://{$this->host}:{$this->port}" . PHP_EOL;

        while (true) {
            $buf = '';
            $from = '';
            $port = 0;

            // 接收数据报
            $bytes = socket_recvfrom($this->socket, $buf, 65535, 0, $from, $port);

            if ($bytes === false) {
                echo "接收错误: " . socket_strerror(socket_last_error()) . PHP_EOL;
                continue;
            }

            echo "来自 {$from}:{$port} ({$bytes} 字节): {$buf}" . PHP_EOL;

            // 回复
            $reply = "已收到: {$buf}";
            socket_sendto($this->socket, $reply, strlen($reply), 0, $from, $port);
        }
    }
}

/**
 * UDP 客户端
 */
function udpSend(string $host, int $port, string $message): string|false
{
    $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
    if ($socket === false) {
        return false;
    }

    socket_sendto($socket, $message, strlen($message), 0, $host, $port);

    $buf = '';
    $from = '';
    $rport = 0;
    socket_recvfrom($socket, $buf, 65535, 0, $from, $rport);

    socket_close($socket);
    return $buf;
}

详细说明

Socket 创建选项

php
<?php
declare(strict_types=1);

// 地址域
$domain = AF_INET;       // IPv4
// $domain = AF_INET6;   // IPv6
// $domain = AF_UNIX;    // Unix Domain Socket

// Socket 类型
$type = SOCK_STREAM;     // TCP(面向连接)
// $type = SOCK_DGRAM;   // UDP(无连接)
// $type = SOCK_RAW;     // 原始 Socket
// $type = SOCK_SEQPACKET; // 有序数据包

// 协议
$protocol = SOL_TCP;     // TCP
// $protocol = SOL_UDP;  // UDP
// $protocol = 0;        // 自动选择

$socket = socket_create($domain, $type, $protocol);

Socket 选项设置

php
<?php
declare(strict_types=1);

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

// SO_REUSEADDR - 允许重用地址(避免 Address already in use)
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);

// SO_REUSEPORT - 允许多个 Socket 绑定同一端口(PHP 8.2+ 部分系统支持)
socket_set_option($socket, SOL_SOCKET, SO_REUSEPORT, 1);

// SO_RCVBUF - 接收缓冲区大小(字节)
socket_set_option($socket, SOL_SOCKET, SO_RCVBUF, 65536);

// SO_SNDBUF - 发送缓冲区大小(字节)
socket_set_option($socket, SOL_SOCKET, SO_SNDBUF, 65536);

// SO_RCVTIMEO - 接收超时(数组格式:sec + usec)
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, [
    'sec'  => 5,
    'usec' => 0,
]);

// SO_SNDTIMEO - 发送超时
socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, [
    'sec'  => 5,
    'usec' => 0,
]);

// TCP_NODELAY - 禁用 Nagle 算法(减少延迟)
socket_set_option($socket, IPPROTO_TCP, TCP_NODELAY, 1);

// SO_KEEPALIVE - 启用心跳检测
socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);

// SO_BROADCAST - 允许广播
socket_set_option($socket, SOL_SOCKET, SO_BROADCAST, 1);

socket_select 多路复用

php
<?php
declare(strict_types=1);

/**
 * 使用 socket_select 实现多路复用 I/O
 *
 * socket_select 是 PHP Socket 编程的核心函数,
 * 允许同时监控多个 Socket 的可读/可写/异常状态。
 */
function selectExample(): void
{
    $sockets = [];
    for ($i = 0; $i < 5; $i++) {
        $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        socket_connect($sock, 'example.com', 80);
        socket_set_nonblock($sock);
        $sockets[] = $sock;
    }

    while (!empty($sockets)) {
        $read = $sockets;
        $write = null;
        $except = null;

        // 参数说明:
        // $read   - 监控可读的 Socket
        // $write  - 监控可写的 Socket
        // $except - 监控异常的 Socket
        // null    - 不设置超时(阻塞)
        // 0, 200000 - 超时 200ms(非阻塞轮询)
        $changed = socket_select($read, $write, $except, 0, 200000);

        if ($changed === false) {
            echo "select 错误" . PHP_EOL;
            break;
        }

        if ($changed === 0) {
            continue; // 超时,继续循环
        }

        foreach ($read as $sock) {
            $data = socket_read($sock, 1024);
            if ($data === false || $data === '') {
                $index = array_search($sock, $sockets, true);
                if ($index !== false) {
                    unset($sockets[$index]);
                    $sockets = array_values($sockets);
                }
                socket_close($sock);
                echo "连接关闭" . PHP_EOL;
            } else {
                echo "收到数据: " . substr($data, 0, 100) . PHP_EOL;
            }
        }
    }
}

阻塞与非阻塞模式

php
<?php
declare(strict_types=1);

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

// 阻塞模式(默认)
// socket_set_block($socket);

// 非阻塞模式
socket_set_nonblock($socket);

// 非阻塞连接需要处理 EINPROGRESS
$connected = @socket_connect($socket, 'example.com', 80);
if (!$connected) {
    $errno = socket_last_error($socket);
    if ($errno === SOCKET_EINPROGRESS || $errno === SOCKET_EWOULDBLOCK) {
        // 使用 select 等待连接完成
        $write = [$socket];
        $read = $except = [];
        $ready = socket_select($read, $write, $except, 10);

        if ($ready > 0 && in_array($socket, $write)) {
            $errorCode = socket_get_option($socket, SOL_SOCKET, SO_ERROR);
            if ($errorCode === 0) {
                echo "非阻塞连接成功" . PHP_EOL;
            }
        }
    }
}

实战示例

HTTP 服务器(简易版)

php
<?php
declare(strict_types=1);

/**
 * 基于 Socket 的简易 HTTP 服务器
 */
class SimpleHttpServer
{
    private \Socket $socket;
    private bool $running = false;
    private string $documentRoot;

    public function __construct(string $documentRoot = __DIR__ . '/public')
    {
        $this->documentRoot = $documentRoot;
    }

    public function start(string $host = '0.0.0.0', int $port = 8080): void
    {
        $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1);
        socket_bind($this->socket, $host, $port);
        socket_listen($this->socket, 128);

        $this->running = true;
        echo "HTTP 服务器运行于 http://{$host}:{$port}" . PHP_EOL;

        while ($this->running) {
            $client = @socket_accept($this->socket);
            if ($client === false) {
                continue;
            }

            // 设置读取超时
            socket_set_option($client, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 5, 'usec' => 0]);

            $this->handleRequest($client);
            socket_close($client);
        }

        socket_close($this->socket);
    }

    private function handleRequest(\Socket $client): void
    {
        // 读取请求头
        $request = '';
        while (true) {
            $chunk = @socket_read($client, 1024, PHP_NORMAL_READ);
            if ($chunk === false || $chunk === '') {
                break;
            }
            $request .= $chunk;
            if (str_contains($request, "\r\n\r\n")) {
                break;
            }
        }

        if (empty($request)) {
            return;
        }

        // 解析请求
        $lines = explode("\r\n", trim($request));
        $requestLine = $lines[0];
        preg_match('/^(\w+)\s+(\S+)\s+HTTP\/\d+\.\d+/', $requestLine, $matches);

        if (count($matches) < 3) {
            $this->sendResponse($client, 400, 'Bad Request');
            return;
        }

        $method = $matches[1];
        $path = $matches[2];

        echo "{$method} {$path}" . PHP_EOL;

        // 路由处理
        if ($path === '/') {
            $path = '/index.html';
        }

        $filePath = $this->documentRoot . $path;

        if (!file_exists($filePath) || !is_file($filePath)) {
            $this->sendResponse($client, 404, 'Not Found', 'Page not found');
            return;
        }

        $content = file_get_contents($filePath);
        $mimeType = $this->getMimeType($filePath);
        $this->sendResponse($client, 200, 'OK', $content, $mimeType);
    }

    private function sendResponse(
        \Socket $client,
        int $statusCode,
        string $statusText,
        string $body = '',
        string $contentType = 'text/html; charset=utf-8'
    ): void {
        $statusLine = "HTTP/1.1 {$statusCode} {$statusText}\r\n";
        $headers = [
            "Content-Type: {$contentType}",
            "Content-Length: " . strlen($body),
            "Connection: close",
            "Server: SimpleHttpServer/1.0",
        ];

        $response = $statusLine;
        foreach ($headers as $header) {
            $response .= "{$header}\r\n";
        }
        $response .= "\r\n";
        $response .= $body;

        socket_write($client, $response, strlen($response));
    }

    private function getMimeType(string $filePath): string
    {
        $extension = pathinfo($filePath, PATHINFO_EXTENSION);
        $mimeTypes = [
            'html' => 'text/html; charset=utf-8',
            'css'  => 'text/css',
            'js'   => 'application/javascript',
            'json' => 'application/json',
            'png'  => 'image/png',
            'jpg'  => 'image/jpeg',
            'gif'  => 'image/gif',
            'svg'  => 'image/svg+xml',
            'ico'  => 'image/x-icon',
        ];

        return $mimeTypes[$extension] ?? 'application/octet-stream';
    }
}

Unix Domain Socket 示例

php
<?php
declare(strict_types=1);

/**
 * Unix Domain Socket 服务端
 */
class UnixSocketServer
{
    private \Socket $socket;
    private string $socketPath;

    public function __construct(string $socketPath = '/tmp/php_unix.sock')
    {
        $this->socketPath = $socketPath;
    }

    public function start(): void
    {
        // 清理旧的 socket 文件
        if (file_exists($this->socketPath)) {
            unlink($this->socketPath);
        }

        $this->socket = socket_create(AF_UNIX, SOCK_STREAM, 0);
        socket_bind($this->socket, $this->socketPath);
        socket_listen($this->socket, 128);

        echo "Unix Socket 服务器启动: {$this->socketPath}" . PHP_EOL;

        while (true) {
            $client = socket_accept($this->socket);
            if ($client === false) {
                continue;
            }

            $data = socket_read($client, 1024);
            if ($data !== false) {
                echo "收到: {$data}" . PHP_EOL;
                socket_write($client, "ECHO: {$data}");
            }

            socket_close($client);
        }
    }
}

/**
 * Unix Domain Socket 客户端
 */
function unixClientSend(string $socketPath, string $message): string|false
{
    $socket = socket_create(AF_UNIX, SOCK_STREAM, 0);

    if (!socket_connect($socket, $socketPath)) {
        return false;
    }

    socket_write($socket, $message, strlen($message));
    $response = socket_read($socket, 1024);

    socket_close($socket);
    return $response;
}

超时连接检测

php
<?php
declare(strict_types=1);

/**
 * 带超时的 TCP 连接
 */
function connectWithTimeout(
    string $host,
    int $port,
    int $timeoutSeconds = 5
): ?\Socket {
    $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    socket_set_nonblock($socket);

    $connected = @socket_connect($socket, $host, $port);

    if ($connected) {
        socket_set_block($socket);
        return $socket;
    }

    $errno = socket_last_error($socket);
    if ($errno !== SOCKET_EINPROGRESS && $errno !== SOCKET_EWOULDBLOCK) {
        socket_close($socket);
        return null;
    }

    // 使用 select 等待连接建立
    $write = [$socket];
    $read = [];
    $except = [$socket];

    $start = time();
    while (true) {
        $elapsed = time() - $start;
        $remaining = $timeoutSeconds - $elapsed;
        if ($remaining <= 0) {
            socket_close($socket);
            echo "连接超时" . PHP_EOL;
            return null;
        }

        $ready = socket_select($read, $write, $except, $remaining);

        if ($ready === false) {
            socket_close($socket);
            return null;
        }

        if (in_array($socket, $except)) {
            socket_close($socket);
            echo "连接异常" . PHP_EOL;
            return null;
        }

        if (!empty($write)) {
            // 检查连接是否成功
            $soError = socket_get_option($socket, SOL_SOCKET, SO_ERROR);
            if ($soError === 0) {
                socket_set_block($socket);
                return $socket;
            }
            socket_close($socket);
            return null;
        }
    }
}

// 使用示例
$sock = connectWithTimeout('example.com', 80, 3);
if ($sock !== null) {
    echo "连接成功!" . PHP_EOL;
    socket_close($sock);
}

注意事项

常见 Socket 错误

错误常量说明
SOCKET_EACCES13权限不足
SOCKET_EADDRINUSE98地址已使用
SOCKET_ECONNREFUSED111连接被拒绝
SOCKET_EINPROGRESS115操作正在进行
SOCKET_EAGAIN11资源暂时不可用

资源清理

php
<?php
declare(strict_types=1);

// 使用 try/finally 确保资源释放
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
try {
    socket_bind($socket, '0.0.0.0', 9501);
    socket_listen($socket);
    // ... 业务逻辑 ...
} finally {
    socket_close($socket);
}

最佳实践

  1. 生产环境推荐使用 Swoole:PHP 的原生 Socket 扩展适合简单场景,高性能场景推荐使用 Swoole 扩展
  2. 使用 socket_select 实现并发:避免多进程/多线程的复杂度
  3. 设置合理的超时:防止死连接占用资源
  4. 设置 SO_REUSEADDR:避免 TIME_WAIT 状态导致重启失败
  5. 异常处理:所有 socket_* 操作都应检查返回值
  6. 使用非阻塞模式:提升服务器并发处理能力

下一节

继续学习:FTP / SSH2 操作

参考链接