Netty筆記之七:Google Protobuf與Netty結合

背景

學過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_packagejava_outer_classnamejava_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框架)。

參考資料

官方網(wǎng)站
指南
java指南

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內(nèi)容