Socket 编程
Socket(套接字)是网络通信的底层接口。PHP 提供了完整的 Socket 扩展,允许开发者直接操作 TCP/UDP 套接字,构建自定义网络协议的服务器和客户端。本节将系统讲解 PHP Socket 编程的核心概念、函数用法和实战案例。
基础概念
什么是 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_EACCES | 13 | 权限不足 |
SOCKET_EADDRINUSE | 98 | 地址已使用 |
SOCKET_ECONNREFUSED | 111 | 连接被拒绝 |
SOCKET_EINPROGRESS | 115 | 操作正在进行 |
SOCKET_EAGAIN | 11 | 资源暂时不可用 |
资源清理
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);
}最佳实践
- 生产环境推荐使用 Swoole:PHP 的原生 Socket 扩展适合简单场景,高性能场景推荐使用 Swoole 扩展
- 使用
socket_select实现并发:避免多进程/多线程的复杂度 - 设置合理的超时:防止死连接占用资源
- 设置
SO_REUSEADDR:避免 TIME_WAIT 状态导致重启失败 - 异常处理:所有 socket_* 操作都应检查返回值
- 使用非阻塞模式:提升服务器并发处理能力
下一节
继续学习:FTP / SSH2 操作