游戏开发协议-Protobuf

简介

Protocol Buffers就是一种google定义的结构化数据格式,用于数据的序列化和反序列化。由于它直接对二进制源数据进行操作,所以它相对于xml来说,足够的小,快以及简单,而且又与语言、平台无关,所以兼容性也有不错的表现。目前很适合做数据存储或 网络通讯间的数据传输。

操作

  1. 首先需要激活protoc命令,使用brew install protobuf安装

  2. 定义.proto文件“login.proto”

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    syntax = "proto3";

    package msg;



    option java_package = "com.example.msg";

    option java_outer_classname = "LoginMsg";



    message Login{

    string username = 1;

    int32 pw = 2;

    }
  3. 编译protoc -I=. --java_out=. login.proto

    生成了LoginMsg.java

  4. 对LoginMsg进行操作

    通过.proto定义生成的LoginMsg.java,已经整合了对LoginMsg的序列化和反序列化相关代码,我们对login这个消息的reader和writer时只需要通过对该class进行操作即可。比如要把loginMsg写入到流里面发送出去,只需要对loginMsg进行赋值然后writer,对象就被序列化为二进制数据写出,或者接收端读取LoginMsg时,调用其ParserbyReader,就可以基于二进制流反序列化为LoginMsg对象。

    write:

    read:

    只需要对上述的stream改造为为socket就可以基于tcp进行消息传输了。

    LoginMsg内容

    Login

    消息结构对象的主体,主要存储数据,同时继承GeneratedMessageV3,内部封装对象的序列化和反序列化,writeTo序列化,paser反序列化。

    LoginOrBuilder

    接口,用来连接Login和Builder,提供类型信息以及对外提供field get方法。

    Builder

    消息对象构建器,对外封装field set方法。

    Descriptor

    消息对象元数据的描述信息,一般用不到,如果你有动态解析的需求可以通过此来处理

    Parser

    解析器,为消息反序列化提供服务

结构关系如下:

MessageLite/Message接口是所有message的抽象接口,message可以基于Parser从字节流数据中构建对象,也可以通过Builder创建的对象序列化后写入字节流数据到IO管道,MessageLite和Message内部都定义了自己的Builder类,继承自MessageLiteOrBuilder以及MessageOrBuiler,并定义了MessageLite/Message和它们各自Builder类的共同接口。

调用时序

。。。

内容搬运https://www.jianshu.com/p/9cb9fb05431a

https://www.jianshu.com/p/ec39f79c0412

JavaScript版本

1
2
3
4
5
6
7
8
syntax = "proto3";
message Login{

string username = 1;

int32 pw = 2;

}

执行protoc -I=. --js_out=. login.proto即可以产生js版本的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
/**

\* @fileoverview

\* @enhanceable

\* @suppress {messageConventions} JS Compiler reports an error if a variable or

\* field starts with 'MSG_' and isn't a translatable message.

\* @public

*/

// GENERATED CODE -- DO NOT EDIT!



goog.provide('proto.Login');



goog.require('jspb.BinaryReader');

goog.require('jspb.BinaryWriter');

goog.require('jspb.Message');





/**

\* Generated by JsPbCodeGenerator.

\* @param {Array=} opt_data Optional initial data array, typically from a

\* server response, or constructed directly in Javascript. The array is used

\* in place and becomes part of the constructed object. It is not cloned.

\* If no data is provided, the constructed object will be empty, but still

\* valid.

\* @extends {jspb.Message}

\* @constructor

*/

proto.Login = function(opt_data) {

jspb.Message.initialize(this, opt_data, 0, -1, null, null);

};

goog.inherits(proto.Login, jspb.Message);

if (goog.DEBUG && !COMPILED) {

proto.Login.displayName = 'proto.Login';

}





