微服務框架Finagle介紹 Part2: 在Finagle中開發基於Http協議的應用

原文地址:http://skaka.me/blog/2016/05/01/finagle2/

在上篇文章中我介紹了Finagle中的Future/Service/Filter. 這篇文章裏, 我們將構建一個基於Http協議的echo服務端和客戶端, 下篇文章將構建一個基於thrift協議的客戶端和服務端. 這兩篇文章對應的源代碼地址在Github. 代碼中有Java和Scala版本兩套版本的實現, 但是這裏我只會介紹Java版本.

首先來看echo應用的Server端代碼, 打開java-finagle-example/src/main/java/com/akkafun/finagle/Server.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Server extends Service<Request, Response> {                             //1

    @Override
    public Future<Response> apply(Request request) {                                 //2
        System.out.println("request: " + request.getContentString());
        Response response = Response.apply(Version.Http11$.MODULE$, Status.Ok());
        response.setContentString(request.getContentString());
        return Future.value(response);
    }

    public static void main(String[] args) throws Exception {
        Server service = new Server();

        ListeningServer server = Http.server().                                      //3
                withLabel("echo-server").
                withTracer(ZipkinTracer.mk("192.168.99.100",
                    9410, DefaultStatsReceiver$.MODULE$, 1.0f)).
                serve(new InetSocketAddress(8081), service);

        Await.result(server);
    }
}
  1. 在Finagle中, 實現一個RPC服務非常簡單. 只需要繼承Service抽象類, 實現它的apply方法. Service抽象類有兩個類型參數, 第一個類型參數代表的是請求對象, 第二個類型參數代表的是返回對象. 這兩個對象的具體類型與Service實現類使用的具體協議有關. 例如我們在echo服務中使用Http協議, 對應的Request類就是com.twitter.finagle.http.Request, 對應的Response類是com.twitter.finagle.http.Response. 如果是thrift協議, 則這兩個類型參數在Service實現類中都是scala.Array<scala.Byte>(Array和Byte都是scala中的類, 對應Java中的數組與byte).

  2. apply方法中, 我們首先使用Response的工廠方法構造一個Response對象. 然後將Request中的請求內容原封不動的設置到Response中, 再將Response設置到Future中返回. 需要最後一步的原因是apply方法的返回值類型是Future<Response>, 但是我們在這個方法中不需要進行異步操作, 所以可以直接使用Future.value(response)將對象包裝成Future返回. 另外, 細心的你應該發現了一行比較礙眼的代碼: Response.apply(Version.Http11$.MODULE$, Status.Ok()), 其中Version的用法很古怪. 這是Java調用Scala伴生對象的副作用, Scala有一些語法和特性在Java中沒有對應的概念, 這種情況下Java調用Scala的代碼就會比較晦澀.

  3. 爲了啓動Service實例, 我們需要構造一個com.twitter.finagle.ListeningServerwithLabel設置服務名稱, withTracer設置監控信息, 這個等後面介紹zipkin的時候在解釋. 最後指定端口啓動服務.

現在來看echo應用的Client端代碼, 打開java-finagle-example/src/main/java/com/akkafun/finagle/Client.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import static scala.compat.java8.JFunction.*;

public class Client {

