PHP编程
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))
,就会遇到以下问题:
- 启动 SSH 连接,获取 shell 流。
- 等待几毫秒加载头信息。
- 向 shell 写入命令执行脚本。
- 如果脚本运行 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; //将当前行赋值给输出
}
只有在遇到有效的输出时(即 $start
为 true
且 $line
不为空),才会将当前行保存到 $output
中。如果你想保存所有输出,可以使用 .= $line .
"\n"
或将每行输出存储到数组中。
这段代码是为了处理一个长时间运行的命令,并确保可以正确获取它的输出。通过循环和计时机制,我们确保脚本运行期间不会错过任何输出,特别是当脚本输出缓慢时。
总结
本教程解释了如何使用 PHP 通过 SSH 连接到远程服务器,发送命令并检查返回结果。希望这能帮助到你,尤其是如果你在用 PHP 和 SSH 交互时遇到问题。