FTP / SSH2 操作
PHP 提供了强大的远程文件操作扩展,包括 FTP 扩展和 SSH2 扩展。FTP 扩展用于与 FTP 服务器交互,SSH2 扩展则提供了安全的远程 shell 访问、文件传输和端口转发能力。本节将全面讲解这两个扩展的使用方法。
基础概念
FTP 扩展
PHP 的 FTP 扩展提供了完整的 FTP 客户端功能,支持:
- 文件上传、下载、删除、重命名
- 目录创建、切换、列表
- 被动/主动模式切换
- SSL/TLS 加密连接(ftps)
- 文件权限修改
SSH2 扩展
SSH2 扩展基于 libssh2 库,支持:
- SSH 密码和密钥认证
- 远程命令执行
- SFTP(安全文件传输)
- SCP 文件复制
- 端口转发
安装
bash
# FTP 扩展(通常默认启用)
./configure --enable-ftp
# SSH2 扩展
# 先安装 libssh2
sudo apt-get install libssh2-1-dev
# 然后编译 PHP
pecl install ssh2
# 或在编译时
./configure --with-ssh2FTP 操作
连接与认证
php
<?php
declare(strict_types=1);
/**
* FTP 连接管理类
*/
class FtpConnection
{
private \FTP\Connection $ftp;
private bool $loggedIn = false;
/**
* 创建 FTP 连接
*/
public function __construct(
string $host,
int $port = 21,
int $timeout = 90
) {
// PHP 8.1+ 使用 \FTP\Connection 类
$this->ftp = ftp_connect($host, $port, $timeout);
if ($this->ftp === false) {
throw new RuntimeException("FTP 连接失败: {$host}:{$port}");
}
}
/**
* 密码登录
*/
public function login(string $username, string $password): void
{
$result = ftp_login($this->ftp, $username, $password);
if (!$result) {
throw new RuntimeException("FTP 登录失败");
}
$this->loggedIn = true;
}
/**
* SSL/TLS 加密登录
*/
public function sslLogin(string $username, string $password): void
{
// 启用 TLS
ftp_set_option($this->ftp, FTP_USE_TLS, true);
$this->login($username, $password);
}
/**
* 设置被动模式
*/
public function setPassive(bool $passive = true): void
{
ftp_pasv($this->ftp, $passive);
}
/**
* 获取连接句柄
*/
public function getConnection(): \FTP\Connection
{
return $this->ftp;
}
/**
* 关闭连接
*/
public function close(): void
{
if ($this->loggedIn) {
ftp_close($this->ftp);
}
}
public function __destruct()
{
$this->close();
}
}
// 使用示例
$ftp = new FtpConnection('ftp.example.com');
$ftp->login('username', 'password');
$ftp->setPassive(true); // 推荐使用被动模式文件上传下载
php
<?php
declare(strict_types=1);
class FtpManager
{
public function __construct(
private readonly \FTP\Connection $ftp
) {}
/**
* 上传文件
*/
public function upload(
string $localPath,
string $remotePath,
int $mode = FTP_BINARY
): bool {
if (!file_exists($localPath)) {
throw new RuntimeException("本地文件不存在: {$localPath}");
}
// FTP_ASCII 或 FTP_BINARY
$result = ftp_put($this->ftp, $remotePath, $localPath, $mode);
if (!$result) {
throw new RuntimeException(
"上传失败: " . ftp_pwd($this->ftp)
);
}
return true;
}
/**
* 下载文件
*/
public function download(
string $remotePath,
string $localPath,
int $mode = FTP_BINARY
): bool {
$result = ftp_get($this->ftp, $localPath, $remotePath, $mode);
if (!$result) {
throw new RuntimeException("下载失败: {$remotePath}");
}
return true;
}
/**
* 断点续传上传
*/
public function uploadResume(
string $localPath,
string $remotePath,
int $startPos = 0,
int $mode = FTP_BINARY
): bool {
return ftp_put(
$this->ftp,
$remotePath,
$localPath,
$mode,
$startPos
);
}
/**
* 断点续传下载
*/
public function downloadResume(
string $remotePath,
string $localPath,
int $mode = FTP_BINARY,
int $resumePos = 0
): bool {
return ftp_get(
$this->ftp,
$localPath,
$remotePath,
$mode,
$resumePos
);
}
/**
* 非阻塞上传(8.2+ ftp_nb_put)
*/
public function uploadNonBlocking(
string $localPath,
string $remotePath,
int $mode = FTP_BINARY
): void {
$result = ftp_nb_put($this->ftp, $remotePath, $localPath, $mode);
while ($result === FTP_MOREDATA) {
// 执行其他任务
$result = ftp_nb_continue($this->ftp);
}
if ($result !== FTP_FINISHED) {
throw new RuntimeException("非阻塞上传失败");
}
}
}目录操作
php
<?php
declare(strict_types=1);
class FtpDirectoryManager
{
public function __construct(
private readonly \FTP\Connection $ftp
) {}
/**
* 列出目录内容
*/
public function list(string $path = '.'): array
{
$items = ftp_nlist($this->ftp, $path);
return $items !== false ? $items : [];
}
/**
* 获取详细目录列表
*/
public function rawList(string $path = '.'): array
{
$items = ftp_rawlist($this->ftp, $path);
return $items !== false ? $items : [];
}
/**
* 切换目录
*/
public function changeDir(string $path): bool
{
return ftp_chdir($this->ftp, $path);
}
/**
* 获取当前目录
*/
public function currentDir(): string|false
{
return ftp_pwd($this->ftp);
}
/**
* 创建目录(支持递归)
*/
public function makeDir(string $path): bool
{
$parts = explode('/', trim($path, '/'));
$currentPath = ftp_pwd($this->ftp);
foreach ($parts as $part) {
if (empty($part)) {
continue;
}
if (!@ftp_chdir($this->ftp, $part)) {
if (!ftp_mkdir($this->ftp, $part)) {
return false;
}
ftp_chdir($this->ftp, $part);
}
}
ftp_chdir($this->ftp, $currentPath);
return true;
}
/**
* 删除目录(支持递归)
*/
public function removeDir(string $path): bool
{
$list = ftp_rawlist($this->ftp, $path);
if ($list === false) {
return ftp_rmdir($this->ftp, $path);
}
foreach ($list as $item) {
// 解析 rawlist 格式
$parts = preg_split('/\s+/', $item);
$type = substr($parts[0], 0, 1);
$name = end($parts);
if ($type === 'd') {
$this->removeDir($path . '/' . $name);
} else {
ftp_delete($this->ftp, $path . '/' . $name);
}
}
return ftp_rmdir($this->ftp, $path);
}
/**
* 删除文件
*/
public function deleteFile(string $path): bool
{
return ftp_delete($this->ftp, $path);
}
/**
* 重命名文件/目录
*/
public function rename(string $oldName, string $newName): bool
{
return ftp_rename($this->ftp, $oldName, $newName);
}
/**
* 获取文件大小
*/
public function fileSize(string $path): int|false
{
return ftp_size($this->ftp, $path);
}
/**
* 获取最后修改时间
*/
public function lastModified(string $path): int|false
{
return ftp_mdtm($this->ftp, $path);
}
/**
* 获取文件权限
*/
public function chmod(string $path, int $mode): bool
{
return ftp_chmod($this->ftp, $mode, $path);
}
/**
* 系统类型标识
*/
public function systemType(): string|false
{
return ftp_systype($this->ftp);
}
}SSH2 操作
SSH 连接与认证
php
<?php
declare(strict_types=1);
/**
* SSH2 连接类
*/
class SshConnection
{
private \SSH2\Connection $connection;
private bool $authenticated = false;
public function __construct(
string $host,
int $port = 22,
array $methods = []
) {
$this->connection = ssh2_connect($host, $port, $methods);
if ($this->connection === false) {
throw new RuntimeException("SSH 连接失败: {$host}:{$port}");
}
}
/**
* 密码认证
*/
public function authPassword(string $username, string $password): void
{
if (!ssh2_auth_password($this->connection, $username, $password)) {
throw new RuntimeException("SSH 密码认证失败");
}
$this->authenticated = true;
}
/**
* 公钥认证
*/
public function authPubkey(
string $username,
string $pubkeyFile,
string $privkeyFile,
string $passphrase = ''
): void {
if (!ssh2_auth_pubkey_file(
$this->connection,
$username,
$pubkeyFile,
$privkeyFile,
$passphrase
)) {
throw new RuntimeException("SSH 公钥认证失败");
}
$this->authenticated = true;
}
/**
* Host Key 验证
*/
public function verifyHostKey(
string $knownHostsFile,
int $knownHostsFormat = SSH2_HOSTKEY_TYPE_RSA
): bool {
$fingerprint = ssh2_fingerprint(
$this->connection,
$knownHostsFormat
);
// 在实际应用中,应与 known_hosts 文件比对
echo "服务器指纹: {$fingerprint}" . PHP_EOL;
return true;
}
public function getConnection(): \SSH2\Connection
{
return $this->connection;
}
public function close(): void
{
// ssh2_disconnect($this->connection);
}
public function __destruct()
{
$this->close();
}
}
// 使用示例
$ssh = new SshConnection('server.example.com', 22);
// 密码认证
$ssh->authPassword('user', 'password');
// 公钥认证(推荐)
// $ssh->authPubkey('user', '/home/user/.ssh/id_rsa.pub', '/home/user/.ssh/id_rsa');远程命令执行
php
<?php
declare(strict_types=1);
class SshExecutor
{
public function __construct(
private readonly \SSH2\Connection $connection
) {}
/**
* 执行命令并返回输出
*/
public function exec(string $command): string
{
$stream = ssh2_exec($this->connection, $command);
if ($stream === false) {
throw new RuntimeException("命令执行失败: {$command}");
}
stream_set_blocking($stream, true);
$stdout = stream_get_contents($stream);
fclose($stream);
return $stdout;
}
/**
* 执行命令并获取 stdout 和 stderr
*/
public function execWithStderr(string $command): array
{
$stdoutStream = ssh2_exec($this->connection, $command);
$stderrStream = ssh2_fetch_stream($stdoutStream, SSH2_STREAM_STDERR);
stream_set_blocking($stdoutStream, true);
stream_set_blocking($stderrStream, true);
$stdout = stream_get_contents($stdoutStream);
$stderr = stream_get_contents($stderrStream);
fclose($stdoutStream);
fclose($stderrStream);
return [
'stdout' => $stdout,
'stderr' => $stderr,
'exitCode' => 0, // 需要通过额外命令获取
];
}
/**
* 交互式 Shell
*/
public function shell(callable $callback): void
{
$stream = ssh2_shell($this->connection, 'xterm-256color');
if ($stream === false) {
throw new RuntimeException("创建 Shell 失败");
}
stream_set_blocking($stream, false);
$callback($stream);
fclose($stream);
}
/**
* 获取退出状态码
*/
public function execWithExitCode(string $command): array
{
// 先执行命令,再获取退出码
$output = $this->exec($command);
$exitCode = (int) trim($this->exec('echo $?'));
return [
'output' => $output,
'exitCode' => $exitCode,
];
}
}
// 使用示例
$ssh = new SshConnection('server.example.com');
$ssh->authPassword('user', 'password');
$executor = new SshExecutor($ssh->getConnection());
// 基本命令执行
$output = $executor->exec('ls -la /var/log');
echo $output;
// 带 stderr 的执行
$result = $executor->execWithStderr('some-command 2>&1');
echo "stdout: {$result['stdout']}";
echo "stderr: {$result['stderr']}";
// 带退出码的执行
$result = $executor->execWithExitCode('grep "pattern" /etc/config');
echo "输出: {$result['output']}";
echo "退出码: {$result['exitCode']}";SFTP 文件操作
php
<?php
declare(strict_types=1);
/**
* SFTP 文件管理器
*/
class SftpManager
{
private \SSH2\Connection $connection;
private $sftp;
public function __construct(\SSH2\Connection $connection)
{
$this->connection = $connection;
$this->sftp = ssh2_sftp($connection);
if ($this->sftp === false) {
throw new RuntimeException("初始化 SFTP 子系统失败");
}
}
/**
* 上传文件
*/
public function upload(
string $localPath,
string $remotePath
): bool {
if (!file_exists($localPath)) {
throw new RuntimeException("本地文件不存在: {$localPath}");
}
$stream = @fopen("ssh2.sftp://{$this->sftp}{$remotePath}", 'w');
if ($stream === false) {
throw new RuntimeException("无法打开远程文件: {$remotePath}");
}
$data = file_get_contents($localPath);
$written = fwrite($stream, $data);
fclose($stream);
return $written === strlen($data);
}
/**
* 使用 ssh2_scp_send 上传
*/
public function scpSend(
string $localPath,
string $remotePath,
int $permissions = 0644
): bool {
return ssh2_scp_send(
$this->connection,
$localPath,
$remotePath,
$permissions
);
}
/**
* 下载文件
*/
public function download(string $remotePath, string $localPath): bool
{
$stream = @fopen("ssh2.sftp://{$this->sftp}{$remotePath}", 'r');
if ($stream === false) {
throw new RuntimeException("无法读取远程文件: {$remotePath}");
}
$content = stream_get_contents($stream);
fclose($stream);
return file_put_contents($localPath, $content) !== false;
}
/**
* 使用 ssh2_scp_recv 下载
*/
public function scpRecv(string $remotePath, string $localPath): bool
{
return ssh2_scp_recv($this->connection, $remotePath, $localPath);
}
/**
* 列出目录
*/
public function listDir(string $path = '.'): array
{
$handle = @opendir("ssh2.sftp://{$this->sftp}{$path}");
if ($handle === false) {
throw new RuntimeException("无法打开目录: {$path}");
}
$items = [];
while (false !== ($name = readdir($handle))) {
if ($name === '.' || $name === '..') {
continue;
}
$stat = $this->stat("{$path}/{$name}");
$items[] = [
'name' => $name,
'isDir' => is_dir("ssh2.sftp://{$this->sftp}{$path}/{$name}"),
'size' => $stat['size'] ?? 0,
'mtime' => $stat['mtime'] ?? 0,
];
}
closedir($handle);
return $items;
}
/**
* 创建目录
*/
public function mkdir(string $path, int $mode = 0755, bool $recursive = true): bool
{
$result = ssh2_sftp_mkdir($this->sftp, $path, $mode, $recursive);
return $result;
}
/**
* 删除文件
*/
public function unlink(string $path): bool
{
return ssh2_sftp_unlink($this->sftp, $path);
}
/**
* 删除目录
*/
public function rmdir(string $path): bool
{
return ssh2_sftp_rmdir($this->sftp, $path);
}
/**
* 重命名
*/
public function rename(string $from, string $to): bool
{
return ssh2_sftp_rename($this->sftp, $from, $to);
}
/**
* 获取文件信息
*/
public function stat(string $path): array|false
{
return ssh2_sftp_stat($this->sftp, $path);
}
/**
* 获取符号链接信息
*/
public function lstat(string $path): array|false
{
return ssh2_sftp_lstat($this->sftp, $path);
}
/**
* 修改权限
*/
public function chmod(string $path, int $mode): bool
{
return ssh2_sftp_chmod($this->sftp, $path, $mode);
}
/**
* 判断文件是否存在
*/
public function exists(string $path): bool
{
return file_exists("ssh2.sftp://{$this->sftp}{$path}");
}
}
// 使用示例
$ssh = new SshConnection('server.example.com');
$ssh->authPubkey('deploy', '/home/deploy/.ssh/id_rsa.pub', '/home/deploy/.ssh/id_rsa');
$sftp = new SftpManager($ssh->getConnection());
// 上传部署文件
$sftp->scpSend('/local/app.tar.gz', '/remote/app.tar.gz');
// 执行远程解压命令
$executor = new SshExecutor($ssh->getConnection());
$executor->exec('cd /remote && tar xzf app.tar.gz');
// 列出远程目录
$files = $sftp->listDir('/remote/app');
print_r($files);实战示例
自动化部署脚本
php
<?php
declare(strict_types=1);
/**
* 基于 SSH2 的自动化部署工具
*/
class Deployer
{
private SshConnection $ssh;
private SshExecutor $executor;
private SftpManager $sftp;
public function __construct(
private readonly string $host,
private readonly string $user,
private readonly string $pubKey,
private readonly string $privKey,
private readonly string $deployPath,
private readonly string $remoteApp
) {
$this->ssh = new SshConnection($host);
$this->ssh->authPubkey($user, $pubKey, $privKey);
$this->executor = new SshExecutor($this->ssh->getConnection());
$this->sftp = new SftpManager($this->ssh->getConnection());
}
public function deploy(string $localArchive): void
{
echo "=== 开始部署 ===" . PHP_EOL;
// 1. 上传归档文件
echo "1. 上传文件..." . PHP_EOL;
$remoteArchive = "/tmp/deploy_" . uniqid() . '.tar.gz';
$this->sftp->scpSend($localArchive, $remoteArchive);
// 2. 创建备份
echo "2. 创建备份..." . PHP_EOL;
$backupDir = $this->deployPath . '_backup_' . date('YmdHis');
$this->executor->exec("cp -r {$this->deployPath} {$backupDir}");
// 3. 解压新版本
echo "3. 解压新版本..." . PHP_EOL;
$this->executor->exec("mkdir -p {$this->deployPath}");
$this->executor->exec("tar xzf {$remoteArchive} -C {$this->deployPath}");
// 4. 清理临时文件
echo "4. 清理临时文件..." . PHP_EOL;
$this->executor->exec("rm -f {$remoteArchive}");
// 5. 重启服务
echo "5. 重启服务..." . PHP_EOL;
$result = $this->executor->execWithExitCode(
"cd {$this->deployPath} && php artisan down && "
. "composer install --no-dev && "
. "php artisan migrate --force && "
. "php artisan cache:clear && "
. "php artisan config:cache && "
. "php artisan up"
);
echo $result['output'];
if ($result['exitCode'] !== 0) {
// 回滚
echo "部署失败,正在回滚..." . PHP_EOL;
$this->executor->exec("rm -rf {$this->deployPath}");
$this->executor->exec("mv {$backupDir} {$this->deployPath}");
throw new RuntimeException("部署失败,已回滚");
}
echo "=== 部署完成 ===" . PHP_EOL;
}
}注意事项
FTP 安全
安全提示
- FTP 传输是明文的,敏感数据应使用 FTPS 或 SFTP
- 生产环境推荐使用 SFTP(SSH2)代替 FTP
- 避免将密码硬编码在代码中,使用环境变量或配置文件
SSH 安全
- 使用公钥认证代替密码认证
- 在连接前验证服务器指纹
- 限制 SSH 密钥的使用范围
错误处理
php
<?php
declare(strict_types=1);
// FTP 错误处理
function safeFtpPut(\FTP\Connection $ftp, string $remote, string $local): bool
{
$result = @ftp_put($ftp, $remote, $local, FTP_BINARY);
if (!$result) {
echo "FTP 错误: 上传 {$remote} 失败" . PHP_EOL;
// 获取详细错误信息
echo "当前目录: " . ftp_pwd($ftp) . PHP_EOL;
echo "系统类型: " . ftp_systype($ftp) . PHP_EOL;
return false;
}
return true;
}最佳实践
- 使用 SFTP 代替 FTP:SFTP 基于 SSH,天然加密
- 被动模式:FTP 客户端设置被动模式避免防火墙问题
- 超时设置:设置合理的连接和数据传输超时
- 资源释放:确保连接在使用后正确关闭
- 异常处理:封装所有远程操作到 try-catch 中
- 日志记录:记录远程操作便于排查问题
下一节
继续学习:SOAP