我的博客第一篇讲的就是用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吧。
  1.    对于Ganymed和Maverick采集相同机器得到返回的结果值小有差异,Ganymed在采集Soralis系统的机器时会多返回一行系统的提示信息,对于结果的处理要特殊处理。
  2.    我的采集包括连通性的测试,就是模拟输出ping命令,这点Maverick的监听关闭机制处理相对巧妙,不必额外增加代码处理,而这里ping命令会让stdout不停的输出(就像在本机ping一台机器不停的返回结果),形成死循环,所以我在代码中增加判断,如果读取结果的while(true)循环执行超过5秒,就break出循环,也可以解决ping命令的死循环问题。
  3.    对于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));,才能得到还原真实情况的结果。

03-19 01:20