PHP Socket 通信教程

Socket(套接字)是网络通信的基石,它允许程序在不同的计算机之间进行数据交换,PHP 提供了功能强大的 Socket 扩展,让我们可以轻松地创建客户端和服务器端应用。

php socket通信教程
(图片来源网络,侵删)

本教程将分为以下几个部分:

  1. 基础概念:了解 Socket、TCP 和 UDP。
  2. 环境准备:确保你的 PHP 环境支持 Socket。
  3. TCP 通信详解:创建一个可靠的 TCP 服务器和客户端。
  4. UDP 通信详解:创建一个轻量级的 UDP 服务器和客户端。
  5. 进阶注意事项:处理并发、连接、错误等。

基础概念

什么是 Socket?

Socket 可以看作是两个程序之间进行通信的“端点”,一个 Socket 由一个 IP 地址和一个端口号唯一标识,当你发送数据时,数据被打包(封装)并通过 Socket 发送到目标 IP 和端口,接收方的程序则从对应的 Socket 读取数据。

TCP vs. UDP

特性 TCP (传输控制协议) UDP (用户数据报协议)
连接性 面向连接 无连接
可靠性 可靠,通过确认、重传、排序等机制确保数据无差错、不丢失、不重复。 不可靠,不保证数据包的顺序或是否到达。
速度 较慢,因为需要建立连接和维护连接状态。 较快,没有连接开销,直接发送。
数据量 无大小限制。 有大小限制(通常受限于网络 MTU,约 1500 字节)。
应用场景 要求高可靠性的场景,如网页浏览、文件传输、邮件。 对速度要求高、能容忍少量丢包的场景,如视频会议、在线游戏、DNS查询。

环境准备

  1. 启用 Socket 扩展:确保你的 PHP 安装中包含了 sockets 扩展。

    • 在 Linux/macOS 上,检查 php.ini 文件,确保没有注释掉 extension=sockets
    • 在 Windows 上,确保 php_sockets.dll 文件在 ext 目录下,且 php.ini 中也启用了它。
    • 你可以通过在命令行运行 php -m | grep sockets 来检查。
  2. 使用命令行:Socket 编程通常在命令行环境下进行,而不是通过 Web 服务器(如 Apache 或 Nginx),因为 Web 服务器本身已经处理了网络连接,我们将使用 php 命令来运行我们的脚本。

    php socket通信教程
    (图片来源网络,侵删)

TCP 通信详解

TCP 是最常用的协议,我们将分步创建一个简单的“回显”(Echo)服务器和一个客户端,服务器接收客户端的消息,并将其原样返回。

TCP 服务器端

服务器端的流程通常是:

  1. 创建一个 Socket。
  2. 绑定 IP 地址和端口号。
  3. 开始监听连接。
  4. 接受客户端连接。
  5. 与客户端进行数据收发。
  6. 关闭连接和 Socket。

代码 (tcp_server.php):

<?php
// 设置错误报告,方便调试
error_reporting(E_ALL);
// 设置脚本在后台运行,不超时
set_time_limit(0);
// 1. 创建一个 Socket
// AF_INET: 使用 IPv4 地址
// SOCK_STREAM: 使用 TCP 协议
// SOL_TCP: 指定 TCP 协议 (与 SOCK_STREAM 配合使用)
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 检查 Socket 是否创建成功
if ($socket === false) {
    echo "socket_create() failed: reason: " . socket_strerror(socket_last_error()) . "\n";
    exit();
}
// 2. 绑定 IP 地址和端口号
$address = '127.0.0.1'; // 监听本地回环地址,即本机
$port = 9999;           // 监听 9999 端口
// 设置 SO_REUSEADDR 选项,允许地址重用,避免 "Address already in use" 错误
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
$result = socket_bind($socket, $address, $port);
if ($result === false) {
    echo "socket_bind() failed: reason: " . socket_strerror(socket_last_error($socket)) . "\n";
    exit();
}
// 3. 开始监听连接
// SOMAXCONN: 系统允许的最大连接数
$result = socket_listen($socket, SOMAXCONN);
if ($result === false) {
    echo "socket_listen() failed: reason: " . socket_strerror(socket_last_error($socket)) . "\n";
    exit();
}
echo "TCP Server is running at $address:$port\n";
// 4. 接受客户端连接 (这是一个阻塞函数,会一直等待直到有客户端连接)
$client = socket_accept($socket);
if ($client === false) {
    echo "socket_accept() failed: reason: " . socket_strerror(socket_last_error($socket)) . "\n";
} else {
    echo "Client connected!\n";
    // 5. 与客户端进行数据收发
    // socket_read() 也是一个阻塞函数,会等待客户端发送数据
    $input = socket_read($client, 1024); // 读取最多 1024 字节
    $input = trim($input); // 去除末尾的空白字符
    echo "Received from client: $input\n";
    // 将收到的消息原样返回给客户端
    $output = "Echo: $input";
    socket_write($client, $output, strlen($output));
    echo "Sent back to client: $output\n";
    // 6. 关闭客户端连接
    socket_close($client);
}
// 关闭服务器 Socket
socket_close($socket);
?>

