背景
學過java的都使用過RMI框架(remote method invocation),遠程方法調(diào)用,比如A,B二個服務器,A調(diào)用B服務器上的方法就像調(diào)用本地方法一樣,但是本質(zhì)上是跨機器的調(diào)用了,A機器將調(diào)用的方法名,參數(shù)通過字節(jié)碼的形式傳輸?shù)紹這臺機器上,B這臺機器將這些字節(jié)碼轉換成對B機器上具體方法的調(diào)用,并將相應的返回值序列化成二進制數(shù)據(jù)傳輸?shù)紸服務器上。
RPC(Remote Procedure Call)其實和rmi及其類似,RPC與RMI框架對比的優(yōu)勢就是好多RPC框架都是跨語言的。
RMI只針對java,A,B服務都使用java編寫。幾乎所有的RPC框架都存在代碼生成,自動代碼屏蔽了底層序列化通信等各種細節(jié)的處理,使得用戶(開發(fā)者)可以像調(diào)用本地方法一樣調(diào)用遠程的方法。一般這種自動生成的代碼在客戶端我們稱為stub,服務端我們稱為skeleton。
序列化與反序列化技術,也稱為編碼與解碼技術,比如我們本篇博客討論的Google Protobuf,和marshalling等技術。
從廣義上來講,webservice也可以稱為RPC框架,但是相比于其他的RPC框架來說,webservice的性能稍微差點,因為決定一個rpc性能的優(yōu)秀與否在于其底層對象編解碼性能。RPC一般都是基于socket協(xié)議傳輸?shù)模鴚ebservice基于http傳輸?shù)模瑂ocket協(xié)議的性能也要高于http協(xié)議傳輸數(shù)據(jù)。所以,一般在公司內(nèi)部各個微服務之間的服務調(diào)用都使用RPC框架多一點,因為在性能上的考慮,而我們總所周知的dubbo雖然也算是RPC框架,但其實并不支持多語言。
什么是protocol buffers?
Protocol buffers是谷歌的語言中立,平臺中立的,可擴展機制的序列化數(shù)據(jù)結構框架-可以看作是xml,但是體積更小,傳輸速率更快,使用更加簡單。一旦你定義了你的數(shù)據(jù)格式,你可以使用生成源代碼去輕松地從各種數(shù)據(jù)流讀和寫你的結構化數(shù)據(jù)并且使用不同的語言。protobuf有2.0版本和3.0版本,3.0版本十grpc框架的基礎
Protocol buffers目前支持Java, Python, Objective-C, 和C++生成代碼。新的proto3語言版本,你可以使用Go, JavaNano, Ruby, 和 C#。
為什么使用Protocol buffers
使用一個簡單的可以從一個文件中去讀寫人員聯(lián)系信息"地址簿"程序。每個在地址簿的人有姓名,id,郵箱地址和一個聯(lián)系人電話號碼屬性。
你如何序列化和檢索這樣的結構化數(shù)據(jù)? 有幾種方法來解決這個問題:
使用java原生的序列化。這是一種默認的方式因為是內(nèi)嵌于java語言的,但是有一大堆眾所周知的問題(參考Effective Java這本書),并且你不能將數(shù)據(jù)分享于C++和Python應用(也就是不能跨語言)。
還可以將數(shù)據(jù)項編碼為單個字符串的ad-hoc方式 - 例如將4個ints編碼為“12:3:-23:67”。 這是一個簡單而靈活的方法,盡管它需要編寫一次性編碼和解析代碼,并且解析具有很小的運行時成本。 這最適合編碼非常簡單的數(shù)據(jù)。
將數(shù)據(jù)序列化為XML。 這種方法可能非常有吸引力,因為XML是(可能的)人類可讀的,并且有很多語言的綁定庫。 如果您想與其他應用程序/項目共享數(shù)據(jù),這可能是一個很好的選擇。 然而,XML浪費性能,編碼/解碼可能會對應用程序造成巨大的性能損失。 另外,檢索XML DOM樹比在一般類中簡單的字段檢索要復雜得多。
Protocol buffers是靈活,高效,自動化的解決方案來解決這個問題。 使用Protocol buffers,您可以編寫一個.proto描述您希望存儲的數(shù)據(jù)結構。 Protocol buffers編譯器創(chuàng)建一個實現(xiàn)自動編碼和解析協(xié)議緩沖區(qū)數(shù)據(jù)的類,并使用高效的二進制格式。 生成的類為組成Protocol buffers的字段提供getter和setter。
使用Protobuf編寫一個編碼解碼的最簡單程序
- 在 .proto結尾的文件中定義消息格式。
- 使用protocol buffers編譯器將 .proto結尾的文件生成對應語言的源代碼(本demo使用java編譯器)。
- 使用Java protocol buffer API 去讀寫消息。
定義一個Student.proto文件
syntax ="proto2";
package com.zhihao.miao.protobuf;
//optimize_for 加快解析的速度
option optimize_for = SPEED;
option java_package = "com.zhihao.miao.protobuf";
option java_outer_classname="DataInfo";
message Student{
required string name = 1;
optional int32 age = 2;
optional string address = 3;
}
在Java項目中,除非你已經(jīng)明確指定了java_package
,否則package
用作Java的包名。即使您提供java_package
,您仍然應該定義一個package
,以避免在Protocol Buffers
名稱空間和非Java語言中的名稱沖突。
在package的定義之后,我們可以看到兩個定義的java選項:java_package
和java_outer_classname
。java_package
指定您生成的類應該存放的Java包名稱。 如果沒有明確指定它,將會使用package定義的name作為包名,但這些名稱通常不是適合的Java包名稱(因為它們通常不以域名開頭)。 java_outer_classname
選項定義應該包含此文件中所有類的類名。 如果你不明確地給出一個java_outer_classname
,它將通過將文件名轉換為駝峰的方式來生成。 例如,默認情況下,“my_proto.proto”將使用“MyProto”作為外部類名稱。
每個元素上的“= 1”,“= 2”標記標識字段在二進制編碼中使用的唯一“標簽”。你可以將經(jīng)常使用或者重復的字段標注成1-15,因為在進行編碼的時候因為少一個字節(jié)進行編碼,所以效率更高。
required
:必須提供該字段的值,否則被認為沒有初始化。嘗試構建一個未初始化的值被會拋出RuntimeException
。解析一個為初始化的消息會拋出IOException。除此之外與optional一樣。
optional
:可以設置或不設置該字段。 如果未設置可選字段值,則使用默認值。
repeated
:字段可能重復任意次數(shù)(包括零)。 重復值的順序將保留在protocol buffer
中。 將重復的字段視為動態(tài)大小的數(shù)組。(本列子中沒有字段定義成repeated類型,定義成repeated
類型其實就是java中List類型的字段。
慎重使用required
類型,將required
類型的字段更改為optional
會有一些問題,而將optional
類型的字段更改為required
類型,則沒有問題。
編譯
使用protocol buffers編譯器將對應的.proto文件編譯成對應的類
關于編譯器的安裝,下載地址:
修改環(huán)境變量
? vim .bash_profile
export PATH=/Users/naeshihiroshi/software/work/protoc-3.3.0-osx-x86_64/bin
? source .bash_profile
? which protoc
/Users/naeshihiroshi/software/work/protoc-3.3.0-osx-x86_64/bin/protoc
進入項目目錄,執(zhí)行編譯語句如下:
? netty_lecture git:(master) ? protoc --java_out=src/main/java src/protobuf/Student.proto
--java_out
后面第一個參數(shù)指定代碼的路徑,具體的包名在.proto文件中的java_package指定了,第二個指定要編譯的proto文件。
自動生成的類名是DataInfo(在java_outer_classname中指定了),自動生成的類太長,這邊就不列出來了。
編寫序列化反序列化測試類
package com.zhihao.miao.protobuf;
//實際使用protobuf序列化框架客戶端將對象轉譯成字節(jié)數(shù)組,然后通過協(xié)議傳輸?shù)椒掌鞫耍掌鞫丝梢允瞧渌恼Z言框架(比如說python)將
//字節(jié)對象反編譯成java對象
public class ProtobuffTest {
public static void main(String[] args) throws Exception{
DataInfo.Student student = DataInfo.Student.newBuilder().
setName("張三").setAge(20).setAddress("北京").build();
//將對象轉譯成字節(jié)數(shù)組,序列化
byte[] student2ByteArray = student.toByteArray();
//將字節(jié)數(shù)組轉譯成對象,反序列化
DataInfo.Student student2 = DataInfo.Student.parseFrom(student2ByteArray);
System.out.println(student2.getName());
System.out.println(student2.getAge());
System.out.println(student2.getAddress());
}
}
執(zhí)行測試類,控制臺打印:
張三
20
北京
Google Protobuf與netty結合
protobuf做為序列化的一種方式,序列化之后通過什么樣的載體在網(wǎng)絡中傳輸?
使用netty使得經(jīng)過protobuf序列化的對象可以通過網(wǎng)絡通信進行客戶端和服務器的信息通信。客戶端使用protobuf將對象序列化成字節(jié)碼,而服務器端通過protobuf將對象反序列化成原本對象。
寫一個使用Protobuf作為序列化框架,netty作為傳輸層的最簡單的demo,需求描述:
- 客戶端傳遞一個User對象給服務端(User對象包括姓名,年齡,密碼)
- 客戶端接收客戶端的User對象并且將其相應的銀行賬戶等信息反饋給客戶端
定義的.proto文件如下:
syntax ="proto2";
package com.zhihao.miao.netty.sixthexample;
option optimize_for = SPEED;
option java_package = "com.zhihao.miao.test.day06";
option java_outer_classname="DataInfo";
message RequestUser{
optional string user_name = 1;
optional int32 age = 2;
optional string password = 3;
}
message ResponseBank{
optional string bank_no = 1;
optional double money = 2;
optional string bank_name=3;
}
使用Protobuf編譯器進行編譯,生成DataInfo對象,
服務器端代碼:
package com.zhihao.miao.test.day06;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class ProtoServer {
public static void main(String[] args) throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup wokerGroup = new NioEventLoopGroup();
try{
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,wokerGroup).channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ProtoServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
wokerGroup.shutdownGracefully();
}
}
}
服務端ProtoServerInitializer(初始化連接):
package com.zhihao.miao.test.day06;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
public class ProtoServerInitializer extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//解碼器,通過Google Protocol Buffers序列化框架動態(tài)的切割接收到的ByteBuf
pipeline.addLast(new ProtobufVarint32FrameDecoder());
//服務器端接收的是客戶端RequestUser對象,所以這邊將接收對象進行解碼生產(chǎn)實列
pipeline.addLast(new ProtobufDecoder(DataInfo.RequestUser.getDefaultInstance()));
//Google Protocol Buffers編碼器
pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
//Google Protocol Buffers編碼器
pipeline.addLast(new ProtobufEncoder());
pipeline.addLast(new ProtoServerHandler());
}
}
自定義服務端的處理器:
package com.zhihao.miao.test.day06;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class ProtoServerHandler extends SimpleChannelInboundHandler<DataInfo.RequestUser> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DataInfo.RequestUser msg) throws Exception {
System.out.println(msg.getUserName());
System.out.println(msg.getAge());
System.out.println(msg.getPassword());
DataInfo.ResponseBank bank = DataInfo.ResponseBank.newBuilder().setBankName("中國工商銀行")
.setBankNo("6222222200000000000").setMoney(560000.23).build();
ctx.channel().writeAndFlush(bank);
}
}
客戶端:
package com.zhihao.miao.test.day06;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
public class ProtoClient {
public static void main(String[] args) throws Exception{
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try{
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
.handler(new ProtoClientInitializer());
ChannelFuture channelFuture = bootstrap.connect("localhost",8899).sync();
channelFuture.channel().closeFuture().sync();
}finally {
eventLoopGroup.shutdownGracefully();
}
}
}
客戶端初始化連接(ProtoClientInitializer),
package com.zhihao.miao.test.day06;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
public class ProtoClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//解碼器,通過Google Protocol Buffers序列化框架動態(tài)的切割接收到的ByteBuf
pipeline.addLast(new ProtobufVarint32FrameDecoder());
//將接收到的二進制文件解碼成具體的實例,這邊接收到的是服務端的ResponseBank對象實列
pipeline.addLast(new ProtobufDecoder(DataInfo.ResponseBank.getDefaultInstance()));
//Google Protocol Buffers編碼器
pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
//Google Protocol Buffers編碼器
pipeline.addLast(new ProtobufEncoder());
pipeline.addLast(new ProtoClientHandler());
}
}
自定義客戶端處理器:
package com.zhihao.miao.test.day06;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class ProtoClientHandler extends SimpleChannelInboundHandler<DataInfo.ResponseBank> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DataInfo.ResponseBank msg) throws Exception {
System.out.println(msg.getBankNo());
System.out.println(msg.getBankName());
System.out.println(msg.getMoney());
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
DataInfo.RequestUser user = DataInfo.RequestUser.newBuilder()
.setUserName("zhihao.miao").setAge(27).setPassword("123456").build();
ctx.channel().writeAndFlush(user);
}
}
運行服務器端和客戶端,服務器控制臺打印:
七月 03, 2017 11:12:03 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xa1a63b58, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0x08c534f3, L:/127.0.0.1:8899 - R:/127.0.0.1:65448]
七月 03, 2017 11:12:03 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xa1a63b58, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
zhihao.miao
27
123456
客戶端控制臺打印:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
6222222200000000000
中國工商銀行
560000.23
總結
本節(jié)我們使用Google Protobuf
定義消息體格式,使用Netty
作為網(wǎng)絡傳輸層框架。其實大多數(shù)RPC框架底層實現(xiàn)都是使用序列化框架和NIO通信框架進行結合。下面還會學習基于Protobuf 3.0協(xié)議的Grpc框架(Google基于Protobuf 3.0協(xié)議的一個跨語言的rpc框架,更加深入的去了解rpc框架)。