PHP 有一个类可以通过 SSH 连接到服务器。本教程将解释如何使用它。

给 WikiBooks 管理员的说明:是的,我知道这很乱,我会尽快整理好 微笑 只是我觉得 WikiBooks 是放这个教程最合适的地方,因为 php.net 的评论区觉得它有点长 :P

给读者的说明:正如你们可能已经看到的,这真的是一个非常非常长的教程。不是因为它是一个难懂的概念,而是因为我想以如此详细的方式解释,以便任何人都能理解。如果你已经有一点 PHP 经验,也没有关系。查看我下面展示的源代码,如果有哪一行不懂,可以在我的教程中查找。我在三个非常重要的概念旁标注了很多 *********。

希望这能帮助很多人。这是我第一次做这么详细的教程,所以请给我一些反馈 微笑

我在让 ssh2 正常工作时遇了不少麻烦,主要是因为我没有理解很多命令背后的概念。因此,当我尝试下面不同用户提供的脚本时,它们并没有工作。我对每个函数进行了研究,做出了一个(几乎)无错误的脚本。最重要的是,我将解释代码中每一行发生了什么,和很多其他用户不同,我会解释每一步做了什么。

下面是代码。你将使用的函数是 readwrite。它通过 SSH 使用用户名/密码认证进行连接,发送一个命令并查找某个特定的输出。如果找到了它,就返回 true。显然,你可能希望做一些不同的事情(例如:获取命令的所有输出,运行多个命令等)。因此,我将详细解释每一行的具体含义。相信我,如果你不理解脚本的某部分,一定要阅读解释!你不想在这里走捷径,否则如果你的情况与我的稍有不同,你可能会得到非常意外的结果。

完整脚本

function readwrite ($write, $lookfor,$ip,$user,$pass) {
    flush(); // 写出之前的所有内容

    // 连接并认证
    $connection = ssh2_connect($ip) or die ("can't connect");
    ssh2_auth_password($connection,$user,$pass) or die("can't auth");

    // 启动 Shell
    $shell = ssh2_shell($connection,"xterm");

    // 等待 Shell 初始化
    usleep(200000); // 如果结果不如预期,可以增加这个值

    $write = "echo '[start]'; $write ; echo '[end]'";
    
    $out = user_exec($shell, $write);
    fclose($shell);
    if(stristr($out, $lookfor)) { // 它存在
        return true;
    }
}

function user_exec($shell,$cmd) {
    fwrite($shell,$cmd . PHP_EOL); // 写入命令到 shell
    $output = ""; // 用来存储输出
    $start = false; // 是否开始了
    $start_time = time(); // 开始时间
    $max_time = 10; // 最大等待时间,单位秒
    while(((time()-$start_time) < $max_time)) { // 如果未超过最大时间
        $line = fgets($shell); // 获取下一行输出
        if(!stristr($line,$cmd)) { // 排除输出中包含的命令本身(因为它也包含 [start] 和 [end])
            if(preg_match('/\[start\]/',$line)) { // 如果看到 [start],表示开始
                $start = true; // 设置开始为 true
            }
            elseif(preg_match('/\[end\]/',$line)) { // 如果看到 [end],表示结束
                    return $output; // 返回输出(最后一行)
            }
            elseif($start && isset($line) && $line != "") {
                    $output = $line; // 返回最后一行输出
            }
        }
    }
}

脚本说明

readwrite 函数头

function readwrite ($write, $lookfor,$ip,$user,$pass) {
}

这是我 readwrite 函数的函数头。它接受 5 个参数:

  • $write:这是我们要发送的命令(例如:cat logfile)。它可以是任何有效的 Bash 命令。
  • $lookfor:这是我们希望返回的某个特定值。例如,如果我们希望查看某个命令的退出状态,可能会是 "0"。
  • $ip、$user、$pass:这些参数分别是连接到远程服务器的 IP 地址、用户名和密码。

注意:$write 中的特殊字符(如 $ 和引号)需要进行转义。如果你不确定命令中是否包含特殊字符,可以先在终端测试这些命令。

flush() 命令

flush(); // 写出之前的所有内容

这是可选的。在我的情况下,我在循环中运行 readwrite 函数,并希望实时查看结果。flush() 会将我们在 echo 缓冲区中的内容立即输出,而不是等待整个脚本执行完毕。

连接与认证

$connection = ssh2_connect($ip) or die ("can't connect");
ssh2_auth_password($connection,$user,$pass) or die("can't auth");

这些行首先通过 ssh2_connect() 函数连接到指定的远程主机。然后通过 ssh2_auth_password() 使用用户名和密码进行认证。如果连接或认证失败,将终止脚本并给出错误信息。

启动 Shell 流

$shell = ssh2_shell($connection,"xterm");

这行代码启动一个交互式的 shell 流。Shell 流是非常有用的,它允许我们通过 PHP 与远程服务器的命令行界面进行交互。

等待 Shell 初始化

usleep(200000); // 增加此值以避免出现不期望的结果

由于某些情况(例如:网络延迟或慢速连接),Shell 初始化可能需要一些时间。因此,我们通过 usleep() 函数让脚本等待一段时间,以确保 Shell 已经准备好处理命令。

