背景
學(xué)過java的都使用過RMI框架(remote method invocation),遠(yuǎn)程方法調(diào)用,比如A,B二個(gè)服務(wù)器锹引,A調(diào)用B服務(wù)器上的方法就像調(diào)用本地方法一樣,但是本質(zhì)上是跨機(jī)器的調(diào)用了辛蚊,A機(jī)器將調(diào)用的方法名粤蝎,參數(shù)通過字節(jié)碼的形式傳輸?shù)紹這臺(tái)機(jī)器上,B這臺(tái)機(jī)器將這些字節(jié)碼轉(zhuǎn)換成對B機(jī)器上具體方法的調(diào)用袋马,并將相應(yīng)的返回值序列化成二進(jìn)制數(shù)據(jù)傳輸?shù)紸服務(wù)器上。
RPC(Remote Procedure Call)其實(shí)和rmi及其類似秸应,RPC與RMI框架對比的優(yōu)勢就是好多RPC框架都是跨語言的虑凛。
RMI只針對java碑宴,A,B服務(wù)都使用java編寫桑谍。幾乎所有的RPC框架都存在代碼生成延柠,自動(dòng)代碼屏蔽了底層序列化通信等各種細(xì)節(jié)的處理,使得用戶(開發(fā)者)可以像調(diào)用本地方法一樣調(diào)用遠(yuǎn)程的方法锣披。一般這種自動(dòng)生成的代碼在客戶端我們稱為stub贞间,服務(wù)端我們稱為skeleton。
序列化與反序列化技術(shù)雹仿,也稱為編碼與解碼技術(shù)增热,比如我們本篇博客討論的Google Protobuf,和marshalling等技術(shù)胧辽。
從廣義上來講峻仇,webservice也可以稱為RPC框架,但是相比于其他的RPC框架來說邑商,webservice的性能稍微差點(diǎn)摄咆,因?yàn)闆Q定一個(gè)rpc性能的優(yōu)秀與否在于其底層對象編解碼性能。RPC一般都是基于socket協(xié)議傳輸?shù)娜硕希鴚ebservice基于http傳輸?shù)目源樱瑂ocket協(xié)議的性能也要高于http協(xié)議傳輸數(shù)據(jù)。所以恶迈,一般在公司內(nèi)部各個(gè)微服務(wù)之間的服務(wù)調(diào)用都使用RPC框架多一點(diǎn)涩金,因?yàn)樵谛阅苌系目紤],而我們總所周知的dubbo雖然也算是RPC框架蝉绷,但其實(shí)并不支持多語言鸭廷。
什么是protocol buffers?
Protocol buffers是谷歌的語言中立,平臺(tái)中立的熔吗,可擴(kuò)展機(jī)制的序列化數(shù)據(jù)結(jié)構(gòu)框架-可以看作是xml辆床,但是體積更小,傳輸速率更快桅狠,使用更加簡單讼载。一旦你定義了你的數(shù)據(jù)格式,你可以使用生成源代碼去輕松地從各種數(shù)據(jù)流讀和寫你的結(jié)構(gòu)化數(shù)據(jù)并且使用不同的語言中跌。protobuf有2.0版本和3.0版本咨堤,3.0版本十grpc框架的基礎(chǔ)
Protocol buffers目前支持Java, Python, Objective-C, 和C++生成代碼。新的proto3語言版本漩符,你可以使用Go, JavaNano, Ruby, 和 C#一喘。
為什么使用Protocol buffers
使用一個(gè)簡單的可以從一個(gè)文件中去讀寫人員聯(lián)系信息"地址簿"程序。每個(gè)在地址簿的人有姓名,id凸克,郵箱地址和一個(gè)聯(lián)系人電話號(hào)碼屬性议蟆。
你如何序列化和檢索這樣的結(jié)構(gòu)化數(shù)據(jù)? 有幾種方法來解決這個(gè)問題:
使用java原生的序列化萎战。這是一種默認(rèn)的方式因?yàn)槭莾?nèi)嵌于java語言的咐容,但是有一大堆眾所周知的問題(參考Effective Java這本書),并且你不能將數(shù)據(jù)分享于C++和Python應(yīng)用(也就是不能跨語言)蚂维。
還可以將數(shù)據(jù)項(xiàng)編碼為單個(gè)字符串的ad-hoc方式 - 例如將4個(gè)ints編碼為“12:3:-23:67”戳粒。 這是一個(gè)簡單而靈活的方法,盡管它需要編寫一次性編碼和解析代碼虫啥,并且解析具有很小的運(yùn)行時(shí)成本蔚约。 這最適合編碼非常簡單的數(shù)據(jù)。
將數(shù)據(jù)序列化為XML孝鹊。 這種方法可能非常有吸引力炊琉,因?yàn)閄ML是(可能的)人類可讀的,并且有很多語言的綁定庫又活。 如果您想與其他應(yīng)用程序/項(xiàng)目共享數(shù)據(jù)苔咪,這可能是一個(gè)很好的選擇。 然而柳骄,XML浪費(fèi)性能团赏,編碼/解碼可能會(huì)對應(yīng)用程序造成巨大的性能損失。 另外耐薯,檢索XML DOM樹比在一般類中簡單的字段檢索要復(fù)雜得多舔清。
Protocol buffers是靈活,高效曲初,自動(dòng)化的解決方案來解決這個(gè)問題体谒。 使用Protocol buffers,您可以編寫一個(gè).proto描述您希望存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)臼婆。 Protocol buffers編譯器創(chuàng)建一個(gè)實(shí)現(xiàn)自動(dòng)編碼和解析協(xié)議緩沖區(qū)數(shù)據(jù)的類抒痒,并使用高效的二進(jìn)制格式。 生成的類為組成Protocol buffers的字段提供getter和setter颁褂。
使用Protobuf編寫一個(gè)編碼解碼的最簡單程序
- 在 .proto結(jié)尾的文件中定義消息格式故响。
- 使用protocol buffers編譯器將 .proto結(jié)尾的文件生成對應(yīng)語言的源代碼(本demo使用java編譯器)。
- 使用Java protocol buffer API 去讀寫消息颁独。
定義一個(gè)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項(xiàng)目中彩届,除非你已經(jīng)明確指定了java_package
,否則package
用作Java的包名誓酒。即使您提供java_package
樟蠕,您仍然應(yīng)該定義一個(gè)package
,以避免在Protocol Buffers
名稱空間和非Java語言中的名稱沖突。
在package的定義之后坯墨,我們可以看到兩個(gè)定義的java選項(xiàng):java_package
和java_outer_classname
寂汇。java_package
指定您生成的類應(yīng)該存放的Java包名稱病往。 如果沒有明確指定它捣染,將會(huì)使用package定義的name作為包名,但這些名稱通常不是適合的Java包名稱(因?yàn)樗鼈兺ǔ2灰杂蛎_頭)停巷。 java_outer_classname
選項(xiàng)定義應(yīng)該包含此文件中所有類的類名耍攘。 如果你不明確地給出一個(gè)java_outer_classname
,它將通過將文件名轉(zhuǎn)換為駝峰的方式來生成畔勤。 例如蕾各,默認(rèn)情況下,“my_proto.proto”將使用“MyProto”作為外部類名稱庆揪。
每個(gè)元素上的“= 1”式曲,“= 2”標(biāo)記標(biāo)識(shí)字段在二進(jìn)制編碼中使用的唯一“標(biāo)簽”。你可以將經(jīng)常使用或者重復(fù)的字段標(biāo)注成1-15缸榛,因?yàn)樵谶M(jìn)行編碼的時(shí)候因?yàn)樯僖粋€(gè)字節(jié)進(jìn)行編碼吝羞,所以效率更高。
required
:必須提供該字段的值内颗,否則被認(rèn)為沒有初始化钧排。嘗試構(gòu)建一個(gè)未初始化的值被會(huì)拋出RuntimeException
。解析一個(gè)為初始化的消息會(huì)拋出IOException均澳。除此之外與optional一樣恨溜。
optional
:可以設(shè)置或不設(shè)置該字段。 如果未設(shè)置可選字段值找前,則使用默認(rèn)值糟袁。
repeated
:字段可能重復(fù)任意次數(shù)(包括零)。 重復(fù)值的順序?qū)⒈A粼?code>protocol buffer中躺盛。 將重復(fù)的字段視為動(dòng)態(tài)大小的數(shù)組项戴。(本列子中沒有字段定義成repeated類型,定義成repeated
類型其實(shí)就是java中List類型的字段颗品。
慎重使用required
類型肯尺,將required
類型的字段更改為optional
會(huì)有一些問題,而將optional
類型的字段更改為required
類型躯枢,則沒有問題则吟。
編譯
使用protocol buffers編譯器將對應(yīng)的.proto文件編譯成對應(yīng)的類
關(guān)于編譯器的安裝,下載地址:
修改環(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
進(jìn)入項(xiàng)目目錄锄蹂,執(zhí)行編譯語句如下:
? netty_lecture git:(master) ? protoc --java_out=src/main/java src/protobuf/Student.proto
--java_out
后面第一個(gè)參數(shù)指定代碼的路徑氓仲,具體的包名在.proto文件中的java_package指定了,第二個(gè)指定要編譯的proto文件。
自動(dòng)生成的類名是DataInfo(在java_outer_classname中指定了)敬扛,自動(dòng)生成的類太長晰洒,這邊就不列出來了。
編寫序列化反序列化測試類
package com.zhihao.miao.protobuf;
//實(shí)際使用protobuf序列化框架客戶端將對象轉(zhuǎn)譯成字節(jié)數(shù)組啥箭,然后通過協(xié)議傳輸?shù)椒?wù)器端谍珊,服務(wù)器端可以是其他的語言框架(比如說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();
//將對象轉(zhuǎn)譯成字節(jié)數(shù)組,序列化
byte[] student2ByteArray = student.toByteArray();
//將字節(jié)數(shù)組轉(zhuǎn)譯成對象,反序列化
DataInfo.Student student2 = DataInfo.Student.parseFrom(student2ByteArray);
System.out.println(student2.getName());
System.out.println(student2.getAge());
System.out.println(student2.getAddress());
}
}
執(zhí)行測試類,控制臺(tái)打蛹苯摹:
張三
20
北京
Google Protobuf與netty結(jié)合
protobuf做為序列化的一種方式砌滞,序列化之后通過什么樣的載體在網(wǎng)絡(luò)中傳輸?
使用netty使得經(jīng)過protobuf序列化的對象可以通過網(wǎng)絡(luò)通信進(jìn)行客戶端和服務(wù)器的信息通信坏怪”慈螅客戶端使用protobuf將對象序列化成字節(jié)碼,而服務(wù)器端通過protobuf將對象反序列化成原本對象铝宵。
寫一個(gè)使用Protobuf作為序列化框架打掘,netty作為傳輸層的最簡單的demo,需求描述:
- 客戶端傳遞一個(gè)User對象給服務(wù)端(User對象包括姓名鹏秋,年齡尊蚁,密碼)
- 客戶端接收客戶端的User對象并且將其相應(yīng)的銀行賬戶等信息反饋給客戶端
定義的.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編譯器進(jìn)行編譯,生成DataInfo對象拼岳,
服務(wù)器端代碼:
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();
}
}
}
服務(wù)端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序列化框架動(dòng)態(tài)的切割接收到的ByteBuf
pipeline.addLast(new ProtobufVarint32FrameDecoder());
//服務(wù)器端接收的是客戶端RequestUser對象,所以這邊將接收對象進(jìn)行解碼生產(chǎn)實(shí)列
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());
}
}
自定義服務(wù)端的處理器:
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序列化框架動(dòng)態(tài)的切割接收到的ByteBuf
pipeline.addLast(new ProtobufVarint32FrameDecoder());
//將接收到的二進(jìn)制文件解碼成具體的實(shí)例,這邊接收到的是服務(wù)端的ResponseBank對象實(shí)列
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);
}
}
運(yùn)行服務(wù)器端和客戶端耐版,服務(wù)器控制臺(tái)打屿艄弧:
七月 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
客戶端控制臺(tái)打印:
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é)
本節(jié)我們使用Google Protobuf
定義消息體格式粪牲,使用Netty
作為網(wǎng)絡(luò)傳輸層框架古瓤。其實(shí)大多數(shù)RPC框架底層實(shí)現(xiàn)都是使用序列化框架和NIO通信框架進(jìn)行結(jié)合。下面還會(huì)學(xué)習(xí)基于Protobuf 3.0協(xié)議的Grpc框架(Google基于Protobuf 3.0協(xié)議的一個(gè)跨語言的rpc框架腺阳,更加深入的去了解rpc框架)落君。