我的博客第一篇讲的就是用Maverick组件实现java ssh协议采集,可惜Maverick是个商业软件,不开放源码且只有45天的试用期。实际上在网上也能搜到不少实现java ssh的开源组件,例如orion-ssh2,trilead-ssh2,ganymed-ssh2,mindterm等组件,实际上orion,trilead,ganymed都是用的相近的源码,这个可以从源码结构看出来。我就用ganymed-ssh2进行研究。
在google上找到的ganymed-ssh2的官网是http://www.ganymed.ethz.ch/ssh2/,进去看官网的英文简介可以看到该网站已经不维护该项目,并已经迁移到http://www.cleondris.ch/,在这个网站点击右上角的Contact,再点击open source就可以看到这个项目的新家,http://www.cleondris.ch/opensource/ssh2/,上面简单介绍了该项目能远程连接上远程机器,支持命令模式和shell模式,本地和远程端口转发,没有任何JCE依赖等,最后特别指出这个项目是为瑞士苏黎世的一个项目所创建。下面提供了2010-08-23发布的ganymed-ssh2-build251beta1.zip可供下载使用,下面还有在线文档和FAQ供开发者参考。
将该文件下载下来解压后可以看到目录结构很简单清晰,ganymed-ssh2-build251beta1.jar就放在外层目录下,examples里面放了几个怎么使用的例子,faq里面是个faq的网页,javadoc是api文档,src里面是源码,我就直接参照例子里最基础的Basic.java进行模仿做一个使用例子。Basic.java的源码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
import ch.ethz.ssh2.StreamGobbler;
public class Basic
{
public static void main(String[] args)
{
String hostname = "127.0.0.1";
String username = "joe";
String password = "joespass";
try
{
/* Create a connection instance */
Connection conn = new Connection(hostname);
/* Now connect */
conn.connect();
/* Authenticate.
* If you get an IOException saying something like
* "Authentication method password not supported by the server at this stage."
* then please check the FAQ.
*/
boolean isAuthenticated = conn.authenticateWithPassword(username, password);
if (isAuthenticated == false)
throw new IOException("Authentication failed.");
/* Create a session */
Session sess = conn.openSession();
sess.execCommand("uname -a && date && uptime && who");
System.out.println("Here is some information about the remote host:");
/*
* This basic example does not handle stderr, which is sometimes dangerous
* (please read the FAQ).
*/
InputStream stdout = new StreamGobbler(sess.getStdout());
BufferedReader br = new BufferedReader(new InputStreamReader(stdout));
while (true)
{
String line = br.readLine();
if (line == null)
break;
System.out.println(line);
}
/* Show exit status, if available (otherwise "null") */
System.out.println("ExitCode: " + sess.getExitStatus());
/* Close this session */
sess.close();
/* Close the connection */
conn.close();
}
catch (IOException e)
{
e.printStackTrace(System.err);
System.exit(2);
}
}
}
我参照该代码写了自己的工具类,自己的需求是要把命令执行结果返回,而不是像例子逐行打印,对该例子打印结果部分稍作修改,写完后跑起来居然发送的命令很多都没结果返回值,少数有结果返回值的也都在一行而没有分行,没分行的原因很好找是我还是用的
String line = br.readLine();
这里必须用另br.,read的方法来做,才能在结果中保留换行符。但是没有结果值的问题让我困惑了半天,官方的例子都没结果值,这到底怎么搞得,网上别人博客也都是这样写的啊。想来想去,就去看api中Session这个类中有哪些方法,我注意到
startShell()
方法,在Maverick项目中不是也用到类似的方法了吗?马上就试下,将这个方法调用放在execCommand方法之前,跑起来这回更离谱了,在跑到exexCommand方法时居然报出了IOException,异常内容是A remote execution has already started.,意思很好理解是就是有个远程方法已经执行了,这下就又搞昏头了,看来这样也不好使啊。脑子一动,开源软件最大的优点不是咋可以看看源码吗?打开源码Session.java看到两个方法的源码如下:
/**
* Execute a command on the remote machine.
*
* @param cmd
* The command to execute on the remote host.
* @throws IOException
*/
public void execCommand(String cmd) throws IOException
{
if (cmd == null)
throw new IllegalArgumentException("cmd argument may not be null");
synchronized (this)
{
/* The following is just a nicer error, we would catch it anyway later in the channel code */
if (flag_closed)
throw new IOException("This session is closed.");
if (flag_execution_started)
throw new IOException("A remote execution has already started.");
flag_execution_started = true;
}
cm.requestExecCommand(cn, cmd);
}
/**
* Start a shell on the remote machine.
*
* @throws IOException
*/
public void startShell() throws IOException
{
synchronized (this)
{
/* The following is just a nicer error, we would catch it anyway later in the channel code */
if (flag_closed)
throw new IOException("This session is closed.");
if (flag_execution_started)
throw new IOException("A remote execution has already started.");
flag_execution_started = true;
}
cm.requestShell(cn);
}
这两个方法的源码还不晦涩,一看就明了,在startShell中把flag_execution_started置为true,那个execCommand不是有个if (flag_execution_started)就抛new IOException("A remote execution has already started.")吗,看来很明显,这两个方法根本不能同时用,那咋搞,继续整呗,程序员哪能轻易缴械投降。我参照Maverick的代码发现要有打开伪终端的方法,我发现这个Session里也有
public void requestPTY(java.lang.String term, int term_width_characters, int term_height_characters, int term_width_pixels, int term_height_pixels, byte[] terminal_modes)方法,参照该方法的参数说明
term - The TERM environment variable value (e.g., vt100)
term_width_characters - terminal width, characters (e.g., 80)term_height_characters - terminal height, rows (e.g., 24)
term_width_pixels - terminal width, pixels (e.g., 640)
term_height_pixels - terminal height, pixels (e.g., 480)
terminal_modes - encoded terminal modes (may be null)
我在执行命令前加上 session.requestPTY("vt100", 80, 24, 640, 480, null);然后再进行测试,事不过三,终于成功采到值。完整代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
import ch.ethz.ssh2.StreamGobbler;
/**
* 运用Ganymed-ssh2开源组件实现ssh协议采集工具类
*
* @author ChangPeng 2012-10-20
*
*/
public final class SSHGanymedUtil {
/**
* 日志信息
*/
private Logger logger;
/**
* 登陆ip
*/
private String hostname;
/**
* 采集端口
*/
private int port;
/**
* ssh用户
*/
private String username;
/**
* ssh口令
*/
private String password;
/**
* 性能指标采集任务
*/
private long taskId;
/**
* ssh采集会话
*/
private Connection connection;
/**
* 构造函数
*
* @param _hostname
* 登陆ip
* @param _port
* 端口
* @param _username
* ssh用户
* @param _password
* ssh口令
*/
public SSHGanymedUtil(String _hostname, int _port, String _username,
String _password) {
this.hostname = _hostname;
this.port = _port;
this.username = _username;
this.password = _password;
}
/**
* 构造函数
*
* @param _hostname
* 登陆ip
* @param _port
* 端口
* @param _username
* ssh用户
* @param _password
* ssh口令
* @param id
* 性能指标id
*/
public SSHGanymedUtil(String _hostname, int _port, String _username,
String _password, Long id) {
this(_hostname, _port, _username, _password);
this.taskId = id;
logger = Logger.getLogger("/util/SSHGanymedUtil/_" + id,
Logger.ALL, true);
}
/**
* 登陆SSH服务器
*
* @throws Exception
*/
public void login() throws Exception {
logger.infoT("start task id is " + taskId);
// 建立连接
connection = new Connection(hostname, port);
try {
// 连接上
connection.connect();
// 进行校验
boolean isAuthenticated = connection.authenticateWithPassword(
username, password);
logger.infoT("isAuthenticated = " + isAuthenticated);
if (isAuthenticated == false)
throw new IOException("Authentication failed.");
} catch (Exception e) {
logger.exception(e);
throw new Exception("UserOrPasswordError");
}
}
/**
* 发送shell命令并获取执行结果
*
* @param command
* 发送执行的命令
* @return 返回命令的执行结果
*/
public String execCommand(final String command) {
logger.infoT("start exexCommand");
final StringBuilder sb = new StringBuilder(256);
// 连接的通道
Session sess = null;
try {
// 创建session
sess = connection.openSession();
// 这句非常重要,开启远程的客户端
sess.requestPTY("vt100", 80, 24, 640, 480, null);
// 开启后睡眠4秒
Thread.sleep(4000);
// 开启终端
// sess.startShell();
// 执行命令
sess.execCommand(command);
// 起始时间,避免连通性陷入死循环
long start = System.currentTimeMillis();
// // 增加timeOut时间
// sess.waitForCondition(ChannelCondition.TIMEOUT, 5000);
InputStream stdout = new StreamGobbler(sess.getStdout());
BufferedReader br = new BufferedReader(
new InputStreamReader(stdout));
char[] arr = new char[512];
int read;
int i = 0;
while (true) {
// 将结果流中的数据读入字符数组
read = br.read(arr, 0, arr.length);
// 推延5秒就退出[针对连通性测试会陷入死循环]
if (read < 0 || (System.currentTimeMillis() - start) > 5000)
break;
// 将结果拼装进StringBuilder
sb.append(new String(arr, 0, read));
i++;
}
logger.infoT("ExitCode: " + sess.getExitStatus() + "i = " + i);
} catch (Throwable e) {
logger.exception(e);
} finally {
// 关闭通道
if (sess != null)
sess.close();
}
return sb.toString();
}
/**
* 关闭ssh连接
*/
public void closeConnection() {
logger.infoT("end task id is " + taskId + "disconnet");
if (connection != null)
connection.close();
}
public String getHostname() {
return hostname;
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
/**
* @param args
*/
public static void main(String[] args) {
char[] c = new char[3072];
for (int i = 0; i < c.length; i++) {
c[i] = 'a';
}
String s = new String(c, 0, 3072);
System.out.println(s);
}
}
下面谈下后续修改的3点体会作为FAQ吧。
- 对于Ganymed和Maverick采集相同机器得到返回的结果值小有差异,Ganymed在采集Soralis系统的机器时会多返回一行系统的提示信息,对于结果的处理要特殊处理。
- 我的采集包括连通性的测试,就是模拟输出ping命令,这点Maverick的监听关闭机制处理相对巧妙,不必额外增加代码处理,而这里ping命令会让stdout不停的输出(就像在本机ping一台机器不停的返回结果),形成死循环,所以我在代码中增加判断,如果读取结果的while(true)循环执行超过5秒,就break出循环,也可以解决ping命令的死循环问题。
- 对于Ganymed和Maverick两者特别要小心的是,有几个命令我用Maverick采集一点问题都没有,但是用Ganymed执行返回值却出错,显示为“syntax error The source line is 1.”等错误信息,意思好像是命令有错,但我把命令通过SecureCRT输进去测试却能返回值,这个也把我搞困惑了很久,后来我将能正确返回值的命令和这几个出错的命令进行比较,终于发现原因所在,原来正确的命令输入到终端后,都需要手动按回车键出现执行结果,而这些出错的命令都隐藏包含了“\r\n”回车换行符了,输入进去什么都不干就自动出现结果,所以将命令更换为无“\r\n”的就一切正常了。
总结: 对于软件包中examples中给出Basic.java在实际执行中可能需要加上sess.requestPTY("vt100", 80, 24, 640, 480, null);开启虚拟终端才能执行命令返回值,另外对于sess.getStdout()流数据的读取,例子给出了String line = br.readLine();但我们不能把读取的line直接append上去,那样得到结果就会成为一行,必须定义一个char数组char[] arr = new char[512],并采用read = br.read(arr, 0, arr.length); ,再sb.append(new String(arr, 0, read));,才能得到还原真实情况的结果。