if (jspb.Message.GENERATE_TO_OBJECT) {

/**

\* Creates an object representation of this proto suitable for use in Soy templates.

\* Field names that are reserved in JavaScript and will be renamed to pb_name.

\* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.

\* For the list of reserved names please see:

\* com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS.

\* @param {boolean=} opt_includeInstance Whether to include the JSPB instance

\* for transitional soy proto support: http://goto/soy-param-migration

\* @return {!Object}

*/

proto.Login.prototype.toObject = function(opt_includeInstance) {

return proto.Login.toObject(opt_includeInstance, this);

};





/**

\* Static version of the {@see toObject} method.

\* @param {boolean|undefined} includeInstance Whether to include the JSPB

\* instance for transitional soy proto support:

\* http://goto/soy-param-migration

\* @param {!proto.Login} msg The msg instance to transform.

\* @return {!Object}

\* @suppress {unusedLocalVariables} f is only used for nested messages

*/

proto.Login.toObject = function(includeInstance, msg) {

var f, obj = {

username: jspb.Message.getFieldWithDefault(msg, 1, ""),

pw: jspb.Message.getFieldWithDefault(msg, 2, 0)

};



if (includeInstance) {

obj.$jspbMessageInstance = msg;

}

return obj;

};

}





/**

\* Deserializes binary data (in protobuf wire format).

\* @param {jspb.ByteSource} bytes The bytes to deserialize.

\* @return {!proto.Login}

*/

proto.Login.deserializeBinary = function(bytes) {

var reader = new jspb.BinaryReader(bytes);

var msg = new proto.Login;

return proto.Login.deserializeBinaryFromReader(msg, reader);

};





/**

\* Deserializes binary data (in protobuf wire format) from the

\* given reader into the given message object.

\* @param {!proto.Login} msg The message object to deserialize into.

\* @param {!jspb.BinaryReader} reader The BinaryReader to use.

\* @return {!proto.Login}

*/

proto.Login.deserializeBinaryFromReader = function(msg, reader) {

while (reader.nextField()) {

if (reader.isEndGroup()) {

break;

}

var field = reader.getFieldNumber();

switch (field) {

case 1:

var value = /** @type {string} */ (reader.readString());

msg.setUsername(value);

break;

case 2:

var value = /** @type {number} */ (reader.readInt32());

msg.setPw(value);

break;

default:

reader.skipField();

break;

}

}

return msg;

};





/**

\* Serializes the message to binary data (in protobuf wire format).

\* @return {!Uint8Array}

*/

proto.Login.prototype.serializeBinary = function() {

var writer = new jspb.BinaryWriter();

proto.Login.serializeBinaryToWriter(this, writer);

return writer.getResultBuffer();

};





/**

\* Serializes the given message to binary data (in protobuf wire

\* format), writing to the given BinaryWriter.

\* @param {!proto.Login} message

\* @param {!jspb.BinaryWriter} writer

\* @suppress {unusedLocalVariables} f is only used for nested messages

*/

proto.Login.serializeBinaryToWriter = function(message, writer) {

var f = undefined;

f = message.getUsername();

if (f.length > 0) {

writer.writeString(

1,

f

);

}

f = message.getPw();

if (f !== 0) {

writer.writeInt32(

2,

f

);

}

};





/**

\* optional string username = 1;

\* @return {string}

*/

proto.Login.prototype.getUsername = function() {

return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));

};





/** @param {string} value */

proto.Login.prototype.setUsername = function(value) {

jspb.Message.setProto3StringField(this, 1, value);

};





/**

\* optional int32 pw = 2;

\* @return {number}

*/

proto.Login.prototype.getPw = function() {

return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0));

};





/** @param {number} value */

proto.Login.prototype.setPw = function(value) {

jspb.Message.setProto3IntField(this, 2, value);

};

代码结构要简单的多,主要调用的是jspb库。

很容易看出来,fieldNumber很重要,所以在前后端更新协议的时候,往后面加,而不是在中间插

message的二进制结构

每个消息字段读取的时候,都会先调用一次readTag或者writeTag。tag等于就是这个value信息的描述或者定义,告知解析器当前fields是什么类型字段,以及读取的顺序,有了这个信息,解析器就知道一个field在流中的开始位置和结束位置,如此一个field解码成功,并且与字段顺序无关。

tag的构成

(fieldNumber << 3) | wireType

Protobuf的优点

fieldNumber 为每个field定义一个编号,其一保证不重复,其二保证其在流中的位置。如若当前数据流中有某个字段,而解析方没有相关的解析代码,解析放会直接skip 这个field,而且读数据的position也会后移,保证后续读取不出问题。

cautions

proto文件要使用UTF-8