protobuf在java應用中通過反射動態創建對象(DynamicMessage)


---恢復內容開始---

最近編寫一個游戲用到protobuf數據格式進行前后台傳輸,苦於protobuf接受客戶端的數據時是需要數據類型的如xxx.parseForm(...),這樣就要求服務器在接受客戶端請求時必須知道客戶端傳遞的數據類型。由於客戶端的請求數據是多種多樣的,服務器端又不知道客戶端的請求到底是哪個類型,這樣就使得服務器端編程帶來很多麻煩,甚至寸步難行。難道就沒有解決辦法了嗎,答案當然是有的。下面就說一下常用的方法。(在看本文之前建議先了解protobuf的一些基本語法,和基本用法)

1.第一種方法也是最簡單的方法,就是在整個應用程序中只定義一個proto文件,那么所有的請求都是一種類型,那么服務器端就不用苦惱怎么解析請求數據了,因為不管哪個請求數據都用同一個對象解析。如下面的列子:

首先貼一個PBMessage.proto文件

//客戶端請求以及服務端響應數據協議
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; //提交或響應的數據
optional DataMsg dataMsg = 6; //服務器端推送數據
optional string sessionKey = 7; //請求的校驗碼
optional int32 sessionId = 8;//當前請求的標示
}

如上述代碼,整個應用都基於PBMessage.proto傳輸,注意到protobuf 語法中 optional修飾符,他表示這個字段是非必須的,也就是說對於客戶端的不同請求,只需要為它填充其請求時用到的字段的值即可,其他的字段的值就不用管了,這樣就可以模擬出各種請求來,那么接下來我們就用:PBMessage.parseForm(byte_PBMesage) //byte_PBMesage表示客戶端請求數據 ,這樣請求的解析就完成了。同時我們注意到: (required int32 actionCode = 2; //操作碼id) ,required表示該字段是必須的,前面請求已經解析好了,在這里我們拿到actionCode 就可以知道我們該用哪個Action事件來處理該請求了(前提是必須維護一張actionCode到Action的映射關系表:Map<int,Action>),至此整個請求的解析和處理都完成了。

接下來說一下第一種方式的優缺點,優點:整個應用消息格式一致統一,操作簡單。缺點:太統一,就不靈活,對於請求很少,消息格式很少的小型應用倒還勉強能用,消息格式多的話再用這種方式就顯得臃腫,不便於管理,失去了程序設計的意義。

 

2.第二種方法,也是我本次用到的方法。苦於提議中方式的局限性,本人通過在網上收集資料以及查看protobuf java版的源碼,發現了一個折中的方式。首先我們看下protobuf源碼中提供的, DynamicMessage 類(顧名思義 動態消息類,眼前一亮有木有),它繼承了AbstractMessage類,比較一下和第一種方式創建的PBMessage類的區別 我們發現PBMessage類繼承了GeneratedMessage類,而GeneratedMessage類繼承了AbstractMessage類,至此我們發現了共同類AbstractMessage,再次證明了DynamicMessage 管用,同時我們再看看AbstractParser<MessageType>類(在PBMessage類中持有AbstractParser類的對象,並用其來解析請求數據),它繼承了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();
 }

發現了他們方法的相似點,在這里我們可以用一個等量關系比喻:DynamicMessage=AbstractMessage+ AbstractParser=PBMessage,也就是說DynamicMessage繼承AbstractMessage(請求消息對象)的同時又間接實現了AbstractParser(請求消息數據解析)對數據解析的功能。現在我們唯一缺少的就是Descriptors.Descriptor(對消息的描述)對象,這個對象該怎么拿到呢,在這里肯定的說,對於不同的.proto請求這里的Descriptors.Descriptor是不一樣的。在這里我們又回到PBMessage對象中,我們發現了其中有這樣一個方法:

 

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了,不是嗎。在這里重點來了,再來理解一下,首先有了PBMessage對象(這里用其來做代表,可以使其他的.proto對象)就可以獲得 Descriptors.Descriptor 對象,有了Descriptors.Descriptor對象就可以創建DynamicMessage對象了,有了DynamicMessage就可以解析對應請求了。下面看代碼:

//存放消息操作碼和消息對象
Map<Integer,Descriptor> descriptorMap=new Map<Integer,Descriptor>;
//把消息描述對象添加進來
descriptorMap.add(100,PBMessage.getDescriptor());
descriptorMap.add(xxx,xxx);

這樣Descriptor有了,其實還可以做得更好一點,通過反射機制,Map里面只存放操作碼和對應的proto對象類名,再通過反射方式創建proto對象在獲得其getDescriptor()方法。這樣就可以在配置文件中配置操作碼和proto對象的關系了。

好接下來我們來接受客戶端的請求試一下,

//客戶端偽代碼
client.send(byte_PBMessage);

然后服務器接受請求並解析,

//服務器偽代碼

byte[] date=server.accept();

//客戶端操作碼
int actionCode;

Descriptor descriptor=descriptorMap.get(actionCode);
//解析請求 DynamicMessage req=DynamicMessage.parseFrom(descriptor, date);

在這里我們發現似乎還少了點什么,好像actionCode還不知道,怎么辦呢,好吧,我們在客戶端發送的請求消息頭上再加上個actionCode,即把操作碼和proto消息合並為一個新的請求發送給客戶端,請求2位為操作碼,那么現在客戶端應該這么發送消息了:

//客戶端偽代碼
 short actionCode=100;

//兩個字節來存放actionCode
 byte [] actionCodeByte=new byte [2];

// 轉換成字節流
actionCodeByte.set(actionCode.toByteArray());//偽代碼,請勿當真

//帶請求頭的消息的總長度
int length=actionCodeByte.length+byte_PBMEssage.length;

byte [] messageByte=new byte[length];

//把操作碼和proto消息合並
messageByte=actionCodeByte+byte_PBMEssage;

client.send(messageByte);

下來是服務器了:

//服務器偽代碼
byte[] data=server.accept();
//把前兩位取出來
byte[] actionCodeByte=data.read(0,2);

// actionCode有了
int actionCode=actionCodeByte.readShort();

// 取出proto消息
byte[] byte_PBMessage=data.read(2,data.length);
.....接下來就和前面的服務器偽代碼一樣了
DynamicMessage req=DynamicMessage.parseFrom(descriptorMap.get(actionCode, byte_PBMessage);
....

至此動態創建對象完成了,接下來就是按照第一種方式維護的ActionMap通過actionCode取到action來處理DynamicMessage 解析好的請求了

,當然actionCode也可以換成actionName,類似的。到這里似乎差不多了,當時始終不完美,因為我們還沒有把DynamicMessage 轉換成PBMessage對象,在后續的action里處理DynamicMessage總是不舒服,解決辦法是通過DynamicMessage對象獲得Descriptor對象,在獲得其所有字段名和值, 然后看一下這個地址的這篇文章(通過字段反射對象部分):http://liufei-fir.iteye.com/blog/1160700,通過反射來還原PBMessage,以上是經過試驗成功的,由於時間原因就不把源碼貼上來了。有什么問題希望大家指正。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM