網絡通訊協議自動轉換之thrift到http

背景

在日常的業務開發中遇到了兩個場景:html

1.因爲業務用的rpc框架是thrift,代碼也是都是用thrift再寫,有一天忽然接到個須要前端要用http訪問接口的需求,因而花了幾天時間把全部的thrift接口又用Controller封裝一層。因爲跨語言,且對方不使用thrift,就須要你提供Http接口前端

2.寫完thrift爲了自測,須要再寫個TestController驗證代碼是否正確,整個流程是否跑通,很是麻煩。node

這兩個場景你們遇到的比較多,因此要是能一寫完thrift接口就能直接轉換爲http接口,那樣就行了。git

放眼整個互聯網中,在互聯網快速迭代的大潮下,愈來愈多的公司選擇nodejs、django、rails這樣的快速腳本框架來開發web端應用,而對於咱們來講公司選擇的後端語言是Java,這就產生了大量的跨語言的調用需求。其實對於thrift來講是支持不少語言的,可是給每次給其餘語言開發都須要開發對應的客戶端,而且還有不少rpc框架並非像thrift同樣支持這麼多語言的,因此如今微服務都推出了service mesh(www.servicemesh.cn/),可是這個依然很新,有須要嘗試的其實能夠起嘗試一下。http、json是自然合適做爲跨語言的標準,各類語言都有成熟的類庫,因此如何把像thrift這種tcp rpc框架轉換成http,對於多語言支持是比較重要的。github

RESTful or JSONRPC

RESTful

最開始想的是如何把thrift接口映射成RESTful,由於這個更加符合互聯網http的標準,可是TCP rpc 對比RESTful有根本的區別,RESTful的核心是資源,而且利用Http協議中的各類方法GET,POST,OPTION等等對資源進行操做,若是想把thrift每一個接口一一映射上,這個難度有點大,畢竟兩個產生不出來任何關聯,這個時候就須要每一個接口進行配置映射,起成本不亞於我重寫一套Controller了,因此RESTful這個方案基本被否決了。web

JSONRPC

JSON-RPC是一個無狀態且輕量級的遠程過程調用(RPC)協議。它容許運行在基於socket,http等諸多不一樣消息傳輸環境的同一進程中。spring

JSONRPC本質上也是個RPC,定位和thrfit相似,不須要進行過多的協議映射。因此咱們選擇了使用JSONRPC,進行Http的轉換。django

JSONRPC請求對象

發送一個請求對象至服務端表明一個rpc調用, 一個請求對象包含下列成員:json

jsonrpc後端

指定JSON-RPC協議版本的字符串,必須準確寫爲「2.0」

method

包含所要調用方法名稱的字符串,以rpc開頭的方法名,用英文句號(U+002E or ASCII 46)鏈接的爲預留給rpc內部的方法名及擴展名,且不能在其餘地方使用。

params

調用方法所須要的結構化參數值,該成員參數能夠被省略。

id

已創建客戶端的惟一標識id,值必須包含一個字符串、數值或NULL空值。若是不包含該成員則被認定爲是一個通知。該值通常不爲NULL[1],若爲數值則不該該包含小數[2]

服務端必須回答相同的值若是包含在響應對象。 這個成員用來兩個對象之間的關聯上下文。

[1] 在請求對象中不建議使用NULL做爲id值,由於該規範將使用空值認定爲未知id的請求。另外,因爲JSON-RPC 1.0 的通知使用了空值,這可能引發處理上的混淆。

[2] 使用小數是不肯定性的,由於許多十進制小數不能精準的表達爲二進制小數。

通知

沒有包含「id」成員的請求對象爲通知, 做爲通知的請求對象代表客戶端對相應的響應對象並不感興趣,自己也沒有響應對象須要返回給客戶端。服務端必須不回覆一個通知,包含那些批量請求中的。

因爲通知沒有返回的響應對象,因此通知不肯定是否被定義。一樣,客戶端不會意識到任何錯誤(例如參數缺省,內部錯誤)。

參數結構

rpc調用若是存在參數則必須爲基本類型或結構化類型的參數值,要麼爲索引數組,要麼爲關聯數組對象。

  • 索引:參數必須爲數組,幷包含與服務端預期順序一致的參數值。

  • 關聯名稱:參數必須爲對象,幷包含與服務端相匹配的參數成員名稱。沒有在預期中的成員名稱可能會引發錯誤。名稱必須徹底匹配,包括方法的預期參數名以及大小寫。

響應對象

當發起一個rpc調用時,除通知以外,服務端都必須回覆響應。響應表示爲一個JSON對象,使用如下成員:

jsonrpc

指定JSON-RPC協議版本的字符串,必須準確寫爲「2.0」

result

該成員在成功時必須包含。

當調用方法引發錯誤時必須不包含該成員。

服務端中的被調用方法決定了該成員的值。

error

該成員在失敗是必須包含。

當沒有引發錯誤的時必須不包含該成員。

該成員參數值必須爲5.1中定義的對象。

id

該成員必須包含。

該成員值必須於請求對象中的id成員值一致。