TCP 客户端

客户端的流程通常是:

  1. 创建一个 Socket。
  2. 连接到服务器。
  3. 发送数据。
  4. 接收数据。
  5. 关闭连接和 Socket。

代码 (tcp_client.php):

<?php
// 设置错误报告
error_reporting(E_ALL);
set_time_limit(0);
// 1. 创建一个 Socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === false) {
    echo "socket_create() failed: reason: " . socket_strerror(socket_last_error()) . "\n";
    exit();
}
// 2. 连接到服务器
$address = '127.0.0.1';
$port = 9999;
$result = socket_connect($socket, $address, $port);
if ($result === false) {
    echo "socket_connect() failed.\nReason: (" . $result . ") " . socket_strerror(socket_last_error($socket)) . "\n";
    exit();
}
echo "Connected to server at $address:$port\n";
// 3. 发送数据
$msg = "Hello, TCP Server!";
socket_write($socket, $msg, strlen($msg));
echo "Sent to server: $msg\n";
// 4. 接收服务器返回的数据
$response = socket_read($socket, 1024);
$response = trim($response);
echo "Received from server: $response\n";
// 5. 关闭连接
socket_close($socket);
?>

如何运行

  1. 启动服务器: 打开一个终端,进入脚本所在目录,运行:

    php tcp_server.php

    你会看到输出:

    TCP Server is running at 127.0.0.1:9999

    然后它会等待连接,光标会停在那里。

  2. 启动客户端: 打开另一个新终端(不要关掉服务器的终端),进入同一目录,运行:

    php tcp_client.php

    你会在客户端终端看到:

    Connected to server at 127.0.0.1:9999
    Sent to server: Hello, TCP Server!
    Received from server: Echo: Hello, TCP Server!
  3. 观察服务器终端: 你会看到服务器的输出:

    Client connected!
    Received from client: Hello, TCP Server!
    Sent back to client: Echo: Hello, TCP Server!

UDP 通信详解

UDP 是无连接的,通信过程更简单,服务器不需要“接受”连接,只需要绑定一个端口并等待数据包。

UDP 服务器端

代码 (udp_server.php):

<?php
error_reporting(E_ALL);
set_time_limit(0);
// 1. 创建一个 Socket
// SOCK_DGRAM: 使用 UDP 协议
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
if ($socket === false) {
    echo "socket_create() failed: reason: " . socket_strerror(socket_last_error()) . "\n";
    exit();
}
// 2. 绑定 IP 地址和端口号
$address = '127.0.0.1';
$port = 9998;
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
$result = socket_bind($socket, $address, $port);
if ($result === false) {
    echo "socket_bind() failed: reason: " . socket_strerror(socket_last_error($socket)) . "\n";
    exit();
}
echo "UDP Server is running at $address:$port\n";
// 3. 循环接收数据包
while (true) {
    // socket_recvfrom 会阻塞,直到收到一个数据包
    // 它会返回客户端的 IP 和端口
    $from = '';
    $port = 0;
    if (socket_recvfrom($socket, $buf, 1024, 0, $from, $port) === false) {
        echo "socket_recvfrom() failed: reason: " . socket_strerror(socket_last_error($socket)) . "\n";
        break;
    }
    echo "Received from $from:$port - $buf\n";
    // 4. 将数据包原样返回给客户端
    $response = "Echo: $buf";
    socket_sendto($socket, $response, strlen($response), 0, $from, $port);
    echo "Sent back to $from:$port\n";
}
// 关闭 Socket
socket_close($socket);
?>

