流
在java程序中与外部交互需要对io进行管理,通常是对输入输出流的管理。比如java中使用process类操作子进程,返回一个process对象
windows运行一个dir命令来获取当前的目录列表
Process process = Runtime.getRuntime().exec("cmd /c dir");
//获取普通输入流
process.getInputStream()
//获取异常输入流
process.getErrorStream()
//获取输出流
process.getOutputStream()
如果是执行一个命令直接获取一个结果,那么直接把普通输入流和异常输入流的数据的数据完全读取出来即可,但是大部分情况需要和输入输出流进行交互,比如下面的情况。
- 通过scp传输一个文件,需要实时获取传输的进度,所以需要实时获取到输入流的数据,不能等它执行完成采取获取流的结果
- 通过伪终端进行ssh登录操作,需要实时获取到输入流的信息来操作输出流
独立的线程
要和流进行交互就要使用独立的线程,让独立的线程去检测输入流是否有数据,如果有数据则写到缓冲区,那怎么判断输入流输出了我想要的数据了呢?很简单,注册一个监听器,监听器的内容是当数据符合我的预期则通知我,每当新数据来了则走一遍监听器,这样我就能实时知道下一步应该怎么做。当然,监听器可以是一组,根据需要来定制。详细代码如下
基本参数
//普通流
private final InputStream inputStream;
//输出流
private final OutputStream outputStream;
//异常输入流
private final InputStream errInputStream;
//存放临时数据
private final StringBuffer stringBuffer = new StringBuffer();
//存放观察者的容器,当有数据准备好了,则会通知所有的观察者
private final ExchangeObserver exchangeObserver;
//不断循环接收输入流的独立线程
private final Thread mainThread;
注册监听器
ExchangeObserver exchangeObserver = streamExchange.getExchangeObserver();
//监听器
FinishOneExchangeListener listener = new FinishOneExchangeListener();
//注册监听器
exchangeObserver.register(loginSuccessListener);
独立线程的运行代码
public void run() {
if (inputStream == null) {
log.error("输入流为空");
return;
}
try {
byte []buf = new byte[1024*1024];
int n = 0;
while (true) {
if (!running.get()) return;
if (mainThread.isInterrupted()) return;
StringBuilder stringBuilder = new StringBuilder();
//实践发现,同一行数据在传输过程中可能是断断续续的,available的数量为0并不代表后面没有数据了
//所以为了保证输出到观察者的数据是完整的一行需要额外等那么一会,做法就是设置一个状态标识上一次有没有数据,没有数据
//就等待一会儿,大概十毫秒,再进入下一个循环周期,直到超过最大空转次数,如果此时还是没有数据,
// 那应该是没有了,数据不会断这么长。
int cycleIndex = 0;
int maxCycleTime = 2;
boolean hasDataStatus = false;
while (true) {
if (mainThread.isInterrupted()) {
return;
}
//读取有效数据
if (inputStream.available() > 0) {
n = inputStream.read(buf, 0, inputStream.available());
} else if (errInputStream != null && errInputStream.available() > 0) {
n = errInputStream.read(buf, 0, errInputStream.available());
}
if (n > 0) {
char[] newChars;
//将收到的字节数组使用对应的编码转换成字符数组
newChars = ByteCharTransferUtil.byteToChar(Arrays.copyOf(buf, n), charset);
stringBuilder.append(newChars);
hasDataStatus = true;
cycleIndex = 0;
n = 0;
continue;
}
//执行空转
if (cycleIndex < maxCycleTime) {
Thread.sleep(10);
cycleIndex++;
} else {
break;
}
}
//如果没有数据则休眠0.1s
if (!hasDataStatus) {
Thread.sleep(100);
} else {
String line = stringBuilder.toString();
line = line.replace("\r", "");
if (maxTempPrintLength > 0 ) {
synchronized (lock) {
if (stringBuffer.length() >= maxTempPrintLength) {
stringBuffer.delete(0, Math.min(line.length(), stringBuffer.length()));
}
stringBuffer.append(line);
}
}
//触发通知所有观察者
exchangeObserver.onNotify(line);
}
}
} catch (IOException e) {
exchangeObserver.onError(e);
running.set(false);
if (mainThread.isInterrupted()) {
return;
}
log.error("io 异常退出:{},{},{}" , key, e.getClass().getSimpleName(), e.getMessage());
} catch (InterruptedException e) {
log.debug("io 正常退出:{}" , key);
}
catch (Exception e) {
log.error("io循环异常退出", e);
}
}
利用流管理器进行SSH跳板登录
流程描述如下,首先通过伪终端连上跳板机,并且获取到输入输出流,使用流管理对象进行管理。流程图如下
代码描述如下
FinishOneExchangeListener的说明:
FinishOneExchangeListener的作用是对数据流进行一次监听,主流程调用FinishOneExchangeListener的wait方法时会等待,当被触发时内部封装的CountDownLatch countDownLatch会被触发,这样主流程就可以继续往下走,这样可以达到一个等待通知的效果。
核心代码如下:
public void onNotify(String str) {
if (predicate == null) {
return;
}
//判断数据是否符合预期
if (predicate.test(str)) {
CountDownLatch countDownLatch = this.countDownLatch;
countDownLatch.countDown();
}
}
详细登录流程
public static boolean login(HostConnectionNode hostConnectionNode,
StreamExchange streamExchange, int level) throws InterruptedException, TimeoutException, IOException {
ExchangeObserver exchangeObserver = streamExchange.getExchangeObserver();
FinishOneExchangeListener loginSuccessListener = new FinishOneExchangeListener();
FinishOneExchangeListener fingerListener = new FinishOneExchangeListener();
FinishOneExchangeListener passwordWaitInputListener = new FinishOneExchangeListener();
FinishOneExchangeListener passwordExpireListener = new FinishOneExchangeListener();
FinishOneExchangeListener passwordErrorListener = new FinishOneExchangeListener();
loginSuccessListener.refreshPattern("Last login");
fingerListener.refreshPattern(Pattern.compile("fingerprint", Pattern.DOTALL) );
passwordWaitInputListener.refreshPattern("[p|P]assword:");
passwordExpireListener.refreshPattern("Your password has expired");
passwordErrorListener.refreshPattern("Permission denied");
exchangeObserver.register(loginSuccessListener);
exchangeObserver.register(fingerListener);
exchangeObserver.register(passwordWaitInputListener);
exchangeObserver.register(passwordExpireListener);
exchangeObserver.register(passwordErrorListener);
try {
streamExchange.write(hostConnectionNode.toString());
if (loginSuccessListener.hasFinish()) {
return true;
}
if (hostConnectionNode.getKeyLocation() == null) {
int retryTime = 10;
while (retryTime -- > 0) {
try {
passwordWaitInputListener.await(1, TimeUnit.SECONDS);
break;
} catch (TimeoutException timeoutException) {
if (fingerListener.hasFinish() && level < 3) {
streamExchange.reset();
streamExchange.write("yes\n");
return login(hostConnectionNode, streamExchange, level+1);
}
if (passwordExpireListener.hasFinish()) {
throw new ConnectionException("密码已过期,节点[%s]".formatted(hostConnectionNode.toString()));
}
}
}
if (retryTime <= 0) {
throw new ConnectionException("登录节点[%s]失败,错误信息:%s".formatted(hostConnectionNode.toString(), streamExchange.getPrintContent()));
}
passwordWaitInputListener.reset();
streamExchange.reset();
streamExchange.write(hostConnectionNode.getPassword());
}
loginSuccessListener.await(15, TimeUnit.SECONDS);
if (passwordExpireListener.hasFinish()) {
throw new ConnectionException("密码已过期,节点[%s]".formatted(hostConnectionNode.toString()));
}
return true;
} catch (TimeoutException e) {
if (passwordExpireListener.hasFinish()) {
throw new ConnectionException("密码已过期,节点[%s]".formatted(hostConnectionNode.toString()));
} else if (fingerListener.hasFinish()) {
streamExchange.reset();
streamExchange.write("yes\n");
return login(hostConnectionNode, streamExchange, level+1);
} else if (passwordErrorListener.hasFinish()) {
throw new ConnectionException("密码错误,节点[%s]".formatted(hostConnectionNode.toString()));
}
else if (passwordWaitInputListener.hasFinish()) {
return login(hostConnectionNode, streamExchange, level+1);
} else {
throw new ConnectionException("跳板登录超时,节点[%s]".formatted(hostConnectionNode.toString()));
}
}
finally {
exchangeObserver.remove(loginSuccessListener);
exchangeObserver.remove(fingerListener);
exchangeObserver.remove(passwordWaitInputListener);
exchangeObserver.remove(passwordExpireListener);
exchangeObserver.remove(passwordErrorListener);
}
}
以上代码都可以在项目shixinmuhuo/PowerExec: PowerExec是一个超强的支持无限跳板的远程执行脚本的工具。致力于解决重复繁琐的手工运维问题。 (github.com)找到,如果对您有帮助就给我点个赞吧。