Java程序員的現代RPC指南

Java程序員的現代RPC指南


1.前言

1.1 RPC框架簡介

最先接觸RPC仍是初學Java時,直接用Socket API傳東西好麻煩。因而發現了JDK直接支持的RMI,而後就用得不亦樂乎,各類大做業裏凡是涉及到分佈式通訊的都用RMI,真是方便。後來用上了Spring,發現Spring提供了好多Exporter,能夠無侵入地將一個POJO暴露爲RPC服務。html

接觸了這麼多RPC框架後,發現當時公司內部本身實現了一套支持壓縮、加密等附加功能的RPC基礎框架,因而就讀了一下源碼,發現原來本身實現個簡單的RPC挺簡單啊,選好序列化框架後用反射爲服務接口生成存根就好了。核心技術就是:序列化和動態反射java

前一陣子又接觸到了多語言支持的RPC框架。其實傳統的方式也是能支持多種編程語言的,只要序列化框架爲多種語言都提供了版本支持,那麼序列化後使用相同的網絡協議傳輸就能實現跨語言的RPC了,這也是最輕量級的自制RPC了,靈活可是手動工做量比較大。再就是重量級的SOAP WebService或簡單方便的REST,後者通常採用JSON格式攜帶數據,最典型的場景就是先後臺的服務調用,從JS到Java的RPC。python

本文要重點介紹的則是另一套RPC框架。相比通常的RPC框架來講,它可以支持多種語言間的RPC;相比WebService,它卻沒有SOAP那麼重量級,又比輕量級的REST高效。對於組件之間須要頻繁通訊、又對性能要求較高的分佈式系統來講,它是不錯的解決方案。程序員

1.2 Protobuf vs. Thrift

Protobuf全名爲Protocol Buffer,是Google推出的支持多語言的RPC基礎設施。經過自定義語言無關的IDL文件和Protoc代碼生成器達到跨語言RPC通訊的目的。但也正由於跨語言,與咱們僅限於Java的那些RPC框架相比稍顯複雜一些。以前研究Protobuf序列化性能時,也正因這一點而採用了Java簡化版Protostuff,詳情請見《序列化戰爭:主流序列化框架Benchmark》apache

Thrift是Facebook推出的RPC框架,與PB相比提供了內建的RPC支持,而PB開源版裏並無RPC功能(也是後面實踐時才發現的)。Thrift的RPC提供了多種網絡模式和序列化的選項,能夠根據不一樣場景來靈活搭配使用。編程

關於Protobuf、Thrift以及本文未涉及的Avro,在《大數據日知錄》裏有具體的比較,感興趣的能夠參考一下。ruby

1.3 核心技術

前面說了傳統RPC框架的核心技術,「現代」RPC爲了支持多語言因此稍顯複雜一點兒。核心技術主要有:IDL、代碼生成器、序列化、RPCmarkdown

  • 在IDL文件中經過不與具體編程語言相關的語法定義通訊類
  • 利用代碼生成器生成出Java語言對應的代碼
  • 引入框架的運行時Jar包,得到序列化、RPC等能力

因此前二者決定了框架對不一樣編程語言的支持能力,然後二者決定了運行時的調用性能。網絡


2.Maven集成

無論使用哪一種框架,既然涉及到了代碼生成,那就要想法與項目構建的過程結合到一塊兒。這裏以Java項目最經常使用的Maven爲例,看一下如何將代碼生成器與Maven相結合,而且有哪些注意事項。app

2.1 Compile階段

按照咱們的設想,代碼生成過程應該在編譯階段,這樣生成的代碼就能跟已有代碼一塊兒被編譯、打包、發佈,二者沒有什麼差異。一旦修改了IDL,直接編譯就能看到最新生成的代碼了,這就是咱們想要的效果。

2.2 Ant集成插件

由於有些框架並不提供專門的插件,因此與Maven最簡單通用的集成方法就是採用maven-antrun-plugin插件。此插件能夠執行任意命令,標準寫法以下:

<build>
        <plugins>
            <plugin>
                <artifactId>maven-antrun-plugin</artifactId>
                <executions>
                    <execution>
                        <id>generate-sources</id>
                        <phase>generate-sources</phase>
                        <configuration>
                            <tasks>
                                <exec executable="xxx.exe">
                                    <arg value="arg1 arg2 ..."/>
                                </exec>
                            </tasks>
                            <sourceRoot>target/generated-sources</sourceRoot>
                        </configuration>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

3.Protobuf實戰

3.1 編寫IDL

語法很是簡單,其中java_generic_services選項決定是否生成Service類,默認不生成,聽說是不建議使用。

package com.test;

option java_generic_services = true;

message Request
{
    required int32 type = 1;
}

message Response
{
    required int32 cpu = 1;
    required int32 memorySize = 2;
}

service AgentService
{
    rpc detectHardware(Request) returns (Response);
}

3.2 Protoc編譯

Windows版預編譯好的protoc.exe支持C++,Java,Python三種最經常使用的語言,若是你只使用這幾種語言的話那就很簡單了。之因此把Protobuf相關文件都放到src/protobuf而非src/main/resources下是由於:src/main/resources裏東西默認會被包含到最終的jar裏。若是咱們不想把protoc.exe和一堆.proto文件打到jar包裏發佈的話,要麼加一個Maven的拷貝filter,或者像本文的方法將Protobuf相關文件都放到src/protobuf下。

