凌峰创科服务平台

php socket 服务器端

我们将从最基础的 TCP Socket 服务器开始,逐步深入,并加入现代的最佳实践,如面向对象和 Swoole 这样的协程扩展。

php socket 服务器端-图1
(图片来源网络,侵删)

第一部分:基础 TCP Socket 服务器

这是最经典、最核心的 Socket 编程模型,它遵循“创建 -> 绑定 -> 监听 -> 接受 -> 通信 -> 关闭”的流程。

核心流程与函数

一个简单的 TCP 服务器生命周期如下:

  1. socket_create(): 创建一个 Socket 套接字。
  2. socket_bind(): 将 Socket 绑定到指定的 IP 地址和端口。
  3. socket_listen(): 开始监听来自客户端的连接请求。
  4. socket_accept(): 阻塞等待,接受一个客户端连接,成功后返回一个新的 Socket 资源,用于与该客户端通信。
  5. socket_read() / socket_write(): 通过 accept 返回的新 Socket 资源,读取客户端发来的数据和向客户端写入数据。
  6. socket_close(): 关闭 Socket 连接。

完整代码示例

这是一个功能完整的单线程、阻塞式 TCP 服务器,它能处理一个客户端,当客户端断开后,它会继续等待下一个客户端。

文件名:server.php

php socket 服务器端-图2
(图片来源网络,侵删)
<?php
// +----------------------------------------------------------------------
// | PHP Socket Server
// +----------------------------------------------------------------------
// 设置错误报告和显示所有错误
error_reporting(E_ALL);
ini_set('display_errors', 1);
// --- 1. 创建 Socket ---
// AF_INET: IPv4 协议
// SOCK_STREAM: TCP 类型
// SOL_TCP: TCP 协议 (等同于 getprotobyname('tcp'))
$serverSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($serverSocket === false) {
    die("socket_create() failed: " . socket_strerror(socket_last_error()) . "\n");
}
// --- 2. 绑定 Socket ---
$address = '0.0.0.0'; // 0.0.0.0 表示监听所有可用的网络接口
$port = 9999;
$result = socket_bind($serverSocket, $address, $port);
if ($result === false) {
    die("socket_bind() failed: " . socket_strerror(socket_last_error($serverSocket)) . "\n");
}
// --- 3. 监听 Socket ---
$result = socket_listen($serverSocket, 5); // 5 是 backlog,表示等待连接的最大队列长度
if ($result === false) {
    die("socket_listen() failed: " . socket_strerror(socket_last_error($serverSocket)) . "\n");
}
echo "Server is running at tcp://{$address}:{$port}\n";
echo "Waiting for a client to connect...\n";
// --- 4. 接受客户端连接 ---
// 这是一个阻塞函数,会一直等待直到有客户端连接
$clientSocket = socket_accept($serverSocket);
if ($clientSocket === false) {
    die("socket_accept() failed: " . socket_strerror(socket_last_error($serverSocket)) . "\n");
}
echo "Client connected!\n";
// --- 5. 与客户端通信 ---
while (true) {
    // 读取客户端数据
    // 1024 是读取的最大字节数
    $data = socket_read($clientSocket, 1024);
    // 如果读取失败或客户端关闭了连接
    if ($data === false) {
        echo "socket_read() failed: " . socket_strerror(socket_last_error($clientSocket)) . "\n";
        break;
    }
    // 如果读取到空字符串,表示客户端断开了连接
    if ($data === '') {
        echo "Client disconnected.\n";
        break;
    }
    // 将数据转为字符串,并去除末尾的空白字符(如 \n, \r)
    $data = trim($data);
    echo "Received from client: {$data}\n";
    // 处理数据并返回响应
    $response = "Server got your message: " . $data . "\n";
    socket_write($clientSocket, $response, strlen($response));
}
// --- 6. 关闭连接 ---
socket_close($clientSocket);
socket_close($serverSocket);
echo "Server shutdown.\n";
?>