若在檢查請求對象id時錯誤(例如參數錯誤或無效請求),則該值必須爲空值。

響應對象必須包含result或error成員,但兩個成員必須不能同時包含。

錯誤對象

當一個rpc調用遇到錯誤時,返回的響應對象必須包含錯誤成員參數,而且爲帶有下列成員參數的對象:

code

使用數值表示該異常的錯誤類型。 必須爲整數。

message

對該錯誤的簡單描述字符串。 該描述應儘可能限定在簡短的一句話。

data

包含關於錯誤附加信息的基本類型或結構化類型。該成員可忽略。 該成員值由服務端定義(例如詳細的錯誤信息,嵌套的錯誤等)。

JsonRpc4j

jsonRpc4j是一款用Java語言實現的JSONRPC的框架,使用JackSon進行JSON解析。他的github地址爲:github.com/briandilley…

在jsonRpc4j中他能夠處理HTTP Server (HttpServletRequest \ HttpServletResponse),因此可以幫助咱們很快的構建httpserver,使用JsonRpc4j很簡單:

ObjectMapper mapper = new ObjectMapper();

JsonRpcServer skeleton = new JsonRpcServer(mapper, new DemoService(), (Class<?>) service.getClass());

skeleton.handle(req, resp);
複製代碼

首先建立一個ObjectMapper,用於JSON的轉換的,而後 把須要變成Server的Service放進JsonRpcServer,最後執行這個請求。

thrift到http

對於thrift到http是利用Serlvet加上jsonRpc4j完成關係的映射,以下圖所示:


HTTP URL

http中關鍵在於http URL如何制定,這裏URL爲了簡單快速明瞭,用如下規則:

POST: servlet-url-pattern + thriftServiceInfaceName

