前言
继上一篇 Android 调用 Termux 执行命令,执行命令的问题基本解决,但是 bash
、awk
、clangd
这类命令可以从标准输入读取信息并维持运行,Termux 第三方调用缺乏有效支持。而 RunCommandService
可以允许命令后台运行,然后我们以某种方式取得该后台程序的标准输入/输出,便可以实现前后端的持续通信。
一、Android 进程间通信(IPC)
进程间通信(In-Process Communication, IPCreliefmsgustrend)主要实现多进程间的数据通信问题。比如一个 UI 程序后台调用一个 CLI 程序执行某功能,每个程序会启动一个进程,UI 进程给 CLI 进程发送输入数据,CLI 进程接收后返回处理结果给 UI 进程。
Android 实现跨进程通信有多种方式,其各有优缺点:
文件:直接在设备上创建文件实现数据通信。原理简单,操作方便,但是效率低。
Binder:结构上 Binder 是一个虚拟的设备驱动(/dev/binder),连接 Service 进程、Client 进程和 Service Manager 进程。其数据只在内核空间与用户空间复制一次,效率较高,但是限制 1M 数据。另外,基于 Binder 的方法有 AIDL、ContentProvider、Messenger 等。
SharedMemory:共享内存在 Android SDK 27 引入,允许开辟一块共享内存空间用于进程间的数据交互。SharedMemory 配合 AIDL/Binder 使用,可以破除 1M 的限制,传输大文件。但是注意直接对内存进行操作,使用完毕需要手动销毁。
Unix Domain Socket:又叫 Local Domain Socket,本地套字节是 Linux 内核提供的功能,数据经过内核,实现本地进程间通信。其效率高,但是 Android 9+ 限制用户 App 间使用 UDS 通信。
Socket:套字节本质上是网络通信,采用 TCP 或 UDP 协议,主要用于网络通信。其中 TCP 协议较复杂,用于建立稳定的通信;UDP 则速度快而不安全。
受安卓不同应用之间的权限限制,支持进程间字节数据 IO 通信的方案较少,本次采用 Socket 实现。
二、Netcat 网络瑞士军刀
Netcat 是一个小巧强大的网络工具,用于网络监听测试等。Netcat 可建立网络通信,支持 TCP/UDP/Unix 协议。在 Termux 端使用 Netcat 建立套字节通信,并将 stdin/stdout 重定向到一个子进程,如此实现通信。
通过以下方式建立 TCP 通信。服务器端:
nc -l -s 127.0.0.1 -p 1234
客户端:
nc 127.0.0.1 1234
此时在客户端输入的内容可传至服务器端,而服务器端输入的内容可传回客户端,二者间实现通信。
另外,使用 Netcat 可以方便反弹 shell 程序。下面是服务器端命令:
nc -l -s 127.0.0.1 -p 1234 bash
用客户端登录到 127.0.0.1:1234,建立连接后,服务端会启动 bash
程序,并接收来自客户端的标准输入,将标准输出发送给客户端。
三、第三方 App 与 Termux 建立 TCP/Socket 通信
通过 RunCommandService
调用 Termux 执行 nc
命令反弹某个程序,然后通过 java.net.Socket
建立 Socket 连接,取得 Socket 的 IO 流,即可实现进程间通信。
调用 Termux。注意,Termux 可使用两个版本的 Netcat:安卓自带的 /system/bin/nc
和 Termux 仓库的 netcat-openbsd
。前者随 ToyBox 在 Android Marshmallow 被引入,支持反弹 shell,而后者不支持;后者支持抽象命名空间 UDS。所以我们使用 /system/bin/nc
。
intent.setClassName("com.termux", "com.termux.app.RunCommandService"); intent.setAction("com.termux.RUN_COMMAND"); intent.putExtra("com.termux.RUN_COMMAND_PATH", "/system/bin/nc"); intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"-l", "-s", "127.0.0.1", "-p", "1234", "bash"}); intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home"); intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", true); intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", "0"); startService(intent);
建立 Socket 连接:
Socket mSocket; InputStream mInput; OutputStream mOutput; public void connect() { mSocket = new Socket(); new Thread(){ public run() { try { sk.connect(new InetSocketAddress("127.0.0.1", 1234)); mInput = sk.getInputStream(); // 写入命令/发出数据 mOutput = sk.getOutputStream(); mOutput.write("ls\n"); mOutput.flush(); // 读取结果 Thread.sleep(200L); int l = mInput.avaliable(); byte[] bs = new byte[l]; mInput.read(bs); System.out.println(new String(bs)); } catch (Exception e) { e.printStackTrace(); } } }.start(); }
四、应用:调用 LSP 语言服务器
语言服务器协议(Language Server Protocol, LSP)是微软推出的一个基于 JSONRPC 的数据协议,用于
Termux 的软件仓库里正好有 clangd
和 ccls
两个 C/C++ 的 LSP 服务器端。笔者测试 ccls
时遇到 BUG,故选用 clangd
测试。
安装 clangd
:
apt install clangd
用 nc
反弹 clangd
:
/system/bin/nc -l -s 127.0.0.1 -p 48455 clangd
Android 客户端建立 Socket IO 通信:
Socket sk = new Socket(new InetAddress("127.0.0.1", 48455));
注意,安卓中 Socket 的 IO 流不允许在 UI 主线程进行操作,需要另起线程,以免阻塞主线程引起卡顿。
读取线程:
new Thread() { public void run() { InputStream mIn = sk.getInputStream(); final int L = 1024; byte[] buf = new byte[L]; while (mIn.read(buf, 0, 16)!=-1) { if (new String(buf, 0, 16).equals("Content-Length: ")) { // read int c // skip \r\n\r\n // read c bytes } } } }.start();
写入线程:
Thread td = new Thread() { public void run() { try { OutputStream mOut = sk.getOutputStream(); byte[] s="{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"initialize\",\"params\":{}}".getBytes(StandardCharsets.UTF_8); mOut.write(("Content-Length: "+s.length+"\r\n\r\n").getBytes()); mOut.write(s); mOut.flush(); } catch (IOException ioe) { ioe.printStackTrace(); } } }; td.start(); td.join(); // 阻塞写入线程,避免同时写入
笔者的开源项目:TermuC - github.com/RainbowC0