1. RPC
1.1 什么是 RPC 荠割?
RPC(Remote Procedure Call Protocol)遠程過程調用協議妹卿,目標就是讓遠程服務調用更加簡單旺矾、透明。
RPC 框架負責屏蔽底層的傳輸方式(TCP 或者 UDP)夺克、序列化方式(XML/Json/ 二進制)和通信細節(jié)箕宙,服務調用者可以像調用本地接口一樣調用遠程的服務提供者,而不需要關心底層通信細節(jié)和調用過程铺纽。
1.2 為什么要用 RPC 柬帕?
當我們的業(yè)務越來越多、應用也越來越多時狡门,自然的陷寝,我們會發(fā)現有些功能已經不能簡單劃分開來或者劃分不出來。
此時可以將公共業(yè)務邏輯抽離出來其馏,將之組成獨立的服務 Service 應用凤跑,而原有的、新增的應用都可以與那些獨立的 Service 應用 交互叛复,以此來完成完整的業(yè)務功能仔引。
所以我們急需一種高效的應用程序之間的通訊手段來完成這種需求,RPC 大顯身手的時候來了!
1.3 常用的 RPC 框架
- gRPC:一開始由 google 開發(fā),是一款語言中立伸蚯、平臺中立、開源的遠程過程調用(RPC)系統鲤看。
- Thrift:thrift 是一個軟件框架,用來進行可擴展且跨語言的服務的開發(fā)耍群。它結合了功能強大的軟件堆棧和代碼生成引擎义桂,以構建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 這些編程語言間無縫結合的、高效的服務蹈垢。
- Dubbo:Dubbo 是一個分布式服務框架慷吊,以及 SOA 治理方案,Dubbo自2011年開源后曹抬,已被許多非阿里系公司使用溉瓶。
- Spring Cloud:Spring Cloud 由眾多子項目組成,如 Spring Cloud Config谤民、Spring Cloud Netflix堰酿、Spring Cloud Consul 等,提供了搭建分布式系統及微服務常用的工具张足。
1.4 RPC 的調用流程
要讓網絡通信細節(jié)對使用者透明触创,我們需要對通信細節(jié)進行封裝,我們先看下一個 RPC 調用的流程涉及到哪些通信細節(jié):
- 服務消費方(client)調用以本地調用方式調用服務为牍;
- client stub接收到調用后負責將方法哼绑、參數等組裝成能夠進行網絡傳輸的消息體岩馍;
- client stub找到服務地址,并將消息發(fā)送到服務端抖韩;
- server stub收到消息后進行解碼蛀恩;
- server stub根據解碼結果調用本地的服務;
- 本地服務執(zhí)行并將結果返回給 server stub茂浮;
- server stub將返回結果打包成消息并發(fā)送至消費方双谆;
- client stub接收到消息,并進行解碼席揽;
- 服務消費方得到最終結果顽馋。
RPC 的目標就是要 2~8 這些步驟都封裝起來,讓用戶對這些細節(jié)透明驹尼,下面是網上的另外一幅圖趣避,感覺一目了然:
2. gRPC
2.1 什么是 gRPC 庞呕?
gRPC 是一個高性能新翎、通用的開源 RPC 框架,其由 Google 2015 年主要面向移動應用開發(fā)并基于 HTTP/2 協議標準而設計住练,基于 ProtoBuf 序列化協議開發(fā)地啰,且支持眾多開發(fā)語言。
由于是開源框架讲逛,通信的雙方可以進行二次開發(fā)亏吝,所以客戶端和服務器端之間的通信會更加專注于業(yè)務層面的內容,減少了對由 gRPC 框架實現的底層通信的關注盏混。
如下圖蔚鸥,DATA 部分即業(yè)務層面內容,下面所有的信息都由 gRPC 進行封裝许赃。
2.2 gRPC 的特點
- 跨語言使用止喷,支持 C++、Java混聊、Go弹谁、Python、Ruby句喜、C#预愤、Node.js、Android Java咳胃、Objective-C植康、PHP 等編程語言;
- 基于 IDL 文件定義服務展懈,通過 proto3 工具生成指定語言的數據結構向图、服務端接口以及客戶端 Stub泳秀;
- 通信協議基于標準的 HTTP/2 設計,支持雙向流榄攀、消息頭壓縮嗜傅、單 TCP 的多路復用、服務端推送等特性檩赢,這些特性使得 gRPC 在移動端設備上更加省電和節(jié)省網絡流量吕嘀;
- 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一種語言無關的高性能序列化框架贞瞒,基于 HTTP/2 + PB, 保障了 RPC 調用的高性能偶房;
- 安裝簡單,擴展方便(用該框架每秒可達到百萬個RPC)军浆。
2.3 gRPC 交互過程
- 交換機在開啟 gRPC 功能后充當 gRPC 客戶端的角色棕洋,采集服務器充當 gRPC 服務器角色;
- 交換機會根據訂閱的事件構建對應數據的格式(GPB/JSON)乒融,通過 Protocol Buffers 進行編寫 proto 文件掰盘,交換機與服務器建立 gRPC 通道,通過 gRPC 協議向服務器發(fā)送請求消息赞季;
- 服務器收到請求消息后愧捕,服務器會通過 Protocol Buffers 解譯 proto 文件,還原出最先定義好格式的數據結構申钩,進行業(yè)務處理次绘;
- 數據處理完后,服務器需要使用 Protocol Buffers 重編譯應答數據撒遣,通過 gRPC 協議向交換機發(fā)送應答消息邮偎;
- 交換機收到應答消息后,結束本次的 gRPC 交互义黎。
簡單地說禾进,gRPC 就是在客戶端和服務器端開啟 gRPC 功能后建立連接,將設備上配置的訂閱數據推送給服務器端轩缤。
我們可以看到整個過程是需要用到 Protocol Buffers 將所需要處理數據的結構化數據在 proto 文件中進行定義命迈。
2.4 Protocol Buffers
你可以理解 ProtoBuf 是一種更加靈活、高效的數據格式火的,與 XML壶愤、JSON 類似,在一些高性能且對響應速度有要求的數據傳輸場景非常適用馏鹤。
ProtoBuf 在 gRPC 的框架中主要有三個作用:定義數據結構征椒、定義服務接口,通過序列化和反序列化方式提升傳輸效率湃累。
為什么 ProtoBuf 會提高傳輸效率呢勃救?
我們知道使用 XML碍讨、JSON 進行數據編譯時,數據文本格式更容易閱讀蒙秒,但進行數據交換時勃黍,設備就需要耗費大量的 CPU 在 I/O 動作上,自然會影響整個傳輸速率晕讲。
Protocol Buffers 不像前者覆获,它會將字符串進行序列化后再進行傳輸,即二進制數據瓢省。
可以看到其實兩者內容相差不大弄息,并且內容非常直觀,但是 Protocol Buffers 編碼的內容只是提供給操作者閱讀的勤婚,實際上傳輸的并不會以這種文本形式摹量,而是序列化后的二進制數據,字節(jié)數會比 JSON馒胆、XML 的字節(jié)數少很多缨称,速率更快。
gPRC 如何支撐跨平臺国章,多語言呢 具钥?
Protocol Buffers 自帶一個編譯器也是一個優(yōu)勢點豆村,前面提到的 proto 文件就是通過編譯器進行編譯的液兽,proto 文件需要編譯生成一個類似庫文件,基于庫文件才能真正開發(fā)數據應用掌动。
具體用什么編程語言編譯生成這個庫文件呢四啰?由于現網中負責網絡設備和服務器設備的運維人員往往不是同一組人,運維人員可能會習慣使用不同的編程語言進行運維開發(fā)粗恢,那么 Protocol Buffers 其中一個優(yōu)勢就能發(fā)揮出來——跨語言柑晒。
從上面的介紹,我們得出在編碼方面 Protocol Buffers 對比 JSON眷射、XML 的優(yōu)點:
- 標準的 IDL 和 IDL 編譯器匙赞,這使得其對工程師非常友好;
- 序列化數據非常簡潔妖碉,緊湊涌庭,與 XML 相比,其序列化之后的數據量約為 1/3 到 1/10欧宜;
- 解析速度非匙埽快,比對應的 XML 快約 20-100 倍冗茸;
- 提供了非常友好的動態(tài)庫席镀,使用非常簡單匹中,反序列化只需要一行代碼。
Protobuf 也有其局限性:
- 由于 Protobuf 產生于 Google豪诲,所以目前其僅支持 Java顶捷、C++、Python 三種語言屎篱;
- Protobuf 支持的數據類型相對較少焊切,不支持常量類型;
- 由于其設計的理念是純粹的展現層協議(Presentation Layer)芳室,目前并沒有一個專門支持 Protobuf 的 RPC 框架专肪。
Protobuf 適用場景:
- Protobuf 具有廣泛的用戶基礎,空間開銷小以及高解析性能是其亮點堪侯,非常適合于公司內部的對性能要求高的 RPC 調用嚎尤;
- 由于 Protobuf 提供了標準的 IDL 以及對應的編譯器,其 IDL 文件是參與各方的非常強的業(yè)務約束伍宦;
- Protobuf 與傳輸層無關芽死,采用 HTTP 具有良好的跨防火墻的訪問屬性,所以 Protobuf 也適用于公司間對性能要求比較高的場景次洼;
- 由于其解析性能高关贵,序列化后數據量相對少,非常適合應用層對象的持久化場景卖毁;
- 主要問題在于其所支持的語言相對較少揖曾,另外由于沒有綁定的標準底層傳輸層協議,在公司間進行傳輸層協議的調試工作相對麻煩亥啦。
2.5 基于 HTTP 2.0 標準設計
除了 Protocol Buffers 之外炭剪,從交互圖中和分層框架可以看到, gRPC 還有另外一個優(yōu)勢——它是基于 HTTP 2.0 協議的翔脱。
由于 gRPC 基于 HTTP 2.0 標準設計奴拦,帶來了更多強大功能,如多路復用届吁、二進制幀错妖、頭部壓縮、推送機制疚沐。
這些功能給設備帶來重大益處暂氯,如節(jié)省帶寬、降低 TCP 連接次數濒旦、節(jié)省 CPU 使用等株旷,gRPC 既能夠在客戶端應用,也能夠在服務器端應用,從而以透明的方式實現兩端的通信和簡化通信系統的構建晾剖。
HTTP 1.X 定義了四種與服務器交互的方式锉矢,分別為 GET、POST齿尽、PUT沽损、DELETE,這些在 HTTP 2.0 中均保留循头,我們看看 HTTP 2.0 的新特性:雙向流绵估、多路復用、二進制幀卡骂、頭部壓縮国裳。
2.6 性能對比
與采用文本格式的 JSON 相比,采用二進制格式的 protobuf 在速度上可以達到前者的 5 倍全跨!
Auth0 網站所做的性能測試結果顯示缝左,protobuf 和 JSON 的優(yōu)勢差異在 Java、Python 等環(huán)境中尤為明顯浓若,下圖是 Auth0 在兩個 Spring Boot 應用程序間所做的對比測試結果渺杉。
結果顯示,protobuf 所需的請求時間最多只有 JSON 的 20% 左右挪钓,即速度是其 5 倍!
下面看一下性能和空間開銷對比是越。
從上圖可得出如下結論:
- XML序列化(Xstream)無論在性能和簡潔性上比較差。
- Thrift 與 Protobuf 相比在時空開銷方面都有一定的劣勢碌上。
- Protobuf 和 Avro 在兩方面表現都非常優(yōu)越倚评。
3. gRPC 實戰(zhàn)
3.1 項目結構
我們先看一下項目結構:
3.2 生成 protobuf 文件
文件 helloworld.proto:
syntax = "proto3";option java_multiple_files = true;option java_package = "io.grpc.examples.helloworld";option java_outer_classname = "HelloWorldProto";option objc_class_prefix = "HLW";package helloworld;// The greeting service definition.service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {}}// The request message containing the user's name.message HelloRequest { string name = 1;}// The response message containing the greetingsmessage HelloReply { string message = 1;}
這里提供了一個 SayHello() 方法,然后入參為 HelloRequest绍赛,返回值為 HelloReply蔓纠,可以看到 proto 文件只定義了入參和返回值的格式辑畦,以及調用的接口吗蚌,至于接口內部的實現,該文件完全不用關心纯出。
文件 pom.xml:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>rpc-study</artifactId> <groupId>org.example</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>grpc-demo</artifactId> <dependencies> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty-shaded</artifactId> <version>1.14.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>1.14.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>1.14.0</version> </dependency> </dependencies> <build> <extensions> <extension> <groupId>kr.motd.maven</groupId> <artifactId>os-maven-plugin</artifactId> <version>1.5.0.Final</version> </extension> </extensions> <plugins> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>0.5.1</version> <configuration> <protocArtifact>com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.14.0:exe:${os.detected.classifier}</pluginArtifact> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>6</source> <target>6</target> </configuration> </plugin> </plugins> </build></project>
這里面的 build 其實是為了安裝 protobuf 插件蚯妇,里面其實有 2 個插件我們需要用到,分別為 protobuf:compile 和 protobuf:compile-javanano暂筝,當我們直接執(zhí)行時箩言,會生成左側文件,其中 GreeterGrpc 提供調用接口焕襟,Hello 開頭的文件功能主要是對數據進行序列化陨收,然后處理入參和返回值。
可能有同學會問,你把文件生成到 target 中务漩,我想放到 main.src 中拄衰,你可以把這些文件 copy 出來,或者也可以通過工具生成:
下載 protoc.exe 工具 饵骨,下載地址:
https://github.com/protocolbuffers/protobuf/releases下載 protoc-gen-grpc 插件, 下載地址:
http://jcenter.bintray.com/io/grpc/protoc-gen-grpc-java/
3.3 服務端和客戶端
文件 HelloWorldClient.java:
public class HelloWorldClient { private final ManagedChannel channel; private final GreeterGrpc.GreeterBlockingStub blockingStub; private static final Logger logger = Logger.getLogger(HelloWorldClient.class.getName()); public HelloWorldClient(String host,int port){ channel = ManagedChannelBuilder.forAddress(host,port) .usePlaintext(true) .build(); blockingStub = GreeterGrpc.newBlockingStub(channel); } public void shutdown() throws InterruptedException { channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); } public void greet(String name){ HelloRequest request = HelloRequest.newBuilder().setName(name).build(); HelloReply response; try{ response = blockingStub.sayHello(request); } catch (StatusRuntimeException e) { logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus()); return; } logger.info("Message from gRPC-Server: "+response.getMessage()); } public static void main(String[] args) throws InterruptedException { HelloWorldClient client = new HelloWorldClient("127.0.0.1",50051); try{ String user = "world"; if (args.length > 0){ user = args[0]; } client.greet(user); }finally { client.shutdown(); } }}
這個太簡單了翘悉,就是連接服務端口,調用 sayHello() 方法居触。
文件 HelloWorldServer.java:
public class HelloWorldServer { private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName()); private int port = 50051; private Server server; private void start() throws IOException { server = ServerBuilder.forPort(port) .addService(new GreeterImpl()) .build() .start(); logger.info("Server started, listening on " + port); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { System.err.println("*** shutting down gRPC server since JVM is shutting down"); HelloWorldServer.this.stop(); System.err.println("*** server shut down"); } }); } private void stop() { if (server != null) { server.shutdown(); } } // block 一直到退出程序 private void blockUntilShutdown() throws InterruptedException { if (server != null) { server.awaitTermination(); } } public static void main(String[] args) throws IOException, InterruptedException { final HelloWorldServer server = new HelloWorldServer(); server.start(); server.blockUntilShutdown(); } // 實現 定義一個實現服務接口的類 private class GreeterImpl extends GreeterGrpc.GreeterImplBase { @Override public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) { HelloReply reply = HelloReply.newBuilder().setMessage(("Hello " + req.getName())).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); System.out.println("Message from gRPC-Client:" + req.getName()); System.out.println("Message Response:" + reply.getMessage()); } }}
主要是實現 sayHello() 方法妖混,里面對數據進行了簡單處理,入參為 “W orld”轮洋,返回的是 “Hello World”制市。
3.4 啟動服務
先啟動 Server,返回如下:
再啟動 Client弊予,返回如下:
同時 Server返回如下:
3.5 項目代碼
4. 寫在最后
這篇文章詳細講解了 RPC 和 gRPC息堂,以及 gRPC 的應用示例,非常全面块促,后面會再把 Thrift 整理出來荣堰。
這個 Demo 看起來很簡單,我 TM 居然搞了大半天竭翠,一開始是因為不知道需要執(zhí)行 2 個不同的插件來生成 protobuf振坚,以為只需要點擊 protobuf:compile 就可以,結果發(fā)現 protobuf:compile-javanano 也需要點一下斋扰。
還有就是我自己喜歡作渡八,感覺通過插件生成 protobuf 不完美,我想通過自己下載的插件传货,手動生成 protobuf 文件屎鳍,結果手動生成的沒有搞定,自動生成的方式也不可用问裕,搞了半天才發(fā)現是緩存的問題逮壁,最后直接執(zhí)行 “Invalidate Caches / Restart” 才搞定。
應征了一句話“no zuo no die”粮宛,不過這個過程還是需要經歷的窥淆。