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

本教程将分为以下几个部分:
- 基础概念:了解 Socket、TCP 和 UDP。
- 环境准备:确保你的 PHP 环境支持 Socket。
- TCP 通信详解:创建一个可靠的 TCP 服务器和客户端。
- UDP 通信详解:创建一个轻量级的 UDP 服务器和客户端。
- 进阶注意事项:处理并发、连接、错误等。
基础概念
什么是 Socket?
Socket 可以看作是两个程序之间进行通信的“端点”,一个 Socket 由一个 IP 地址和一个端口号唯一标识,当你发送数据时,数据被打包(封装)并通过 Socket 发送到目标 IP 和端口,接收方的程序则从对应的 Socket 读取数据。
TCP vs. UDP
| 特性 | TCP (传输控制协议) | UDP (用户数据报协议) |
|---|---|---|
| 连接性 | 面向连接 | 无连接 |
| 可靠性 | 可靠,通过确认、重传、排序等机制确保数据无差错、不丢失、不重复。 | 不可靠,不保证数据包的顺序或是否到达。 |
| 速度 | 较慢,因为需要建立连接和维护连接状态。 | 较快,没有连接开销,直接发送。 |
| 数据量 | 无大小限制。 | 有大小限制(通常受限于网络 MTU,约 1500 字节)。 |
| 应用场景 | 要求高可靠性的场景,如网页浏览、文件传输、邮件。 | 对速度要求高、能容忍少量丢包的场景,如视频会议、在线游戏、DNS查询。 |
环境准备
-
启用 Socket 扩展:确保你的 PHP 安装中包含了
sockets扩展。- 在 Linux/macOS 上,检查
php.ini文件,确保没有注释掉extension=sockets。 - 在 Windows 上,确保
php_sockets.dll文件在ext目录下,且php.ini中也启用了它。 - 你可以通过在命令行运行
php -m | grep sockets来检查。
- 在 Linux/macOS 上,检查
-
使用命令行:Socket 编程通常在命令行环境下进行,而不是通过 Web 服务器(如 Apache 或 Nginx),因为 Web 服务器本身已经处理了网络连接,我们将使用
php命令来运行我们的脚本。
(图片来源网络,侵删)
TCP 通信详解
TCP 是最常用的协议,我们将分步创建一个简单的“回显”(Echo)服务器和一个客户端,服务器接收客户端的消息,并将其原样返回。
TCP 服务器端
服务器端的流程通常是:
- 创建一个 Socket。
- 绑定 IP 地址和端口号。
- 开始监听连接。
- 接受客户端连接。
- 与客户端进行数据收发。
- 关闭连接和 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 客户端
客户端的流程通常是:
- 创建一个 Socket。
- 连接到服务器。
- 发送数据。
- 接收数据。
- 关闭连接和 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);
?>
如何运行
-
启动服务器: 打开一个终端,进入脚本所在目录,运行:
php tcp_server.php
你会看到输出:
TCP Server is running at 127.0.0.1:9999然后它会等待连接,光标会停在那里。
-
启动客户端: 打开另一个新终端(不要关掉服务器的终端),进入同一目录,运行:
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! -
观察服务器终端: 你会看到服务器的输出:
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);
?>
如何运行
-
启动 UDP 服务器:
php udp_server.php
-
启动 UDP 客户端: 在另一个终端运行:
php udp_client.php
你会看到类似的交互效果,但服务器会一直运行,等待下一个数据包的到来。
进阶注意事项
阻塞 vs. 非阻塞
默认情况下,socket_accept(), socket_read(), socket_recvfrom() 等函数是阻塞的,意味着脚本会暂停执行,直到有事件发生(如连接、数据到达),对于简单的脚本来说这是可以的,但对于需要处理多个连接的服务器,阻塞会导致性能问题。
可以使用 socket_set_nonblock($socket) 将 Socket 设置为非阻塞模式,在非阻塞模式下,这些函数会立即返回,如果没有数据或连接,会返回一个错误码(EAGAIN 或 EWOULDBLOCK),你可以通过 socket_last_error() 获取。
处理多个客户端 (并发)
一个简单的 while(true) 循环只能处理一个客户端,要同时处理多个客户端,你需要使用多进程或多线程技术。
- 多进程 (Forking):在 Linux/Unix 系统中,当一个客户端连接后,服务器
fork(分叉) 一个子进程来处理这个客户端,父进程则继续回去accept新的连接。 - 多线程:PHP 没有内置的多线程支持,但可以通过
pthreads扩展实现,但这比较复杂且不常用。 select()/stream_select():这是一种 I/O 多路复用技术,可以监视多个 Socket 的状态,当其中一个或多个 Socket 准备好进行读写操作时,select()会返回,这比轮询更高效。- 高级扩展:对于生产环境,强烈建议使用更成熟的网络服务框架,如 Swoole 或 ReactPHP,它们基于事件驱动,性能极高,能轻松处理成千上万的并发连接。
错误处理
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 等现代框架。