首先全部thrift方法公共的路徑在Servlet中制定,全部/thrift/*的URL都走ThriftSerlvet

<servlet>

 <servlet-name>thriftSerlvet</servlet-name>

 <servlet-class>com.thrift.ThriftSerlvet</servlet-class>

 </servlet>

 <servlet-mapping>

 <servlet-name>thriftSerlvet</servlet-name>

 <url-pattern>/thrift/*</url-pattern>

 </servlet-mapping>
複製代碼

咱們有以下一個thrift

public class CustomerThriftServiceImpl implements customerService.Iface{

 @Override

 public QueryCustomerResp queryCustomer(QueryRuleReq queryReq) throws TException {

 QueryCustomerResp result = new QueryCustomerResp();

 try {

 result.setCustomr(new Customer());

 result.setStatus(ThriftRespStatusHelper.OK);

 } catch (Exception e) {

 LOGGER.error("查詢出現錯誤{}", e);

 result.setStatus(ThriftRespStatusHelper.failure("查詢失敗"));

 }

 return result;

 }

}
複製代碼

因此咱們的URL以下/thrift/customerService

ThrifSerlvet

咱們全部的thrift的請求都會通過這個serlvet,而後經過其作jsonRpcServer的路由分發代碼以下:

public class ThriftSerlvet extends HttpServlet {

    public static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
    public static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";
    public static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers";

    private final Map<String, JsonRpcServer> rpcServerMap = new ConcurrentHashMap<>();

    private Logger LOGGER = LoggerFactory.getLogger(ThriftSerlvet.class);

    public static final String JSON_FILTER_ID = "thriftPropFilter";

    @Override
    public void init() throws ServletException {
        super.init();
        WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        Map<String, ThriftServerPublisher> publisherMap = rootContext.getBeansOfType(ThriftServerPublisher.class);
        if (publisherMap == null || publisherMap.size() == 0) {
            return;
        }
        for (ThriftServerPublisher serverPublisher : publisherMap.values()) {
            try {
                Field serviceImplField = serverPublisher.getClass().getDeclaredField("serviceImpl");
                serviceImplField.setAccessible(true);
                Object serveiceImpl = serviceImplField.get(serverPublisher);
                addJsonRpcServer(serveiceImpl, serverPublisher.getServiceSimpleName());
            } catch (Exception e) {
                LOGGER.error("this serverPublisher:{}, get the filed:{} has error", serverPublisher, "serviceImpl", e);
            }
        }
    }

    private void addJsonRpcServer(Object serveiceImpl, String serviceSimpleName) {
        serviceSimpleName = serviceSimpleName.replaceFirst(String.valueOf(serviceSimpleName.charAt(0)), String.valueOf(serviceSimpleName.charAt(0)).toLowerCase());
        LOGGER.info("serverPubliser");
        ObjectMapper mapper = new ObjectMapper();
        SimpleFilterProvider simpleFilterProvider = new SimpleFilterProvider();
        simpleFilterProvider.addFilter(JSON_FILTER_ID, new ThriftPropertiesFilter());
        mapper.setFilterProvider(simpleFilterProvider);
        mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
            @Override
            public Object findFilterId(Annotated a) {
                return JSON_FILTER_ID;
            }
        });
        JsonRpcServer rpcServer = new JsonRpcServer(mapper, serveiceImpl, serveiceImpl.getClass().getSuperclass());
        rpcServer.setInterceptorList(Arrays.asList(new ThriftJsonInterceptor()));
        rpcServerMap.put(serviceSimpleName, rpcServer);

    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*");
        resp.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, "POST");
        resp.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, "*");
        if (req.getMethod().equalsIgnoreCase("OPTIONS")) {
            resp.sendError(200);
        } else if (req.getMethod().equalsIgnoreCase("POST")) {
            String uri = req.getRequestURI();
            String path = req.getServletPath();
            String serviceName = uri.substring(path.length(), uri.length()).replaceAll("/", "");
            JsonRpcServer rpcServer = rpcServerMap.get(serviceName);
            if (rpcServer == null) {
                resp.sendError(404);
                return;
            }
            rpcServer.handle(req, resp);
        } else {
            //方法不被容許
            resp.sendError(405);
        }

    }

}

複製代碼

執行過程以下:

1.init:初始化的時候咱們須要把咱們全部的thriftService的Bean從Spring容器中拿出來,而後對每一個Service構建一個不一樣的JsonRpcServer,放進Map中等待service方法路由。

這裏初始化有幾點注意:

  • ObjectMapper咱們對於輸出過濾了以set開頭的由於jackSon轉換thrift的時候會把thrift本身生成的文件給轉換出來。

  • 這裏咱們在spring的配置文件中要配置

<aop:aspectj-autoproxy proxy-target-class="true"/>複製代碼

顯示的要使用cglib,若是不指定這個默認是Jdk的代理,jdk代理的話默認就拿不到本身原本的類了,這裏必需要使用cglib代理,這樣經過getSuperClass便可得到本身原本的Class。

2.service就比較簡單了,咱們先加了容許跨域,而後指定只有POST方法才能訪問。

JsonRpc4j的修改

對於這個開源項目並無直接用他而是對他進行了修改,爲何會須要進行修改呢?

咱們簡單看看下面這個方法

public Person sayHello(Person person, Type type);複製代碼

若是咱們想用調用這個服務的話須要傳入的json爲:

{"jsonrpc": "2.0", "method": "sayHello", "params": \[{"age":"12","name":"lizhao"},{"type":1}\], "id": 1}複製代碼

上面這個json,params參數傳入的是數組,其實咱們更但願傳的是下面這樣,由於對於這種參數須要用名字指定,才能更加可讀,減小出錯的機率:

{"jsonrpc": "2.0", "method": "sayHello", "params": {"person":{"age":"12","name":"lizhao"},"type":{"type":1}}, "id": 1}複製代碼

可是這樣傳的話會報出找不到方法,jsonrpc4j官方的作法是用註解,將方法修改爲:

public Person sayHello(@JsonRpcParam("person")Person person, @JsonRpcParam("type")Type type);複製代碼

可是用過thrift的同窗都知道,thrift的不少代碼都是根據IDL生成的,這樣會致使一個問題,不能使用註解,由於一旦用了註解以後下次生成會直接覆蓋。因此這裏咱們必需要使用傳入的參數的名字才行,具體修改的實現以下:

private List<JsonRpcParam> getAnnotatedParameterNames(Method method) {

 List<JsonRpcParam> parameterNames = new ArrayList<>();

​

 List<Parameter> parameters = ReflectionUtil.getParameters(method);

 Iterator<Parameter> parameterIterator = parameters.iterator();

​

 List<String> parameterLocalNames = ReflectionUtil.getParameterLocalNames(method);

 Iterator<String> parameterLocalNameIterator = parameterLocalNames.iterator();

​

 while (parameterIterator.hasNext() && parameterLocalNameIterator.hasNext()) {

 parameterNames.add(getJsonRpcParamType(parameterIterator.next(), parameterLocalNameIterator.next()));

 }

 return parameterNames;

 }

​

 public static List<String> getParameterLocalNames(Method method) {

 List<String> parameterNames = new ArrayList<>();

 Collections.addAll(parameterNames, PARAMETER\_NAME\_DISCOVERER.getParameterNames(method));

 return Collections.unmodifiableList(parameterNames);

 }
複製代碼

這裏主要是使用spring中的ParameterNameDiscoverer經過字節碼獲取參數名字,這樣咱們就不須要用註解便可使用傳參數名字的方式。

Swagger

Swagger是一個規範且完整的框架,提供描述、生產、消費和可視化RESTful Web Service。swagger其實不是很適合對於這種,可是也能進行生成,能夠經過重寫swagger-maven-plugin這個開源框架,能生成本身指定的,可是因爲這個目前只用來作快速調試,swagger這部分暫時尚未計劃。

總結

本次主要介紹瞭如何從thrfit轉換爲http,還有更多的細節,鑑權,分佈式追蹤系統埋點等等須要補充,這種方法實現http可能不是最好的,我以爲最好的仍是要實現rest,畢竟rest纔是互聯網系統調用所承認的,可是經過這種方式瞭解瞭如何從一個協議轉換成另一個協議,補充了本身在協議轉換這方面的一些空白吧。

參考文檔

jsonRpc2.0規範

更多技術福利請掃描下方公衆號

相關文章
相關標籤/搜索