架構設計:系統間通訊(11)——RPC實例Apache Thrift 上篇

一、概述

經過上一篇文章《架構設計:系統間通訊(10)——RPC的基本概念》的介紹,相信讀者已經理解了基本的RPC概念。爲了加深這個理解,後面幾篇文章我將詳細講解一款典型的RPC規範的實現Apache Thrift。Apache Thrift的介紹一共分爲三篇文章,上篇講解Apache Thrift的基本使用;中篇講解Apache Thrift的工做原理(主要圍繞Apache Thrift使用的消息格式封裝、支持的網絡IO模型和它的客戶端請求處理方式);下篇對Apache Thrift的不足進行分析,並基於Apache Thrift實現一個本身設計的RPC服務治理的管理方案。這樣對咱們後續理解Dubbo的服務治理方式會有很好的幫助做用。php

二、基本知識

Thrift最初由facebook開發用作系統內各語言之間的RPC框架 。2007年由facebook貢獻到apache基金 ,08年5月進入apache孵化器 ,稱爲Apache Thrift。和其餘RPC實現相比,Apache Thrift主要的有點是:支持的語言多(C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk等多種語言)、併發性能高(還記得上篇文章中,咱們提到的影響RPC性能的幾個關鍵點嗎?)。java

爲了支持多種語言,Apache Thrift有一套本身的接口定義語言,而且經過Apache Thrift的代碼生成程序,可以生成各類編程語言的代碼。這樣是保證各類語言進行通信的前提條件。爲了可以實現簡單的Apache Thrift實例,首先咱們就須要講解一下Apache Thrift的IDL。node

2-一、Thrift代碼生成程序安裝

若是您是在windows環境下運行進行Apache Thrift的試驗,那麼您無需安裝任何工具,直接下載Apache Thrift在windows下的代碼生成程序http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.3/thrift-0.9.3.exe(在這篇文章寫做時,使用的是Apache Thrift的0.9.3版本);若是您運行在Linux系統下,那麼下載http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.3/thrift-0.9.3.tar.gz,並進行編譯、安裝(過程很簡單,這裏就再也不贅述了)。安裝後記得添加運行位置到環境變量中。c++

2-二、IDL格式概要

如下是一個簡單的IDL文件定義:apache

# 命名空間的定義 注意‘java’的關鍵字 namespace java testThrift.iface # 結構體定義 struct Request { 1:required string paramJSON; 2:required string serviceName;
} # 另外一個結構體定義 struct Reponse { 1:required  RESCODE responeCode; 2:required string responseJSON;
} # 異常描述定義 exception ServiceException { 1:required EXCCODE exceptionCode; 2:required string exceptionMess;
} # 枚舉定義 enum RESCODE {
    _200=200;
    _500=500;
    _400=400;
} # 另外一個枚舉 enum EXCCODE {
    PARAMNOTFOUND = 2001;
    SERVICENOTFOUND = 2002;
} # 服務定義 service HelloWorldService {
    Reponse send(1:Request request) throws (1:ServiceException e);
}1234567891011121314151617181920212223242526272829303132333435363738

以上IDL文件是能夠直接用來生成各類語言的代碼的。下面給出經常使用的各類不一樣語言的代碼生成命令:編程

# 生成java thrift-0.9.3 -gen java ./demoHello.thrift # 生成c++ thrift-0.9.3 -gen cpp ./demoHello.thrift # 生成php thrift-0.9.3 -gen php ./demoHello.thrift # 生成node.js thrift-0.9.3 -gen js:node ./demoHello.thrift # 生成c# thrift-0.9.3 -gen csharp ./demoHello.thrift # 您能夠經過如下命令查看生成命令的格式 thrift-0.9.3 -help1234567891011121314151617

2-2-一、基本類型

基本類型就是:無論哪種語言,都支持的數據形式表現。Apache Thrift中支持如下幾種基本類型:json

  • bool: 布爾值 (true or false), one bytec#

  • byte: 有符號字節windows

  • i16: 16位有符號整型數組

  • i32: 32位有符號整型

  • i64: 64位有符號整型

  • double: 64位浮點型

  • string: 字符串/字符數組

  • binary: 二進制數據(在java中表現爲java.nio.ByteBuffer)

2-2-二、struct結構

在面嚮對象語言中,表現爲「類定義」;在弱類型語言、動態語言中,表現爲「結構/結構體」。定義格式以下:

struct <結構體名稱> { <序號>:[字段性質] <字段類型> <字段名稱> [= <默認值>] [;|,]
}1234

實例:

struct Request { 1:required binary paramJSON; 2:required string serviceName 3:optional i32 field1 = 0; 4:optional i64 field2, 5: list<map<string , string>> fields3
}1234567

  • 結構體名稱:能夠按照您的業務需求,給定不一樣的名稱(區分大小寫)。可是要注意,一組IDL定義文件中結構體名稱不能重複,且不能使用IDL已經佔用的關鍵字(例如required 、struct 等單詞)。

  • 序號:序號很是重要。正整數,按照順序排列使用。這個屬性在Apache Thrift進行序列化的時候被使用。

  • 字段性質:包括兩種關鍵字:required 和 optional,若是您不指定,那麼系統會默認爲required。required表示這個字段必須有值,而且Apache Thrift在進行序列化時,這個字段都會被序列化;optional表示這個字段不必定有值,且Apache Thrift在進行序列化時,這個字段只有有值的狀況下才會被序列化。

  • 字段類型:在struct中,字段類型能夠是某一個基礎類型,也能夠是某一個以前定義好的struct,還能夠是某種Apache Thrift支持的容器(set、map、list),還能夠是定義好的枚舉。字段的類型是必須指定的。

  • 字段名稱:字段名稱區分大小寫,不能重複,且不能使用IDL已經佔用的關鍵字(例如required 、struct 等單詞)。

  • 默認值:您能夠爲某一個字段指定默認值(也能夠不指定)。

  • 結束符:在struct中,支持兩種結束符,您可使用「;」或者「,」。固然您也能夠不使用結束符(Apache Thrift代碼生成程序,會本身識別到)

2-2-三、containers集合/容器

Apache Thrift支持三種類型的容器,容器在各類編程語言中廣泛存在:

  • list< T >:有序列表(JAVA中表現爲ArrayList),T能夠是某種基礎類型,也能夠是某一個以前定義好的struct,還能夠是某種Apache Thrift支持的容器(set、map、list),還能夠是定義好的枚舉。有序列表中的元素容許重複。

  • set< T >:無序元素集合(JAVA中表現爲HashSet),T能夠是某種基礎類型,也能夠是某一個以前定義好的struct,還能夠是某種Apache Thrift支持的容器(set、map、list),還能夠是定義好的枚舉。無序元素集合中的元素不容許重複,一旦重複後一個元素將覆蓋前一個元素。

  • map

2-2-四、enmu枚舉

enum <枚舉名稱> { <枚舉字段名> = <枚舉值>[;|,]
}123

示例以下:

enum RESCODE { _200=200; _500=500; _400=400;
}12345

2-2-五、常量定義

Apache Thrift容許定義常量。常量的關鍵字爲「const」,常量的類型能夠是Apache Thrift的基礎類型,也能夠是某一個以前定義好的struct,還能夠是某種Apache Thrift支持的容器(set、map、list),還能夠是定義好的枚舉。示例以下:

const i32 MY_INT_CONST = 111111; const i64 MY_LONG_CONST = 11111122222222333333334444444; const RESCODE MY_RESCODE = RESCODE._200;12345

2-2-六、exception 異常

Apache Thrift的exception,主要在定義服務接口時使用。其定義方式相似於struct(您能夠理解成,把struct關鍵字換成exception關鍵字便可),示例以下:

exception ServiceException { 1:required EXCCODE exceptionCode; 2:required string exceptionMess;
}1234

2-2-七、service 服務接口

Apache Thrift中最重要的IDL定義之一。在後續的代碼生成階段,經過IDL定義的這些服務將構成Apache Thrift客戶端調用Apache Thrift服務端的基本遠端過程。service服務接口的定義形式以下所示:

