protobuf在java应用中通过反射动态创建对象
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
---恢復(fù)內(nèi)容開始---
最近編寫一個游戲用到protobuf數(shù)據(jù)格式進(jìn)行前后臺傳輸,苦于protobuf接受客戶端的數(shù)據(jù)時是需要數(shù)據(jù)類型的如xxx.parseForm(...),這樣就要求服務(wù)器在接受客戶端請求時必須知道客戶端傳遞的數(shù)據(jù)類型。由于客戶端的請求數(shù)據(jù)是多種多樣的,服務(wù)器端又不知道客戶端的請求到底是哪個類型,這樣就使得服務(wù)器端編程帶來很多麻煩,甚至寸步難行。難道就沒有解決辦法了嗎,答案當(dāng)然是有的。下面就說一下常用的方法。(在看本文之前建議先了解protobuf的一些基本語法,和基本用法)
1.第一種方法也是最簡單的方法,就是在整個應(yīng)用程序中只定義一個proto文件,那么所有的請求都是一種類型,那么服務(wù)器端就不用苦惱怎么解析請求數(shù)據(jù)了,因?yàn)椴还苣膫€請求數(shù)據(jù)都用同一個對象解析。如下面的列子:
首先貼一個PBMessage.proto文件
//客戶端請求以及服務(wù)端響應(yīng)數(shù)據(jù)協(xié)議 option java_outer_classname = "PBMessageProto";
package com.ppsea.message; import "main/resources/message/DataMsg.proto";
message PBMessage{ optional int32 playerId = 1; //玩家id required int32 actionCode = 2; //操作碼id optional bytes data = 5; //提交或響應(yīng)的數(shù)據(jù) optional DataMsg dataMsg = 6; //服務(wù)器端推送數(shù)據(jù) optional string sessionKey = 7; //請求的校驗(yàn)碼 optional int32 sessionId = 8;//當(dāng)前請求的標(biāo)示 } 如上述代碼,整個應(yīng)用都基于PBMessage.proto傳輸,注意到protobuf 語法中 optional修飾符,他表示這個字段是非必須的,也就是說對于客戶端的不同請求,只需要為它填充其請求時用到的字段的值即可,其他的字段的值就不用管了,這樣就可以模擬出各種請求來,那么接下來我們就用: PBMessage.parseForm(byte_PBMesage) // byte_PBMesage表示客戶端請求數(shù)據(jù) ,這樣請求的解析就完成了。同時我們注意到: (required int32 actionCode = 2; // 操作碼id) ,required表示該字段是必須的,前面請求已經(jīng)解析好了,在這里我們拿到 actionCode 就可以知道我們該用哪個Action事件來處理該請求了(前提是必須維護(hù)一張actionCode到Action的映射關(guān)系表:Map<int,Action>),至此整個請求的解析和處理都完成了。
接下來說一下第一種方式的優(yōu)缺點(diǎn),優(yōu)點(diǎn):整個應(yīng)用消息格式一致統(tǒng)一,操作簡單。缺點(diǎn):太統(tǒng)一,就不靈活,對于請求很少,消息格式很少的小型應(yīng)用倒還勉強(qiáng)能用,消息格式多的話再用這種方式就顯得臃腫,不便于管理,失去了程序設(shè)計(jì)的意義。
2.第二種方法,也是我本次用到的方法。苦于提議中方式的局限性,本人通過在網(wǎng)上收集資料以及查看protobuf java版的源碼,發(fā)現(xiàn)了一個折中的方式。首先我們看下protobuf源碼中提供的, DynamicMessage 類(顧名思義 動態(tài)消息類,眼前一亮有木有),它繼承了AbstractMessage類,比較一下和第一種方式創(chuàng)建的PBMessage類的區(qū)別 我們發(fā)現(xiàn)PBMessage類繼承了GeneratedMessage類,而GeneratedMessage類繼承了AbstractMessage類,至此我們發(fā)現(xiàn)了共同類AbstractMessage,再次證明了DynamicMessage 管用,同時我們再看看AbstractParser<MessageType>類(在PBMessage類中持有AbstractParser類的對象,并用其來解析請求數(shù)據(jù)),它繼承了Parser<MessageType>接口,看看其部分方法:
public abstract MessageType parseFrom(byte[] paramArrayOfByte) throws InvalidProtocolBufferException;
public abstract MessageType parseFrom(InputStream paramInputStream) throws InvalidProtocolBufferException;
public abstract MessageType parseFrom(InputStream paramInputStream,ExtensionRegistryLite paramExtensionRegistryLite) throws InvalidProtocolBufferException; ,再看看DynamicMessage 里面提供的方法:
public static DynamicMessage parseFrom(Descriptors.Descriptor type,byte[] data) throws InvalidProtocolBufferException {
return ((Builder) newBuilder(type).mergeFrom(data)).buildParsed();}
public static DynamicMessage parseFrom(Descriptors.Descriptor type,byte[] data, ExtensionRegistry extensionRegistry)throws InvalidProtocolBufferException { return ((Builder) newBuilder(type).mergeFrom(data, extensionRegistry)).buildParsed(); }
public static DynamicMessage parseFrom(Descriptors.Descriptor type,InputStream input) throws IOException {
return ((Builder) newBuilder(type).mergeFrom(input)).buildParsed();}
public static DynamicMessage parseFrom(Descriptors.Descriptor type,InputStream input, ExtensionRegistry extensionRegistry)throws IOException { return ((Builder) newBuilder(type).mergeFrom(input, extensionRegistry)).buildParsed(); }
發(fā)現(xiàn)了他們方法的相似點(diǎn),在這里我們可以用一個等量關(guān)系比喻:DynamicMessage=AbstractMessage+ AbstractParser=PBMessage,也就是說DynamicMessage繼承AbstractMessage(請求消息對象)的同時又間接實(shí)現(xiàn)了AbstractParser(請求消息數(shù)據(jù)解析)對數(shù)據(jù)解析的功能?,F(xiàn)在我們唯一缺少的就是 Descriptors.Descriptor(對消息的描述)對象,這個對象該怎么拿到呢,在這里肯定的說,對于不同的.proto請求這里的Descriptors.Descriptor是不一樣的。在這里我們又回到PBMessage對象中,我們發(fā)現(xiàn)了其中有這樣一個方法:
public final class PBMessageProto {
..................//此處省略若干行public static final class PBMessage extendscom.google.protobuf.GeneratedMessage implements PBMessageOrBuilder {...........//此處省略若干行 public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { return com.ppsea.message.PBMessageProto.internal_static_com_ppsea_message_PBMessage_descriptor; } } } 這不就是我們苦苦尋找的東西嗎,通過這個方法就可以拿到Descriptor了,不是嗎。在這里重點(diǎn)來了,再來理解一下,首先有了 PBMessage對象(這里用其來做代表,可以使其他的.proto對象)就可以獲得 Descriptors.Descriptor 對象,有了Descriptors.Descriptor對象就可以創(chuàng)建DynamicMessage對象了,有了DynamicMessage就可以解析對應(yīng)請求了。下面看代碼:
//存放消息操作碼和消息對象 Map<Integer,Descriptor> descriptorMap=new Map<Integer,Descriptor>; //把消息描述對象添加進(jìn)來 descriptorMap.add(100,PBMessage.getDescriptor()); descriptorMap.add(xxx,xxx); 這樣Descriptor有了,其實(shí)還可以做得更好一點(diǎn),通過反射機(jī)制,Map里面只存放操作碼和對應(yīng)的proto對象類名,再通過反射方式創(chuàng)建proto對象在獲得其getDescriptor()方法。這樣就可以在配置文件中配置操作碼和proto對象的關(guān)系了。
好接下來我們來接受客戶端的請求試一下,
//客戶端偽代碼 client.send(byte_PBMessage); 然后服務(wù)器接受請求并解析,
//服務(wù)器偽代碼
byte[] date=server.accept();
//客戶端操作碼 int actionCode;
Descriptor descriptor=descriptorMap.get(actionCode);
//解析請求 DynamicMessage req=DynamicMessage.parseFrom(descriptor, date); 在這里我們發(fā)現(xiàn)似乎還少了點(diǎn)什么,好像 actionCode還不知道,怎么辦呢,好吧,我們在客戶端發(fā)送的請求消息頭上再加上個actionCode,即把操作碼和proto消息合并為一個新的請求發(fā)送給客戶端,請求2位為操作碼,那么現(xiàn)在客戶端應(yīng)該這么發(fā)送消息了:
//客戶端偽代碼 short actionCode=100;
//兩個字節(jié)來存放actionCode byte [] actionCodeByte=new byte [2];
// 轉(zhuǎn)換成字節(jié)流 actionCodeByte.set(actionCode.toByteArray());//偽代碼,請勿當(dāng)真
//帶請求頭的消息的總長度 int length=actionCodeByte.length+byte_PBMEssage.length;
byte [] messageByte=new byte[length];
//把操作碼和proto消息合并 messageByte=actionCodeByte+byte_PBMEssage;
client.send(messageByte); 下來是服務(wù)器了:
//服務(wù)器偽代碼 byte[] data=server.accept(); //把前兩位取出來 byte[] actionCodeByte=data.read(0,2);
// actionCode有了 int actionCode=actionCodeByte.readShort();
// 取出proto消息 byte[] byte_PBMessage=data.read(2,data.length); .....接下來就和前面的服務(wù)器偽代碼一樣了 DynamicMessage req=DynamicMessage.parseFrom(descriptorMap.get(actionCode, byte_PBMessage); .... 至此動態(tài)創(chuàng)建對象完成了,接下來就是按照第一種方式維護(hù)的ActionMap通過actionCode取到action來處理DynamicMessage 解析好的請求了
,當(dāng)然actionCode也可以換成actionName,類似的。到這里似乎差不多了,當(dāng)時始終不完美,因?yàn)槲覀冞€沒有把DynamicMessage 轉(zhuǎn)換成PBMessage對象,在后續(xù)的action里處理DynamicMessage總是不舒服,解決辦法是通過DynamicMessage對象獲得Descriptor對象,在獲得其所有字段名和值, 然后看一下這個地址的這篇文章(通過字段反射對象部分):http://liufei-fir.iteye.com/blog/1160700,通過反射來還原PBMessage,以上是經(jīng)過試驗(yàn)成功的,由于時間原因就不把源碼貼上來了。有什么問題希望大家指正。
轉(zhuǎn)載于:https://my.oschina.net/u/257088/blog/277461
總結(jié)
以上是生活随笔為你收集整理的protobuf在java应用中通过反射动态创建对象的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Scribefire发CSDN博客
- 下一篇: LeetCode --- Valid P