两个命令行程序的交互——使用Java的Process类完成复杂控制台程序的自动化操作(以围棋GTP协议为例)

1701人浏览 / 6人评论

围棋GTP实例(以GNUGo程序为例)

GitHub地址链接:https://github.com/zhmgczh/GTPService

这个项目其实继承并发展于我的上一个项目——用模式识别实现围棋转接器的开源Java项目——一个类似GTool2.exe的工具,是一个变相的补充,主要解决如何使用Java完成两个控制台应用程序相互交流以达成交互运行目的的问题。

只要稍微学过编程的人都会对于命令行应用程序非常熟悉,它是我们接触编程之后首先需要掌握的基本程序类型(最经典的莫过于各种版本的HelloWorld程序)。现在有这样一个问题——如果有一个命令行程序,它需要另外一个命令行程序提供输入,并给另一个命令行程序提供输出,我们如何构建一个转接器完成这两个程序互相交互的复杂操作?

有人可能会说——把这两个程序写成网络程序使用http协议进行交互不就行了吗?事实上绝大多数情况是不行的,首先,如果这两个程序是自己写的,那我们为何不把他们写成一个程序呢?正因为两个程序来源不同、接口不同,所以我们才需要用控制台完成交互。很多情况下,我们是没有办法修改两个程序自身的代码,而只知道它们输入对应的输出。

我们首先需要给这个问题一个直观的背景,于是不得不说我为何想到了这个问题。最近我下载了一些新的高级围棋引擎,例如LeelaZero、PhoenixGo等,这些围棋引擎大多是没有图形界面而单单只靠命令行操作的。所幸的是,它们的操作都基于一套成熟的规则——围棋GTP协议。天祺围棋网站上有一篇文章专门介绍这个协议,点击查看

下面是以GNUGo为例的一个示例,以数字开头的行都是用户输入的行,程序会给出相应的输出。

> ./gnugo --mode gtp
1 boardsize 7
=1
 
2 clear_board
=2
 
3 play black D5
=3
 
4 genmove white
=4 C3
 
5 play black C3
?5 illegal move
 
6 play black E3
=6
 
7 showboard
=7
   A B C D E F G
 7 . . . . . . . 7
 6 . . . . . . . 6
 5 . . + X + . . 5
 4 . . . + . . . 4
 3 . . O . X . . 3
 2 . . . . . . . 2     WHITE (O) has captured 0 stones
 1 . . . . . . . 1     BLACK (X) has captured 0 stones
   A B C D E F G
 
8 quit
=8

整个交流过程清晰明了,略懂围棋的人都可以一目了然。

当然,基于GTP协议的围棋引擎不止GNU一种,基本上主流高水平围棋引擎都支持GTP协议。最关键的地方来了,如何使两个基于GTP协议的围棋引擎相互对弈呢?这就回到了上面刚刚说过的那个问题——让两个命令行程序交互。

如果是直接交互那问题可能简单很多,但是这样的场景显然有些复杂。现在有一个特殊任务,比如这里的围棋引擎输入“genmove black”后返回了一个围棋子坐标XX,我们需要在另一个引擎里输入“play black XX”才能完成转接操作,将一个围棋引擎的落子信息传递到另一个围棋引擎。这个工作不是将两个程序各自的输出作为对方的输入可以解决的,区别于ACM/ICPC算法竞赛中的交互题(算法竞赛中的交互题中,答案程序的输出直接作为测评程序的输入,反之亦然)。这个任务同样区别于输入输出重定向,因为我们不可能一次性获得一个程序的所有输出并一起输入到另一个程序中,每个程序的下一个输出都是和另一个程序的上一个输出都是有关系的。

综上所述,我们的任务是——制作一个用于两个程序交互的第三者程序,完成两个程序的交互转接工作。

这个程序应该服从什么样的架构呢?我一开始想到的架构如下图:

初始架构想法

有了这个思想就可以开始实现,Java的Process类和ProcessBuilder类给我们提供了方便的资源。用Process类启动一个程序,接下来的事情就需要获得这个程序的输入和输出,我们使用Process类的getInputStream和getOutputStream方法。需要注意的是,getInputStream和getOutputStream获取的分别为程序的输出流和输入流,因为其他程序的输出相对于转接器而言就是输入信号,输入则为输出信号。

实现了一个“理论上”可用的版本,跑的时候就开始各种阻塞,不因为别的,就因为我们的程序是用的同步时序逻辑。输入是没有任何问题的,但是输出有大问题。关键的问题在于我们不知道程序何时给出它的输出以及这个输出何时停止了,因为围棋引擎大多需要思考的时间,如果我们发现输出流中没有内容,可能是因为输出已经停止了,也可能是因为下一个输出还没有被给出。

我们需要继续监听还是停止监听走向下一步,就不能通过输出流的状态进行判断。我们知道,标准输出流和标准错误流如果不及时取出,会造成程序阻塞导致失去响应,所以我们不得不随时取出所有输出(无论有效的还是无效的),保证程序正常工作。可以分别开线程完成“取出”的操作,也可以将标准输出流和标准错误流合二为一(ProcessBuilder类有一个redirectErrorStream方法,将其参数设为true即可将标准错误流合并到标准输入流中)。

为了确保程序的高效性,我们也要将读取数据和处理数据分开变成两个线程。为了使两个程序高效精准地对接,我们要使用队列(Queue)模拟一个响应请求流作为接受请求和处理请求的中转站。修改后的架构如下图:

修改后的架构

这是一个多异步时序逻辑,目的是保证程序的高效运转,避免阻塞、死锁等现象出现。

我的程序就是依据这个逻辑实现了所有的代码。这个GTPService项目的功能有:两个命令行程序互相下棋并在界面上显示实时棋盘(有保存棋谱功能)、一个命令行程序和一个GUI界面的程序对弈(在GoAutomation项目的基础上稍作修改实现)。目前为止,已经被实现的功能有以下几类——命令行 vs 命令行、命令行 vs GUI、GUI vs GUI(前两个是在本项目中实现的,最后一个是在GoAutomation项目中实现的)。

本项目使用了面向对象的方法,设计了一个抽象类GTP,泛指所有支持围棋GTP协议的引擎,每个类只需要重载抽象方法init(int size)填写引擎的初始化参数(参照ProcessBuilder类的使用方法),将一个String类List参数传给父类方法init(List<String>list,int size)即可。

命令行围棋引擎与另一个命令行围棋引擎对弈效果图(对弈图形界面上鼠标不可点击):

命令行vs命令行效果图

全部评论

2022-11-12 00:35
我也在学习这个围棋引擎通信问题,苦恼好久了。刚开始学java,试了试leelagtp,发现不太会用。