service <服務名稱> {
    <void | 返回指類型> <服務方法名>([<入參序號>:[required | optional] <參數類型> <參數名> ...]) [throws ([<異常序號>:[required | optional] <異常類型> <異常參數名>...])]
}123

  • 服務名稱:服務名能夠按照您的業務需求自行制定,注意服務名是區分大小寫的。IDL中服務名稱只有兩個限制,就是不能重複使用相同的名稱,不能使用IDL已經佔用的關鍵字(例如required 、struct 等單詞)。

  • 返回值類型:若是這個調用方法沒有返回類型,那麼能夠關鍵字「void」; 能夠是Apache Thrift的基礎類型,也能夠是某一個以前定義好的struct,還能夠是某種Apache Thrift支持的容器(set、map、list),還能夠是定義好的枚舉。

  • 服務方法名:服務方法名能夠根據您的業務需求自定製定,注意區分大小寫。在同一個服務中,不能重複使用一個服務方法名命名多個方法(必定要注意),不能使用IDL已經佔用的關鍵字。

  • 服務方法參數:<入參序號>:[required | optional] <參數類型> <參數名>。注意和struct中的字段定義類似,能夠指定required或者optional;若是不指定則系統默認爲required 。若是一個服務方法中有多個參數名,那麼這些參數名稱不能重複。

  • 服務方法異常:throws ([<異常序號>:[required | optional] <異常類型> <異常參數名>。throws關鍵字是服務方法異常定義的開始點。在throws關鍵字後面,能夠定義1個或者多個不一樣的異常類型。

Apache Thrift服務定義的示例以下:

service HelloWorldService { Reponse send(1:Request request) throws (1:ServiceException e);
}123

2-2-八、namespace命名空間

Apache Thrift支持爲不一樣語言制定不一樣的命名空間:

namespace java testThrift.iface namespace php testThrift.iface namespace cpp testThrift.iface12345

2-2-九、註釋

Apache Thrift 支持多種風格的註釋。這是爲了適應不一樣語言背景的開發者:

/*
* 註釋方式1:
**/ // 註釋方式2 # 註釋方式3


 

 1234567

2-2-十、include關鍵字

若是您的整個工程中有多個IDL定義文件(IDL定義文件的文件名能夠隨便取)。那麼您可使用include關鍵字,在IDL定義文件A中,引入一個其餘的IDL文件:

include "other.thrift"


 

 1

請注意,必定使用雙引號(不要用成中文的雙引號咯),而且不使用「;」或者「,」結束符。

以上就是IDL基本的語法了,因爲篇幅緣由不可能把每種語法、每個細節都講到,可是以上的語法要點已經足夠您編輯一個適應業務的,靈活的IDL定義了。若是您須要瞭解更詳細的Thrift IDL語法,能夠參考官方文檔的講述:http://thrift.apache.org/docs/idl

2-三、最簡單的Thrift代碼

  • 定義Thrift中業務接口HelloWorldService.Iface的實現:

package testThrift.impl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.thrift.TException; import testThrift.iface.HelloWorldService.Iface; import testThrift.iface.RESCODE; import testThrift.iface.Reponse; import testThrift.iface.Request; /**
 * 咱們定義了一個HelloWorldService.Iface接口的具體實現。<br>
 * 注意,這個父級接口:HelloWorldService.Iface,是由thrift的代碼生成工具生成的<br>
 * 要運行這段代碼,請導入maven-log4j的支持。不然修改LOGGER.info方法
 * @author yinwenjie
 */ public class HelloWorldServiceImpl implements Iface { /**
     * 日誌
     */ private static final Log LOGGER = LogFactory.getLog(HelloWorldServiceImpl.class); /**
     * 在接口定義中,只有一個方法須要實現。<br>
     * HelloWorldServiceImpl.send(Request request) throws TException <br>
     * 您能夠理解成這個接口的方法接受客戶端的一個Request對象,而且在處理完成後向客戶端返回一個Reponse對象<br>
     * Request對象和Reponse對象都是由IDL定義的結構,並經過「代碼生成工具」生成相應的JAVA代碼。
     */ @Override public Reponse send(Request request) throws TException { /*
         * 這裏就是進行具體的業務處理了。
         * */ String json = request.getParamJSON();
        String serviceName = request.getServiceName();
        HelloWorldServiceImpl.LOGGER.info("獲得的json:" + json + " ;獲得的serviceName: " + serviceName); // 構造返回信息 Reponse response = new Reponse();
        response.setResponeCode(RESCODE._200);
        response.setResponseJSON("{\"user\":\"yinwenjie\"}"); return response;
    } 
}123456789101112131415161718192021222324252627282930313233343536373839404142434445

各位能夠看到,上面一段代碼中具體業務和過程和普通的業務代碼沒有任何區別。甚至這段代碼的實現都不知道本身將被Apache Thrift框架中的客戶端調用

  • 而後咱們開始書寫Apache Thrift的服務器端代碼:

package testThrift.man; import java.util.concurrent.Executors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.BasicConfigurator; import org.apache.thrift.TProcessor; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.server.TThreadPoolServer; import org.apache.thrift.server.TThreadPoolServer.Args; import org.apache.thrift.transport.TServerSocket; import testThrift.iface.HelloWorldService; import testThrift.iface.HelloWorldService.Iface; import testThrift.impl.HelloWorldServiceImpl; public class HelloBoServerDemo { static {
        BasicConfigurator.configure();
    } /**
     * 日誌
     */ private static final Log LOGGER =LogFactory.getLog(HelloBoServerDemo.class); public static final int SERVER_PORT = 9111; public void startServer() { try {
            HelloBoServerDemo.LOGGER.info("看到這句就說明thrift服務端準備工做 ...."); // 服務執行控制器(只要是調度服務的具體實現該如何運行) TProcessor tprocessor = new HelloWorldService.Processor<Iface>(new HelloWorldServiceImpl()); // 基於阻塞式同步IO模型的Thrift服務,正式生產環境不建議用這個 TServerSocket serverTransport = new TServerSocket(HelloBoServerDemo.SERVER_PORT); // 爲這個服務器設置對應的IO網絡模型、設置使用的消息格式封裝、設置線程池參數 Args tArgs = new Args(serverTransport);
            tArgs.processor(tprocessor);
            tArgs.protocolFactory(new TBinaryProtocol.Factory());
            tArgs.executorService(Executors.newFixedThreadPool(100)); // 啓動這個thrift服務 TThreadPoolServer server = new TThreadPoolServer(tArgs);
            server.serve();
        } catch (Exception e) {
            HelloBoServerDemo.LOGGER.error(e);
        }
    } /**
     * @param args
     */ public static void main(String[] args) {
        HelloBoServerDemo server = new HelloBoServerDemo();
        server.startServer();
    }
}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162

以上的代碼有幾點須要說明:

  1. TBinaryProtocol:這個類代碼Apache Thrift特有的一種二進制描述格式。它的特色是傳輸單位數據量所使用的傳輸量更少。Apache Thrift還支持多種數據格式,例如咱們熟悉的JSON格式。後文咱們將詳細介紹Apache Thrift中的數據格式。

  2. tArgs.executorService():是否是以爲這個executorService很熟悉,是的這個就是JAVA JDK 1.5+ 後java.util.concurrent包提供的異步任務調度服務接口,Java標準線程池ThreadPoolExecutor就是它的一個實現。

  3. server.serve(),因爲是使用的同步阻塞式網絡IO模型,因此這個應用程序的主線程執行到這句話之後就會保持阻塞狀態了。不過下層網絡狀態不出現錯誤,這個線程就會一直停在這裏。

另外,同HelloWorldServiceImpl 類中的代碼,請使用Log4j。若是您的測試工程裏面沒有Log4j,請改用System.out。

  • 接下來咱們進行最簡單的Apache Thrift Client的代碼編寫:

package testThrift.client; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.BasicConfigurator; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.transport.TSocket; import testThrift.iface.HelloWorldService; import testThrift.iface.Reponse; import testThrift.iface.Request; /**
 * 一樣是基於同步阻塞模型的thrift client。
 * @author yinwenjie
 */ public class HelloClient { static {
        BasicConfigurator.configure();
    } /**
     * 日誌
     */ private static final Log LOGGER = LogFactory.getLog(HelloClient.class); public static final void main(String[] args) throws Exception { // 服務器所在的IP和端口 TSocket transport = new TSocket("127.0.0.1", 9111);
        TProtocol protocol = new TBinaryProtocol(transport); // 準備調用參數 Request request = new Request("{\"param\":\"field1\"}", "\\mySerivce\\queryService");
        HelloWorldService.Client client = new HelloWorldService.Client(protocol); // 準備傳輸 transport.open(); // 正式調用接口 Reponse reponse = client.send(request); // 必定要記住關閉 transport.close();

        HelloClient.LOGGER.info("response = " + reponse);
    }
}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647

  • Thrift客戶端所使用的網絡IO模型,必需要與Thrift服務器端所使用的網絡IO模型一致。也就是說服務器端若是使用的是阻塞式同步IO模型,那麼客戶端就必須使用阻塞式同步IO模型。

  • Thrift客戶端所使用的消息封裝格式,必需要與Thrift服務器端所使用的消息封裝格式一直。也就是說服務器端若是使用的是二進制流的消息格式TBinaryProtocol,那麼客戶端就必須一樣使用二進制劉的消息格式TBinaryProtocol。

  • 其它的代碼要麼就是由IDL定義並由Thrift的代碼生成工具生成;要麼就不是重要的代碼,因此爲了節約篇幅就沒有必要再貼出來了。如下是運行效果。

  • 服務器端運行效果

這裏寫圖片描述

  • 服務器端收到客戶端請求後,取出線程池中的線程進行運行

這裏寫圖片描述

請注意服務器端在收到客戶端請求後的運行方式:取出一條線程池中的線程,而且運行這個服務接口的具體實現。接下來咱們立刻介紹Apache Thrift的工做細節。

(接下文)

相關文章
相關標籤/搜索