UDP 客户端

代码 (udp_client.php):

<?php
error_reporting(E_ALL);
set_time_limit(0);
// 1. 创建一个 Socket
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
if ($socket === false) {
    echo "socket_create() failed: reason: " . socket_strerror(socket_last_error()) . "\n";
    exit();
}
// 2. 定义服务器地址和端口
$serverAddress = '127.0.0.1';
$serverPort = 9998;
$msg = "Hello, UDP Server!";
// 3. 发送数据包到服务器
// 注意:UDP 的 socket_write 对应的是 socket_sendto
socket_sendto($socket, $msg, strlen($msg), 0, $serverAddress, $serverPort);
echo "Sent to server: $msg\n";
// 4. 接收服务器返回的数据包
// 注意:UDP 的 socket_read 对应的是 socket_recvfrom
$from = '';
$port = 0;
socket_recvfrom($socket, $response, 1024, 0, $from, $port);
$response = trim($response);
echo "Received from $from:$port - $response\n";
// 5. 关闭 Socket
socket_close($socket);
?>

如何运行

  1. 启动 UDP 服务器

    php udp_server.php
  2. 启动 UDP 客户端: 在另一个终端运行:

    php udp_client.php

你会看到类似的交互效果,但服务器会一直运行,等待下一个数据包的到来。


进阶注意事项

阻塞 vs. 非阻塞

默认情况下,socket_accept(), socket_read(), socket_recvfrom() 等函数是阻塞的,意味着脚本会暂停执行,直到有事件发生(如连接、数据到达),对于简单的脚本来说这是可以的,但对于需要处理多个连接的服务器,阻塞会导致性能问题。

可以使用 socket_set_nonblock($socket) 将 Socket 设置为非阻塞模式,在非阻塞模式下,这些函数会立即返回,如果没有数据或连接,会返回一个错误码(EAGAINEWOULDBLOCK),你可以通过 socket_last_error() 获取。

处理多个客户端 (并发)

一个简单的 while(true) 循环只能处理一个客户端,要同时处理多个客户端,你需要使用多进程或多线程技术。

  • 多进程 (Forking):在 Linux/Unix 系统中,当一个客户端连接后,服务器 fork (分叉) 一个子进程来处理这个客户端,父进程则继续回去 accept 新的连接。
  • 多线程:PHP 没有内置的多线程支持,但可以通过 pthreads 扩展实现,但这比较复杂且不常用。
  • select() / stream_select():这是一种 I/O 多路复用技术,可以监视多个 Socket 的状态,当其中一个或多个 Socket 准备好进行读写操作时,select() 会返回,这比轮询更高效。
  • 高级扩展:对于生产环境,强烈建议使用更成熟的网络服务框架,如 SwooleReactPHP,它们基于事件驱动,性能极高,能轻松处理成千上万的并发连接。

错误处理

Socket 操作可能会因为各种原因失败(端口被占用、连接被拒绝、读取超时等),每次调用关键函数(socket_create, socket_bind, socket_listen, socket_accept, socket_read, socket_write 等)后,都应该检查其返回值,并使用 socket_strerror(socket_last_error()) 获取具体的错误信息。

数据格式

Socket 传输的是原始字节流,如果你需要发送复杂的数据结构(如数组、对象),你需要先将它们序列化。

  • 序列化serialize() / json_encode()
  • 反序列化unserialize() / json_decode()

在发送时,通常会在数据前加上数据的长度,以便接收方知道需要读取多少字节,发送 "Hello" 时,可以发送 "5:Hello"


本教程带你从零开始,了解了 PHP Socket 编程的基础知识,并实现了 TCP 和 UDP 的简单通信示例,Socket 编码是 PHP 中的一个强大功能,但同时也需要小心处理并发、错误和性能问题。

对于初学者,掌握本教程的内容已经足够,当你需要构建高性能、高并发的网络应用时,可以进一步学习 Swoole 或 ReactPHP 等现代框架。