다른 언어

Go, Java, C#, PHP 등 어떤 언어에서든 HTTP나, 명령 하나를 JSON 한 줄로 주고받는 소켓 직접 연결로 Droidline을 씁니다. 실제로 실행해 본 예제가 함께 있습니다.

번역된 페이지입니다. 영어 문서와 내용이 다르면 영어 문서가 기준입니다. English

모든 언어에 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";

세 예제의 공통점이자, 다른 언어로 옮길 때 그대로 따라 하면 되는 부분입니다.

  1. http://localhost:8780/devices/_/<command>로 POST합니다. _ 자리에 폰 이름을 넣어도 됩니다.
  2. Content-Type: application/json 헤더와 함께, 명령의 파라미터를 담은 JSON 객체를 보냅니다.
  3. 응답에서 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으로 주고받습니다. TCP보다 WebSocket이 다루기 쉬운 언어에서 쓰세요.

프로토콜에서 모든 필드와 이벤트, 그리고 네트워크를 끊는 명령이 최종 결과를 알리는 방식을 설명합니다.

무엇을 고를까

HTTP소켓 직접 연결
수고가장 적음: HTTP 라이브러리면 무엇이든조금: 연결을 유지하고 줄 단위로 읽기
속도명령마다 연결 하나모든 명령에 연결 하나
여러 명령 동시 실행요청 여러 개를 병렬로연결 하나에서 파이프라이닝
이벤트(알림, 폰의 오프라인 전환)안 됨됨, subscribe 후

HTTP로 시작하세요. 이벤트가 필요하거나 초당 명령을 많이 보내야 할 때 소켓으로 바꾸면 됩니다.