    public static void main(String[] args) throws TimeoutException, InterruptedException {
        Service<Request, Response> service = Http.client().                             //1
                withLabel("echo-client").
                withTracer(ZipkinTracer.mk("192.168.99.100",
                    9410, DefaultStatsReceiver$.MODULE$, 1.0f)).
                newService("127.0.0.1:8081");

        //create a "Greetings!" request.
        Reader data = Reader$.MODULE$.fromStream(                                       //2
            new ByteArrayInputStream("Greetings!".getBytes(StandardCharsets.UTF_8)));
        Request request = Request.apply(Version.Http11$.MODULE$,
            Method.Post$.MODULE$, "/", data);

        Future<Response> responseFuture = Await.ready(service.apply(request));          //3
        responseFuture.onSuccess(func(response -> {                                     //4
            System.out.println(String.format("response status: %s, response string: %s",
                    response.status().toString(), response.contentString()));
            return BoxedUnit.UNIT;
        }));
        responseFuture.onFailure(func(e -> {
            System.out.println("error: " + e.toString());
            return BoxedUnit.UNIT;
        }));
        responseFuture.ensure(func(() -> {
            service.close();
            //IDE may complain here, just ignore
            return BoxedUnit.UNIT;
        }));

    }
}
  1. 這部分代碼和我們之前的Server類代碼很像. 在Server類中, 我們創建了一個Service實例並監聽了8081端口, 現在客戶端通過newService創建了一個Service的stub.

  2. 這部分代碼用來構造一個消息內容爲Greetings的Http請求.

  3. service.apply(request)就是一次客戶端到服務端的RPC調用. 這個調用的返回值是Future<Response>.
    service.apply(request)是一個異步操作, 主線程調用這個方法並不會阻塞, 有可能主線程退出了實際調用還沒有完成. 所以這裏就要用到Await.ready了. Await.ready的作用是等待一個Future執行完成再返回, 是一個同步操作. 通過調用Await.ready我們就能將一個異步操作轉化成一個同步操作.

  4. 接下來我們在Future上註冊請求成功與失敗的回調函數. 請求成功的回調函數中只是簡單的打印出響應的消息內容.
    這裏有個細節需要說明一下. Future的onSuccess方法需要傳入一個Scala的函數特質: scala.Function1[Response, BoxedUnit]. 如果是Java6或7, 我們可以這樣實現這個特質:

1
2
3
4
5
6
7
8
responseFuture.onSuccess(new AbstractFunction1<Response, BoxedUnit>(){
    @Override
    public BoxedUnit apply(Response response) {
        System.out.println(String.format("response status: %s, response string: %s",
                response.status().toString(), response.contentString()));
        return BoxedUnit.UNIT;
    }
});

在Java8中, 這種匿名類我們一般會使用Lambda代替, 理想情況下寫法是這樣:

1
2
3
4
5
responseFuture.onSuccess(response -> {
    System.out.println(String.format("response status: %s, response string: %s",
            response.status().toString(), response.contentString()));
    return BoxedUnit.UNIT;
});

可惜的是這種寫法編譯不會通過, 因爲只有符合FunctionalInterface定義的接口才能使用Lambda表達式(什麼是FunctionalInterface, 請參考Javadoc), 而在Scala2.11中, scala.Function1不是一個FunctionalInterface(Scala2.12會兼容Java8). 爲了在這裏使用Lambda, 我們使用了scala-java8-compat這個庫, 調用scala.compat.java8.JFunction.func方法將一個FunctionalInterface轉化成scala.Function1.

可以看出, 在Java中調用Finagle的API不是很方便. 所以Finagle適合以Scala爲主, Java爲輔的項目. 如果項目全是Java, 則值得爲Finagle主要的API寫一層Java的適配層, 來屏蔽Java調用Scala代碼會出現的一些晦澀代碼.

現在我們啓動服務端和客戶端來看看運行結果. 首先啓動Server類, 然後啓動Client. Client運行完畢自動結束, 你應該能在Client的控制檯看到如下輸出:

1
response status: Status(200), response string: Greetings!

Server控制檯的輸出:

1
request: Greetings!

Http協議比較適合用於對外提供服務, 並且一般會使用REST. 在Finagle中使用REST可以使用Finch庫. 這個庫輕量小巧, API簡單, 提供了一套很方便的對Http消息進行操作的DSL. 如果是內網服務調用, 一般推薦使用結構緊湊, 傳輸效率高的協議. 比如protocol buffer, thrift或Avro. Finagle對thrift有很好的支持, 下篇文章我將介紹在Finagle中如何開發thrift應用.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章