其他语言
通过 HTTP 或原始套接字(每条命令一行 JSON),在 Go、Java、C#、PHP 或任何其他语言中使用 Droidline,附带经过测试的示例。
本页为译文。如与英文版不一致,以英文版为准。 English
并不是每种语言都有 SDK,你也不需要 SDK。服务器接受两种发送命令的方式,任何语言都能使用:
- HTTP:每条命令一个请求。这是最简单的选择;所有端点见 HTTP 指南。
- 原始套接字:打开一个到
127.0.0.1:8780的 TCP 连接,每行写入一个 JSON 对象,再逐行读回 JSON 对象。它更适合长时间运行的程序,也是接收通知等事件的唯一方式。
本页的每个示例都在 droidline-fakephone 上运行过。每个示例都会打开演示应用,尝试点击一个不存在的元素来演示错误处理,并读取一个值。
通过 HTTP
// Java 11 或更高版本。运行方式:java Droid.java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Droid {
static final HttpClient http = HttpClient.newHttpClient();
// 在唯一在线的手机上运行一条命令,并以文本形式返回 JSON 回复。
static String run(String command, String jsonBody) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create("http://localhost:8780/devices/_/" + command))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
return res.statusCode() + " " + res.body();
}
public static void main(String[] args) throws Exception {
System.out.println(run("launch", "{\"package\":\"dev.droidline.demo\"}"));
System.out.println(run("input", "{\"by\":\"id\",\"value\":\"email\",\"text\":\"knife\"}"));
System.out.println(run("touch", "{\"by\":\"text\",\"value\":\"Nope\",\"timeout\":1}"));
}
}// .NET 6 或更高版本。运行方式:dotnet run droid.cs(.NET 10),或粘贴到 Program.cs 中
using System.Text;
using System.Text.Json.Nodes;
var http = new HttpClient { BaseAddress = new Uri("http://localhost:8780/") };
// 在唯一在线的手机上运行一条命令,并返回解析后的回复。
async Task<JsonNode> Run(string command, JsonObject body)
{
var content = new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json");
var res = await http.PostAsync($"devices/_/{command}", content);
return JsonNode.Parse(await res.Content.ReadAsStringAsync())!;
}
Console.WriteLine(await Run("launch", new() { ["package"] = "dev.droidline.demo" }));
var r = await Run("touch", new() { ["by"] = "text", ["value"] = "Nope", ["timeout"] = 1 });
if (!(bool)r["ok"]!)
Console.WriteLine($"{r["error"]}: {r["msg"]}");
var title = await Run("get_text", new() { ["by"] = "text", ["value"] = "Sign in" });
Console.WriteLine(title["value"]);<?php
// 在唯一在线的手机上运行一条命令,并返回解码后的回复。
function droid(string $command, array $params = []): array {
$context = stream_context_create(["http" => [
"method" => "POST",
"header" => "Content-Type: application/json",
"content" => json_encode((object) $params),
"ignore_errors" => true, // 保留 4xx 和 5xx 回复的 JSON 正文
]]);
$body = file_get_contents("http://localhost:8780/devices/_/$command", false, $context);
return json_decode($body, true);
}
print_r(droid("launch", ["package" => "dev.droidline.demo"]));
$r = droid("touch", ["by" => "text", "value" => "Nope", "timeout" => 1]);
if (!$r["ok"]) {
echo $r["error"], ": ", $r["msg"], "\n";
}
echo droid("current")["package"], "\n";这些示例的共同点,也就是在其他任何语言中都要照搬的部分:
- 向
http://localhost:8780/devices/_/<command>发送POST请求,或者把_换成手机的名称。 - 发送请求头
Content-Type: application/json,以及一个包含命令参数的 JSON 对象。 - 读取回复中的
ok。如果它为 false,error中是错误码,msg中是一条可读的消息;此时 HTTP 状态码是 4xx 或 5xx,所以要确保你的 HTTP 库仍然把响应体交给你。
通过原始套接字
套接字协议是纯文本:每个请求和每个回复都是单独一行的 JSON 对象,以换行符结尾。你为每个请求选定一个 id,回复会把它带回来,因此你可以连续发送多个请求,不必等待:
→ {"id":1,"cmd":"launch","package":"dev.droidline.demo"}
← {"id":1,"ok":true,"ms":612}
→ {"id":2,"cmd":"touch","by":"text","value":"Nope","timeout":1}
← {"id":2,"ok":false,"error":"NOT_FOUND","msg":"Could not find text 'Nope' within 1s. ...","retryable":true}
package main
import (
"bufio"
"encoding/json"
"fmt"
"net"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8780")
if err != nil {
panic(err) // droidline serve 在运行吗?
}
defer conn.Close()
in := bufio.NewReader(conn)
// send 写入一行请求,再读取一行回复。
send := func(req map[string]any) map[string]any {
line, _ := json.Marshal(req)
conn.Write(append(line, '\n'))
reply, err := in.ReadBytes('\n')
if err != nil {
panic(err)
}
var out map[string]any
json.Unmarshal(reply, &out)
return out
}
fmt.Println(send(map[string]any{"id": 1, "cmd": "launch", "package": "dev.droidline.demo"}))
r := send(map[string]any{"id": 2, "cmd": "touch", "by": "text", "value": "Nope", "timeout": 1})
if r["ok"] != true {
fmt.Println("failed:", r["error"], r["msg"])
}
fmt.Println(send(map[string]any{"id": 3, "cmd": "current"})["package"])
}# SDK 会替你完成这些;下面是底层实际发生的事。
import json, socket
s = socket.create_connection(("127.0.0.1", 8780))
f = s.makefile("rw", encoding="utf-8")
def send(req):
f.write(json.dumps(req) + "\n")
f.flush()
return json.loads(f.readline())
print(send({"id": 1, "cmd": "launch", "package": "dev.droidline.demo"}))
print(send({"id": 2, "cmd": "touch", "by": "text", "value": "Nope", "timeout": 1}))上面的示例在每个请求之后读取一个回复,这是最简单的模式。完整规则如下:
- 请求字段是命令的参数,加上
id、cmd,以及可选的device(手机的名称或 ID)。 - 发给同一部手机的命令,回复按顺序返回。不同手机的回复可能以任意顺序到达,所以要按
id匹配。 - 发送
{"id":9,"cmd":"subscribe","events":["notification"]}之后,以{"event":...}开头的行会夹在回复之间到达。读取代码应该根据event字段区分这两种行。 - 同一端口上的
GET /ws通过 WebSocket 传输同样的行,适合那些用 WebSocket 比用 TCP 更方便的语言。
协议说明了每个字段、各种事件,以及会切断网络的命令如何报告最终结果。
如何选择
| HTTP | 原始套接字 | |
|---|---|---|
| 工作量 | 最少:任何 HTTP 库都行 | 稍多:要保持连接并逐行读取 |
| 速度 | 每条命令一个连接 | 所有命令共用一个连接 |
| 同时执行多条命令 | 并行发出多个请求 | 在一个连接上流水线发送 |
| 事件(通知、设备离线) | 不支持 | 支持,需先 subscribe |
先从 HTTP 开始。需要接收事件,或者每秒要发送很多命令时,再改用套接字。