如何运行和测试

  1. 保存代码: 将上面的代码保存为 server.php

  2. 打开终端: 进入 server.php 所在的目录。

  3. 启动服务器: 运行命令 php server.php

    $ php server.php
    Server is running at tcp://0.0.0.0:9999
    Waiting for a client to connect...

    服务器会阻塞在 socket_accept,等待客户端连接。

    php socket 服务器端-图3
    (图片来源网络,侵删)
  4. 测试客户端: 打开另一个终端窗口,你可以使用 telnetnc (netcat) 作为客户端。

    • 使用 telnet:
      $ telnet 127.0.0.1 9999
      Trying 127.0.0.1...
      Connected to localhost.
      Escape character is '^]'.
      hello server      <-- 你输入的内容
      Server got your message: hello server <-- 服务器返回的响应
      ^]                <-- 输入 Ctrl+] 进入 telnet 命令模式
      telnet> quit      <-- 输入 quit 退出
      Connection closed.
    • 使用 nc:
      $ nc 127.0.0.1 9999
      hello from nc
      Server got your message: hello from nc
  5. 观察服务器输出: 当你在客户端输入并发送消息后,服务器的终端会显示接收到的内容。


第二部分:进阶 - 面向对象与多客户端处理

上面的例子只能同时处理一个客户端,当它在与一个客户端通信时,其他客户端必须等待,为了处理多个客户端,我们需要引入多进程或多线程,在 PHP 中,最常见和稳定的方式是使用多进程。

面向对象封装

我们把服务器逻辑封装成一个类,这样更易于管理和扩展。

文件名:TcpServer.php

<?php
class TcpServer
{
    private $serverSocket;
    private $address;
    private $port;
    public function __construct(string $address = '0.0.0.0', int $port = 9999)
    {
        $this->address = $address;
        $this->port = $port;
        $this->serverSocket = null;
    }
    public function start()
    {
        $this->serverSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if (!$this->serverSocket) {
            throw new RuntimeException("socket_create failed: " . socket_strerror(socket_last_error()));
        }
        socket_set_option($this->serverSocket, SOL_SOCKET, SO_REUSEADDR, 1); // 地址复用
        if (!socket_bind($this->serverSocket, $this->address, $this->port)) {
            throw new RuntimeException("socket_bind failed: " . socket_strerror(socket_last_error($this->serverSocket)));
        }
        if (!socket_listen($this->serverSocket, 5)) {
            throw new RuntimeException("socket_listen failed: " . socket_strerror(socket_last_error($this->serverSocket)));
        }
        echo "Server started at tcp://{$this->address}:{$this->port}\n";
        while (true) {
            // 阻塞等待客户端连接
            $clientSocket = socket_accept($this->serverSocket);
            if ($clientSocket === false) {
                echo "socket_accept failed: " . socket_strerror(socket_last_error($this->serverSocket)) . "\n";
                continue;
            }
            echo "New client connected.\n";
            // 为每个客户端连接创建一个子进程来处理
            $pid = pcntl_fork();
            if ($pid == -1) {
                // 创建进程失败
                die("Could not fork process\n");
            } else if ($pid) {
                // 父进程
                // 关闭父进程中与客户端通信的 socket,只保留监听 socket
                socket_close($clientSocket);
                // 父进程继续循环,接受下一个连接
                continue;
            } else {
                // 子进程
                // 关闭子进程中用于监听的 server socket
                socket_close($this->serverSocket);
                $this->handleClient($clientSocket);
                exit(0); // 子进程处理完毕后退出
            }
        }
    }
    private function handleClient($clientSocket)
    {
        while (true) {
            $data = socket_read($clientSocket, 1024);
            if ($data === false) {
                echo "socket_read failed: " . socket_strerror(socket_last_error($clientSocket)) . "\n";
                break;
            }
            if ($data === '') {
                echo "Client disconnected.\n";
                break;
            }
            $data = trim($data);
            echo "Received from client: {$data}\n";
            $response = "Server (PID: " . getmypid() . ") got your message: " . $data . "\n";
            socket_write($clientSocket, $response, strlen($response));
        }
        socket_close($clientSocket);
    }
    public function __destruct()
    {
        if ($this->serverSocket) {
            socket_close($this->serverSocket);
        }
    }
}
// 使用示例
try {
    $server = new TcpServer('0.0.0.0', 9999);
    $server->start();
} catch (RuntimeException $e) {
    echo $e->getMessage() . "\n";
}