格式化命令

$write = "echo '[start]'; $write ; echo '[end]'";

我们将命令用 echo '[start]'echo '[end]' 包裹起来。这是为了确保我们只获取命令的输出,而不包括其他无关的输出。

发送请求并获取输出

$out = user_exec($shell, $write);

我们调用 user_exec() 函数来执行命令,并将输出存储在 $out 变量中。

关闭 Shell 流

fclose($shell);

脚本执行完毕后,我们关闭 Shell 流,类似于 mysql_close() 关闭数据库连接。

检查输出是否包含目标字符串

if(stristr($out, $lookfor)) { // 它存在
    return true;
}

stristr() 函数用来查找 $out 中是否包含 $lookfor 字符串。如果找到了目标字符串,函数返回 true

用户执行函数 (user_exec)

该函数定义如下:

function user_exec($shell, $cmd) {
}

首先,我们回顾一下之前调用的 user_exec 函数,实际上就是这个函数被执行了。我们传入了两个参数:$shell 流和命令 $cmd

向 Shell 流写入数据

fwrite($shell, $cmd . PHP_EOL); //向 shell 写入命令

记得我之前说过可以从流中读取数据?其实你也可以写入流!fwrite 就是用来做这件事的!我们将命令和 PHP_EOL(表示换行符)写入 $shell 流。PHP_EOL 是 PHP 中换行符的常量,相当于按下键盘的回车键。虽然你也可以使用 \r\n,但有人建议使用 PHP_EOL,这样做也无妨(能不能有人解释一下为什么?)

一些局部变量

$output = ""; //用来存储输出
$start = false; //是否开始处理

$output 用来存储输出内容,而 $start 用来标记是否已经到达 "[start]" 标记。

循环计时:为什么它很重要

$start_time = time(); //记录开始时间
$max_time = 10; //最大执行时间,单位:秒
while(((time() - $start_time) < $max_time)) { }//如果未超时

在这里,我们使用 time() 函数来获取当前的 Unix 时间戳(即自 Unix 纪元以来的秒数)。如果命令运行时间过长,while($line = fgets($shell)) 就无法正常工作了。

假设我们执行的是一个 shell 脚本,运行需要 3 秒钟。那么如果我们使用 while($line = fgets($shell)),就会遇到以下问题:

  1. 启动 SSH 连接,获取 shell 流。
  2. 等待几毫秒加载头信息。
  3. 向 shell 写入命令执行脚本。
  4. 如果脚本运行 2 秒钟,PHP 会尝试读取输出,但头信息可能只用了 0.5 秒就处理完了,接下来 PHP 会读取脚本的输出,而此时脚本还没有输出任何内容。PHP 会误以为已经没有输出,并退出循环。

因此,使用计时来确保脚本的执行不会因为超时而导致问题。

脚本计时:为什么要设置超时

$start_time = time(); //开始时间
$max_time = 10; //最大超时时间(秒)
while(((time() - $start_time) < $max_time)) { }//检查是否超时

这是一个非常聪明且简单的实现方式。time() 返回当前的 Unix 时间戳,通过与 start_time 的差值来判断是否超时。每次循环都会检查是否超时,直到达到最大时间。

注意: 设置 $max_time 时,要确保脚本的执行时间小于 $max_time。如果设置一个过大的时间(比如 1000000000000000000 秒),可能会导致脚本无限执行,直到超时,这会造成问题。建议设置一个合理的时间限制。

获取下一行输出

$line = fgets($shell); //获取下一行输出

每次循环时,通过 fgets($shell) 获取 shell 脚本的下一行输出。

确保命令不会出现在输出中

if (!stristr($line, $cmd)) {} //避免输出中包含命令本身

我们只关心命令的输出,而不是其他终端的随机信息。通过 stristr 函数检查当前行是否包含命令内容。如果包含,则跳过该行。

检查输出中的 "[start]" 标记

if (preg_match('/\[start\]/', $line)) { }//检查是否遇到 [start] 标记

当遇到 "[start]" 标记时,设置 $start = true,表示我们已经开始接收输出。

检查输出中的 "[end]" 标记

elseif (preg_match('/\[end\]/', $line)) { //检查是否遇到 [end] 标记
    return $output;} //返回输出并结束循环

当遇到 "[end]" 标记时,返回已经收集的输出,并结束循环。

确保输出不为空

elseif ($start && isset($line) && $line != "") {
    $output = $line; //将当前行赋值给输出
}

只有在遇到有效的输出时(即 $starttrue$line 不为空),才会将当前行保存到 $output 中。如果你想保存所有输出,可以使用 .= $line . "\n" 或将每行输出存储到数组中。


这段代码是为了处理一个长时间运行的命令,并确保可以正确获取它的输出。通过循环和计时机制,我们确保脚本运行期间不会错过任何输出,特别是当脚本输出缓慢时。

总结

本教程解释了如何使用 PHP 通过 SSH 连接到远程服务器,发送命令并检查返回结果。希望这能帮助到你,尤其是如果你在用 PHP 和 SSH 交互时遇到问题。

最后修改: 2025年01月10日 星期五 01:47