:https://segmentfault.com/blog...java
前阵子休息天平常在寻找项目里很差的代码,看到了这样的一段代码:shell
private Result sshSameExec(Session session, String cmd) { if (log.isDebugEnabled()) { log.debug("shell command: {}", cmd); } UserInfo ui = getUserInfo(); session.setUserInfo(ui); int exitStatus = 0; StringBuilder builder = new StringBuilder(); ChannelExec channel; InputStream in; InputStream err; try { session.connect(connectTimeout); channel = (ChannelExec) session.openChannel("exec"); channel.setCommand(cmd); in = channel.getInputStream(); err = channel.getErrStream(); channel.connect(); } catch (Exception e) { throw new CloudRuntimeException(e); } try { long lastRead = Long.MAX_VALUE; byte[] tmp = new byte[1024]; while (true) { while (in.available() > 0 || err.available() > 0) { int i = 0; if (in.available() > 0) { i = in.read(tmp, 0, 1024); } else if (err.available() > 0) { i = err.read(tmp, 0, 1024); } if (i < 0) { break; } lastRead = System.currentTimeMillis(); builder.append(new String(tmp, 0, i)); } if (channel.isClosed()) { if (in.available() > 0) { continue; } exitStatus = channel.getExitStatus(); break; } if (System.currentTimeMillis() - lastRead > exeTimeout) { break; } } } catch (IOException e) { throw new CloudRuntimeException(e); } finally { channel.disconnect(); session.disconnect(); } if (0 != exitStatus) { return Result.createByError(ErrorData.builder() .errorCode(ResultCode.EXECUTE_SSH_FAIL.getCode()) .detail(builder.toString()) .title(ResultCode.EXECUTE_SSH_FAIL.toString()) .build()); } else { return Result.createBySuccess(builder.toString()); } }
简单解释一下这段代码——即经过ssh到一台机器上,而后执行一些命令.对命令输出的东西,开了一个循环,每一次读必定的位置,而后以字节流的形式读回来.segmentfault
这段代码有点丑,因而我闻到了学习的味道.缓存
首先是对两个Stream的消费,很显然,在多核环境下,咱们同时也只可以消费其中一个Stream.其次,这代码太挫了,本身定义一个tmp,而后102四、1024这样的去取出来.bash
在改良以前,咱们先来回顾一下JavaIO的接口定义.网络
这里有同窗可能问了,为啥叫它低抽象接口呢?由于它离底层太近了,计算机原本就是处理二进制的,而这两个接口正是用来处理二进制数据流的.session
先简单看一眼这两个接口:app
** * This abstract class is the superclass of all classes representing * an input stream of bytes. * * <p> Applications that need to define a subclass of <code>InputStream</code> * must always provide a method that returns the next byte of input. * * @author Arthur van Hoff * @see java.io.BufferedInputStream * @see java.io.ByteArrayInputStream * @see java.io.DataInputStream * @see java.io.FilterInputStream * @see java.io.InputStream#read() * @see java.io.OutputStream * @see java.io.PushbackInputStream * @since JDK1.0 */ public abstract class InputStream implements Closeable {.....}
/** * This abstract class is the superclass of all classes representing * an output stream of bytes. An output stream accepts output bytes * and sends them to some sink. * <p> * Applications that need to define a subclass of * <code>OutputStream</code> must always provide at least a method * that writes one byte of output. * * @author Arthur van Hoff * @see java.io.BufferedOutputStream * @see java.io.ByteArrayOutputStream * @see java.io.DataOutputStream * @see java.io.FilterOutputStream * @see java.io.InputStream * @see java.io.OutputStream#write(int) * @since JDK1.0 */ public abstract class OutputStream implements Closeable, Flushable {...}
咱们能够发现,它们都实现了Closeable的接口.所以你们在使用这些原生类时,要注意在结束时调用Close方法哦.ssh
这两个接口的经常使用实现类有:
- FileInputStream
和FileOutputStream
async
DataInputStream
和DataOutputStream
ObjectInputStream
和ObjectOutputStream
为啥说它是高级抽象接口呢?咱们先来看看它们的注释:
/** * Abstract class for writing to character streams. The only methods that a * subclass must implement are write(char[], int, int), flush(), and close(). * Most subclasses, however, will override some of the methods defined here in * order to provide higher efficiency, additional functionality, or both. * * @see Writer * @see BufferedWriter * @see CharArrayWriter * @see FilterWriter * @see OutputStreamWriter * @see FileWriter * @see PipedWriter * @see PrintWriter * @see StringWriter * @see Reader * * @author Mark Reinhold * @since JDK1.1 */ public abstract class Writer implements Appendable, Closeable, Flushable {
/** * Abstract class for reading character streams. The only methods that a * subclass must implement are read(char[], int, int) and close(). Most * subclasses, however, will override some of the methods defined here in order * to provide higher efficiency, additional functionality, or both. * * * @see BufferedReader * @see LineNumberReader * @see CharArrayReader * @see InputStreamReader * @see FileReader * @see FilterReader * @see PushbackReader * @see PipedReader * @see StringReader * @see Writer * * @author Mark Reinhold * @since JDK1.1 */ public abstract class Reader implements Readable, Closeable {
咱们能够看到,这个抽象类是用来面向character
的,也就是字符.字符的抽象等级必然比字节高,由于字符靠近上层,即人类.
若是咱们直接使用上述实现类去打开一个文件(如FileWriter
、FileReader
、FileInputStream
、FileOutputStream
),对其对象调用read
、write
、readLine
等,每一个请求都是由基础OS直接处理的,这会使一个程序效率低得多——由于它们都会引起磁盘访问or网络请求等.
为了减小这种开销,Java 平台实现缓冲 I/O 流。缓冲输入流从被称为缓冲区(buffer)的存储器区域读出数据;仅当缓冲区是空时,本地输入 API 才被调用。一样,缓冲输出流,将数据写入到缓存区,只有当缓冲区已满才调用本机输出 API。
用于包装非缓存流的缓冲流类有4个:BufferedInputStream
和BufferedOutputStream·用于建立字节缓冲字节流,
BufferedReader和
BufferedWriter`用于建立字符缓冲字节流.
以前,咱们提到了这段代码写得搓的地方:
故此,咱们能够考虑对每一个Stream都进行包装,支持用线程去消费,其次咱们能够用高级抽象分接口去适配Byte,而后去装饰成Buffer.
接下来,咱们来看一段ZStack里的工具类ShellUtils
,为了节省篇幅,咱们仅仅截出它在IDE里的
概览:
run方法的核心:
public ShellResult run() { StopWatch watch = new StopWatch(); watch.start(); try { if (withSudo) { command = String.format("sudo %s", command); } ProcessBuilder pb = new ProcessBuilder(Arrays.asList("/bin/bash", "-c", command)); if (baseDir == null) { baseDir = System.getProperty("user.home"); } pb.directory(new File(baseDir)); process = pb.start(); if (!suppressTraceLog && logger.isTraceEnabled()) { logger.debug(String.format("exec shell command[%s]", command)); } Writer stdout; int stdoutLog = stdoutLogStrategy(); if (stdoutLog == LOG_TO_FILE) { stdout = new BufferedWriter(new FileWriter(stdoutFile)); } else if (stdoutLog == LOG_TO_SCREEN) { stdout = new BufferedWriter(new OutputStreamWriter(System.out)); } else { stdout = new StringWriter(); } Writer stderr; int stderrLog = stderrLogStrategy(); if (stderrLog == LOG_TO_FILE) { stderr = new BufferedWriter(new FileWriter(stderrFile)); } else if (stderrLog == LOG_TO_SCREEN) { stderr = new BufferedWriter(new OutputStreamWriter(System.err)); } else { stderr = new StringWriter(); } StreamConsumer stdoutConsumer = new StreamConsumer(process.getInputStream(), new PrintWriter(stdout, true), stdoutLog != LOG_TO_FILE); StreamConsumer stderrConsumer = new StreamConsumer(process.getErrorStream(), new PrintWriter(stderr, true), stderrLog != LOG_TO_FILE); stderrConsumer.start(); stdoutConsumer.start(); process.waitFor(); stderrConsumer.join(TimeUnit.SECONDS.toMillis(30)); stdoutConsumer.join(TimeUnit.SECONDS.toMillis(30)); ShellResult ret = new ShellResult(); ret.setCommand(command); ret.setRetCode(process.exitValue()); if (stderrLog == LOG_TO_STRING) { ret.setStderr(stderr.toString()); } else if (stderrLog == LOG_TO_FILE) { stderr.close(); } if (stdoutLog == LOG_TO_STRING) { ret.setStdout(stdout.toString()); } else if (stdoutLog == LOG_TO_FILE) { stdout.close(); } return ret; } catch (Exception e) { StringBuilder sb = new StringBuilder(); sb.append("Shell command failed:\n"); sb.append(command); throw new ShellException(sb.toString(), e); } finally { if (process != null) { process.destroy(); } watch.stop(); if (!suppressTraceLog && logger.isTraceEnabled()) { logger.trace(String.format("shell command[%s] costs %sms to finish", command, watch.getTime())); } } } }
咱们能够看到StreamConsumer
这个类,咱们来看一下它的代码:
private static class StreamConsumer extends Thread { final InputStream in; final PrintWriter out; final boolean flush; StreamConsumer(InputStream in, PrintWriter out, boolean flushEveryWrite) { this.in = in; this.out = out; flush = flushEveryWrite; } @Override public void run() { BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(in)); String line; while ( (line = br.readLine()) != null) { out.println(line); if (flush) { out.flush(); } } } catch (Exception e) { logger.warn(e.getMessage(), e); } finally { try { if (br != null) { br.close(); } } catch (IOException e) { logger.warn(e.getMessage(), e); } } } }
这段代码已经达到了咱们的理想状态:线程消费,高级抽象.
闲话很少说,先贴代码为敬:
import java.io.InputStream import java.io.InputStreamReader class StreamGobbler(private val inputStream: InputStream, private var result: StringBuilder) : Runnable { override fun run() { InputStreamReader(inputStream).buffered().use { it.lines().forEach { r -> result.append(r) } } } }
仍是同样熟悉的配方,咱们逐行来解读:
InputStreamReader
,这意味着它能够输出字符流了,而后咱们使用了Kotlin的接口将其装饰成了Buffer.先看一下上面的图,咱们都知道内核态线程是由OS调度的,但当一个线程拿到时间片时,却调到了阻塞IO,那么只能等在那边,浪费时间.
而协程则能够解决这个问题,当一个Job
hang住的时候,能够去作别的事情,绕开阻塞.更好的利用时间片.
最后,咱们来看一下成品代码:
override fun sshExecWithCoroutine(session: Session, cmd: String): SimpleResult<out String> { val ui = InnerUserInfo() session.userInfo = ui val exitStatus: Int var channel = ChannelExec() val inputBuilder = StringBuilder() val errorBuilder = StringBuilder() try { session.connect(connectTimeout) channel = session.openChannel("exec") as ChannelExec channel.setCommand(cmd) channel.connect() val inputStream = StreamGobbler(channel.inputStream, inputBuilder) val errStream = StreamGobbler(channel.errStream, errorBuilder) val customJob = GlobalScope.launch { customStream(inputStream, errStream) } while (!customJob.isCompleted) { // wait job be done } exitStatus = channel.exitStatus } catch (e: IOException) { throw java.lang.RuntimeException(e) } finally { if (channel.isConnected) { channel.disconnect() } if (session.isConnected) { session.disconnect() } } return if (0 != exitStatus) { return SimpleResult.createByError(ErrorData.Builder() .errorCode(ResultCode.EXECUTE_SSH_FAIL.value) .detail(errorBuilder.toString()) .title(ResultCode.EXECUTE_SSH_FAIL.toString()) .build()) } else { SimpleResult.createBySuccess(inputBuilder.toString()) } } private suspend fun customStream(inputStream: StreamGobbler, errorStream: StreamGobbler) { val inputDeferred = GlobalScope.async { inputStream.run() } val errorDeferred = GlobalScope.async { errorStream.run() } inputDeferred.join() errorDeferred.join() }