Storm Component的多语言支持是通过父子线程实现通信,通信格式为JSON。
下面看一个小例子,父子进程都是Java语言:
// 父进程,语言为Java
public class ParentProcess {
// 子进程
Process subProcess;
public void run() throws IOException, InterruptedException {
// 运行子进程的命令,实际与直接在控制台下运行java SubProcess效果等同
String[] command = new String[] { "java", "SubProcess" };
// 通过ProcessBuilder构建子进程
ProcessBuilder builder = new ProcessBuilder(command);
subProcess = builder.start();
// 单独启动一个线程负责向子进程中写入数据,这里只写入一句问候语
// subProcess.getOutputStream()绑定于子进程的标准输入
Thread writeThread = new Thread( new Runnable() {
public void run() {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(subProcess.getOutputStream()));
try {
writer.write("hello son!");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
);
writeThread.start();
// 单独启动一个线程负责接收子进程中发送给父进程的数据
// subProcess.getInputStream()绑定于子进程的标准输出
Thread readThread = new Thread( new Runnable() {
public void run() {
BufferedReader reader = new BufferedReader(new InputStreamReader(subProcess.getInputStream()));
String line = null;
try {
while((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
);
readThread.start();
// TODO 实际中还需要启动一个线程负责读取子进程的错误流subProcess.getErrorStream(),否则由可能造成IO阻塞,在此忽略
// 等待父子进程完成通信交互
readThread.join();
writeThread.join();
// 销毁子进程
subProcess.destroy();
}
public static void main(String[] args) {
ParentProcess process = new ParentProcess();
try {
process.run();
}
catch (IOException e) {
e.printStackTrace();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 子进程,语言同样为Java
public class SubProcess {
public void run() throws IOException {
// 单独启动一个线程负责接收父进程发来的数据
Thread readThread = new Thread( new Runnable() {
public void run() {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line = null;
try {
while((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
);
readThread.start();
// 单独启动一个线程负责向父进程发送数据
Thread writeThread = new Thread( new Runnable() {
public void run() {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
try {
writer.write("hello parent!");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
);
writeThread.start();
}
public static void main(String[] args) {
SubProcess process = new SubProcess();
try {
process.run();
} catch (IOException e) {
e.printStackTrace();
}
}
}
编译两个Java文件,将ParentProcess.class与SubProcess.class放在同一个目录下,运行java ParentProcess,可以看到运行结果为:
hello son!
hell parent!
至此完成了一个简单的父子进程通信。
为了实现多语言,可以将父子进程的实现替换为任意支持标准IO的编程语言,shell, python, scala, clojure等等。。
下面来看看Storm支持多语言Component的实现。
// shell子进程的代理,通过java提供了一层封装
public class ShellProcess {
// 子进程的标准输入,标准输出和标准错误流
private DataOutputStream processIn;
private BufferedReader processOut;
private InputStream processErrorStream;
// Java标准进程对象,用于运行Shell脚本
private Process _subprocess;
// 具体要执行的Shell命令
private String[] command;
public ShellProcess(String[] command) {
this.command = command;
}
// 初始化Shell子进程。
public Number launch(Map conf, TopologyContext context) throws IOException {
ProcessBuilder builder = new ProcessBuilder(command);
// 设置子进程工作目录
builder.directory(new File(context.getCodeDir()));
_subprocess = builder.start();
// 接收子进程的标准IO流
processIn = new DataOutputStream(_subprocess.getOutputStream());
processOut = new BufferedReader(new InputStreamReader(_subprocess.getInputStream()));
processErrorStream = _subprocess.getErrorStream();
// 读取子进程的PID, 此逻辑在Shell脚本中实现
return (Number)readMessage().get("pid");
}
// 向子进程中写入对象,序列化为JSON格式数据
public void writeMessage(Object msg) throws IOException {
writeString(JSONValue.toJSONString(msg));
}
// 具体写入IO
private void writeString(String str) throws IOException {
byte[] strBytes = str.getBytes("UTF-8");
processIn.write(strBytes, 0, strBytes.length);
processIn.writeBytes("\nend\n");
processIn.flush();
}
// 从子进程中读取数据,将数据反序列化为JSONObject对象
public JSONObject readMessage() throws IOException {
String string = readString();
JSONObject msg = (JSONObject)JSONValue.parse(string);
if (msg != null) {
return msg;
} else {
throw new IOException("unable to parse: " + string);
}
}
// 清空子进程标准错误流,防止阻塞
public String getErrorsString() {
if(processErrorStream!=null) {
try {
return IOUtils.toString(processErrorStream);
} catch(IOException e) {
return "(Unable to capture error stream)";
}
} else {
return "";
}
}
// 具体业务方法,从子进程中读取数据,直到读入"end"标识结束
private String readString() throws IOException {
StringBuilder line = new StringBuilder();
synchronized (processOut) {
while (true) {
String subline = processOut.readLine();
if(subline==null) {
StringBuilder errorMessage = new StringBuilder();
errorMessage.append("Pipe to subprocess seems to be broken!");
if (line.length() == 0) {
errorMessage.append(" No output read.\n");
}
else {
errorMessage.append(" Currently read output: " + line.toString() + "\n");
}
errorMessage.append("Shell Process Exception:\n");
errorMessage.append(getErrorsString() + "\n");
throw new RuntimeException(errorMessage.toString());
}
if(subline.equals("end")) {
break;
}
if(line.length()!=0) {
line.append("\n");
}
line.append(subline);
}
}
return line.toString();
}
}
// 驱动Shell子进程的父进程,所有需要用Shell实现的Bolt组件需要继承此类
public class ShellBolt implements IBolt {
// 子进程代理对象
private ShellProcess _process;
// 子进程执行的命令
private String[] _command;
// 与子进程标准IO通信的线程
private Thread _readerThread;
private Thread _writerThread;
public void prepare(Map stormConf, TopologyContext context, final OutputCollector collector) {
// 生成子进程代理对象
_process = new ShellProcess(_command);
// 启动子线程
try {
Number subpid = _process.launch(stormConf, context);
} catch (IOException e) {
throw new RuntimeException("Error when launching multilang subprocess\n" + _process.getErrorsString(), e);
}
// 读取子进程数据,根据具体命令做相应上层业务处理
_readerThread = new Thread(new Runnable() {
public void run() {
while (_running) {
try {
JSONObject action = _process.readMessage();
if (action == null) {
// ignore sync
}
String command = (String) action.get("command");
if(command.equals("ack")) {
handleAck(action);
} else if (command.equals("fail")) {
handleFail(action);
} else if (command.equals("error")) {
handleError(action);
} else if (command.equals("log")) {
String msg = (String) action.get("msg");
LOG.info("Shell msg: " + msg);
} else if (command.equals("emit")) {
handleEmit(action);
}
} catch (InterruptedException e) {
} catch (Throwable t) {
die(t);
}
}
}
});
_readerThread.start();
// 向子进程写入数据
_writerThread = new Thread(new Runnable() {
public void run() {
while (_running) {
try {
Object write = _pendingWrites.poll(1, SECONDS);
if (write != null) {
_process.writeMessage(write);
}
} catch (InterruptedException e) {
} catch (Throwable t) {
die(t);
}
}
}
});
_writerThread.start();
}
}