多进程模型说明

  • pcntl_fork(): 这个函数会创建一个子进程,它会复制父进程的整个内存空间。
  • 父进程: pcntl_fork() 返回子进程的 PID (Process ID),父进程的职责是继续循环,调用 socket_accept() 接受新的连接,然后将每个新连接交给一个子进程去处理,父进程自己不参与具体的通信。
  • 子进程: pcntl_fork() 返回 0,子进程的唯一任务就是处理分配给它的那个客户端连接,处理完成后,调用 exit(0) 终止自己,避免变成僵尸进程。
  • socket_close() 的作用: 在父进程中,关闭 $clientSocket 是非常关键的一步,因为子进程复制了父进程的文件描述符,如果不关闭,会导致文件描述符泄漏,同样,在子进程中关闭 $serverSocket 也是必要的,因为子进程不需要监听新连接。

第三部分:现代选择 - Swoole 扩展

传统的 PHP Socket 编程是阻塞式的,并发能力非常弱,要实现高并发,需要依赖多进程/多线程,这会增加编程的复杂性。

Swoole 是一个高性能的 PHP 协程框架,它提供了强大的异步网络编程能力,可以轻松实现成千上万的并发连接。

安装 Swoole

你需要安装 Swoole 扩展。

# 使用 pecl 安装 (推荐)
pecl install swoole
# 或者从源码编译安装
git clone https://github.com/swoole/swoole-src.git
cd swoole-src
phpize
./configure
make && make install
# 然后在 php.ini 中添加 extension=swoole

Swoole TCP 服务器示例

Swoole 的 API 非常简洁,并且是基于事件驱动的,性能极高。

文件名:swoole_server.php

<?php
// +----------------------------------------------------------------------
// | Swoole TCP Server
// +----------------------------------------------------------------------
use Swoole\Server;
// 创建一个 TCP 服务器对象
// SWOOLE_PROCESS 是默认模式,也是多进程模式
$server = new Server('0.0.0.0', 9999);
// 设置运行参数
$server->set([
    'worker_num' => 4, // 设置启动的 Worker 进程数,CPU 核心数的 1-4 倍
    'daemonize' => false, // 是否以守护进程方式运行
    'log_file' => '/tmp/swoole.log', // 指定日志文件
]);
// 监听连接进入事件
$server->on('Connect', function ($server, $fd) {
    echo "Client #{$fd} connected.\n";
});
// 监听数据接收事件
$server->on('Receive', function ($server, $fd, $reactorId, $data) {
    echo "Received from #{$fd}: {$data}\n";
    // 向客户端发送数据
    $server->send($fd, "Server (Swoole) got your message: {$data}\n");
});
// 监听连接关闭事件
$server->on('Close', function ($server, $fd) {
    echo "Client #{$fd} closed.\n";
});
// 启动服务器
echo "Swoole TCP Server started at tcp://0.0.0.0:9999\n";
$server->start();

Swoole 的优势

  • 高性能: 基于事件驱动和协程,单进程即可处理海量并发连接。
  • 异步非阻塞: I/O 操作(如网络请求、文件读写)不会阻塞整个进程,其他协程可以继续执行。
  • 丰富的协议支持: 内置了对 HTTP, WebSocket, HTTP2, Redis, MySQL 等协议的支持。
  • 现代化的 API: 回调函数的写法清晰明了,易于理解和管理。

总结与对比

特性 原生 PHP Socket Swoole
模型 阻塞式、多进程 异步非阻塞、事件驱动、协程
并发能力 低 (每个连接需要一个进程) 极高 (单进程可处理数万连接)
易用性 较低,需要处理进程管理、信号等 高,API 封装良好,开箱即用
适用场景 学习 Socket 原理、简单任务、无高并发需求 高性能实时应用、WebSocket 服务、RPC 服务、微服务
依赖 仅 PHP 核心功能 需要安装 Swoole 扩展

如何选择?

  • 如果你是初学者,想了解 Socket 的底层原理,从原生 PHP Socket 开始是最好的选择。
  • 如果你要构建一个生产环境、需要高并发、高性能的服务,毫无疑问应该选择 Swoole,它能让你用 PHP 写出媲美 Go 和 Node.js 的网络服务。
分享:
扫描分享到社交APP
上一篇
下一篇