<build>
        <plugins>
            <plugin>
                <artifactId>maven-antrun-plugin</artifactId>
                <executions>
                    <execution>
                        <id>generate-sources</id>
                        <phase>generate-sources</phase>
                        <configuration>
                            <tasks>
                                <exec executable="src/protobuf/protoc.exe" failonerror="true">
                                    <arg value="--java_out=src/main/java"/>
                                    <arg value="src/protobuf/idl/*.proto"/>
                                </exec>
                            </tasks>
                            <sourceRoot>target/generated-sources</sourceRoot>
                        </configuration>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

3.3 使用源碼

如下是利用Protobuf生成的代碼進行序列化和反序列化的示例。因爲缺乏RPC功能,因此也只能測試一下序列化功能了。

public class PbRpcTest {

    public static void main(String[] args) throws InvalidProtocolBufferException {
        // Build request
        Agent.Request.Builder reqBuilder = Agent.Request.newBuilder();
        reqBuilder.setType(1);
        Agent.Request req = reqBuilder.build();
        System.out.println(req);

        // Parse from bytes
        byte[] bytes = req.toByteArray();
        Agent.Request req2 = Agent.Request.parseFrom(bytes);
        System.out.println(req2.getType());
    }
}

4.Thrift

4.1 編譯IDL

與Protobuf的IDL類似,Thrift的IDL也很簡單。

namespace java com.test service AgentService {
    string detectHardware()
}

4.2 Java

Thrift支持多種網絡和序列化模式,這裏採起最簡單的同步阻塞和二進制序列化的方式。

public class RpcClientTest {

    public static void main(String[] args) throws TException {
        TSocket transport = new TSocket("127.0.0.1", 8090);
        TProtocol protocol = new TBinaryProtocol(transport);
        AgentService.Client client = new AgentService.Client(protocol);
        transport.open();

        System.out.println(client.detectHardware());
    }
}

public class RpcServerTest {

    public static void main(String[] args) throws TTransportException {
        AgentService.Processor<AgentService.Iface> processor = new AgentService.Processor<AgentService.Iface>(
                new AgentServiceImpl()
        );

        TServerSocket transport = new TServerSocket(8090);
        TServer.Args tArgs = new TServer.Args(transport);
        tArgs.processor(processor);
        tArgs.protocolFactory(new TBinaryProtocol.Factory());

        TServer server = new TSimpleServer(tArgs);
        server.serve();
    }
}

public class AgentServiceImpl implements AgentService.Iface {

    @Override
    public String detectHardware() throws TException {
        return "hello";
    }
}

4.3 Python

Python要想運行時支持Thrift,也須要安裝相應的插件。我是在Windows下的Cygwin中完成安裝的,而後在cmd中執行卻報錯仍是沒找到thrift模塊,結果發現是cmd和Cygwin默認執行的Python版本不同。汗,以前可能2和3都裝了忘記了,生成的代碼用Python 3運行的話會有問題:

$ tar -xzvf thrift-0.9.3.tar.gz
$ cd thrift-0.9.3/
$ python setup.py install

注意必定要指定IP地址,不然Java客戶端調用Python服務端時會報」Connection refused」錯誤,詳見Stackoverflow上的問題解答

import sys, glob
sys.path.append('gen-py')
#sys.path.insert(0, glob.glob('../../lib/py/build/lib*')[0])

from agent import AgentService
from agent.ttypes import *

from thrift import Thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TServer

class AgentServiceHandler:
    def __init__(self):
        print('init')

    def detectHardware(self):
        print('detect!')
        return 'hello~~~'

handler = AgentServiceHandler()
processor = AgentService.Processor(handler)
transport = TSocket.TServerSocket(host="127.0.0.1", port=8090)
tfactory = TTransport.TBufferedTransportFactory()
pfactory = TBinaryProtocol.TBinaryProtocolFactory()

server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)
print('Starting the server...')
server.serve()
print('done.')

這就是目錄結構,如今就能夠啓動服務端,客戶端仍然用以前的Java客戶端,調用成功!

$ tree py-demo/ -I "*.pyc"
py-demo/
|-- agent.thrift
|-- gen-py
|   |-- __init__.py
|   `-- agent
|       |-- __init__.py
|       |-- __pycache__
|       |-- AgentService.py
|       |-- AgentService-remote
|       |-- constants.py
|       `-- ttypes.py
|-- server.py
`-- thrift-0.9.3.exe

$ python server.py
init
Starting the server...
detect!

4.總結

與直接使用Java其餘RPC框架相比的確麻煩了一些,例如Spring中就自帶了一些Exporter能夠無侵入的實現RPC服務。但熟悉了Protobuf和Thrift之後發現實際上仍是挺方便的,並且Windows版預編譯好的Protoc支持C++,Java,Python三種最經常使用的語言,Thrift則支持幾乎主流的各類語言,足夠咱們使用了。

參考資料:

  1. Protobuf語言指南
  2. Thrift入門及Java實例演示
相關文章
相關標籤/搜索