# ProtocolBuffers学习笔记

## 1. 简介

Protocol Buffers是google发明的一种数据交换格式，独立于语言，独立于平台。与其他的数据交换格式有所不同，Protocol Buffers是一种二进制的格式，因此在网络传输的时候效率更高，相对于json它的文件体积更小，相对于xml，它的解析速度更快。

作为一种效率和兼容性都很优秀的二进制数据传输格式，可以用于**网络传输、配置文件、数据存储**等领域。

## 2. 在java中的使用

### 2.1 新建test.proto文件

```
syntax = "proto2";

package tutorial;

option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}
```

### 2.2 编译protot文件

下载编译器：

> [https://github.com/google/protobuf/releases](https://github.com/google/protobuf/releases)

一般在每个发布版本的protoc-{版本号}-{平台}.zip压缩包里面包含对应的编译器。由于LZ用的是win系统，故下载了`protoc-3.6.1-win32.zip`，解压之后在bin目录下会看到一个`proto.exe`文件。在`proto.exe`所在的目录下面就可以使用`protoc`命令进行编译了。

```
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
```

实例：

```
C:\Users\wangjun\Downloads>protoc -I=./ --java_out=./ ./test.proto
```

编译成功后会在当前目录下生成对用的java文件：

```
C:\Users\wangjun\Downloads\com\example\tutorial\AddressBookProtos.java
```

### 2.3 在项目中使用编译好的java文件

protobuf的构造和获取如下：

```
// 获取
ClassName.ObjectName Object = ClassName.ObjectName.parseFrom(byteData);

// 构造，使用builder模式
ClassName.ObjectName.Builder builder = ClassName.ObjectName.newBuilder();
ClassName.ObjectName object = builder.setName(name).setAge(11).setAddress(address).build();
byte[] byteData = object.toByteArray(); // 转成byte在网络上传输
```

新建一个java工程，目录结构如下：

```
com
    ├─example
    │  └─tutorial
    │          AddressBookProtos.java
    │
    └─wangjun
        └─protoc
                Client.java
                Constants.java
                Server.java
```

导入`protobuf-java-3.5.1.jar`(LZ使用的是此版本，读者请自行下载适用版本)。此包可以对ProtocolBuffer消息进行序列化和反序列化。

编写实例代码：

#### 1）Constants.java

```java
package com.wangjun.protoc;

public class Constants {

    public static final int PORT = 9988;
}
```

#### 2）Server.java

```java
package com.wangjun.protoc;

import java.net.ServerSocket;
import java.net.Socket;

import com.example.tutorial.AddressBookProtos;
import com.google.protobuf.ByteString;

public class Server {

    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(Constants.PORT);
        System.out.println("server started...");
        Socket socket = ss.accept();
        System.out.println("a client connected!");
        //从输入流中解析出Person对象
        AddressBookProtos.Person person = AddressBookProtos.Person.parseFrom(ByteString.readFrom(socket.getInputStream()));
        if(person != null) {
            System.out.println("server received data:\n" + person.toString());
        }
    }

}
```

#### 3）Client.java

```java
package com.wangjun.protoc;

import java.io.OutputStream;
import java.net.Socket;

import com.example.tutorial.AddressBookProtos;

public class Client {
    public static void main(String[] args) throws Exception {

        Socket socket = new Socket("localhost", Constants.PORT);
        //构造一个Person对象
        AddressBookProtos.Person person = AddressBookProtos.Person.newBuilder().setName("zhangsan")
                .setId(20).setEmail("zhangsan@xxxmail.com").build();
        OutputStream os = socket.getOutputStream();
        //将Person对象写到输出流中
        os.write(person.toByteArray());
        os.flush();
        //这里注意一定要关闭流，否则服务端会报错
        os.close();
        System.out.println("client send person");
    }

}
```

### 2.4 测试

先运行server。

```
server started...
```

再运行client。

```
client send person
```

server端已经接收到消息。

```
server started...
a client connected!
server received data:
name: "zhangsan"
id: 20
email: "zhangsan@xxxmail.com"
```

至此java中使用ProtocolBuffer的实例就完成了。

## 3. protobuf数据格式

protobuf消息定义的格式为：

```
限定修饰符① | 数据类型② | 字段名称③ | = | 字段编码值④ | [字段默认值⑤]
```

### 3.1 限定修饰符

限定修饰符包含 required\optional\repeated。

| 限定符      | 含义                                                                                                                                                                                                                                |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| required | 表示是一个必须字段，必须相对于发送方，在发送消息之前必须设置该字段的值，对于接收方，必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常，导致消息被丢弃。                                                                                                                       |
| optional | 表示是一个可选字段，可选对于发送方，在发送消息时，可以有选择性的设置或者不设置该字段的值。对于接收方，如果能够识别可选字段就进行相应的处理，如果无法识别，则忽略该字段，消息中的其它字段正常处理。因为optional字段的特性，很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段，这样老的版本无需升级程序也可以正常的与新的软件进行通信，只不过新的字段无法识别而已，因为并不是每个节点都需要新的功能，因此可以做到按需升级和平滑过渡。 |
| repeated | 表示该字段可以包含0\~N个元素。其特性和optional一样，但是每一次可以包含多个值。可以看作是在传递一个数组的值。 编译后的java文件对应类型为List。                                                                                                                                                 |

### 3.2 数据类型

| .proto类型 | Java 类型    | C++类型  | 备注                                             |
| -------- | ---------- | ------ | ---------------------------------------------- |
| double   | double     | double |                                                |
| float    | float      | float  |                                                |
| int32    | int        | int32  | 使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数，那么请使用sint32。 |
| int64    | long       | int64  | 使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数，那么请使用sint64。 |
| uint32   | int\[1]    | uint32 | 无符号类型                                          |
| uint64   | long\[1]   | uint64 | 无符号类型                                          |
| sint32   | int        | int32  | 使用可变长编码方式。有符号的整型值。编码时比通常的int32高效。              |
| sint64   | long       | int64  | 使用可变长编码方式。有符号的整型值。编码时比通常的int64高效。              |
| fixed32  | int\[1]    | uint32 | 总是4个字节。如果数值总是比总是比228大的话，这个类型会比uint32高效。        |
| fixed64  | long\[1]   | uint64 | 总是8个字节。如果数值总是比总是比256大的话，这个类型会比uint64高效。        |
| sfixed32 | int        | int32  | 总是4个字节。                                        |
| sfixed64 | long       | int64  | 总是8个字节。                                        |
| bool     | boolean    | bool   |                                                |
| string   | String     | string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。             |
| bytes    | ByteString | string | 可能包含任意顺序的字节数据。                                 |

### 3.3 字段名称

protobuf建议字段的命名采用以下划线分割的驼峰式。例如 first\_name 而不是firstName。

### 3.4 字段编码值

有了该值，通信双方才能互相识别对方的字段。当然相同的编码值，其限定修饰符和数据类型必须相同。

编码值的取值范围为 1\~2^32（4294967296）。

其中 1\~15的编码时间和空间效率都是最高的，编码值越大，其编码的时间和空间效率就越低。

protobuf 建议把经常要传递的值把其字段编码设置为1-15之间的值。消息中的字段的编码值无需连续，只要是合法的，并且不能在同一个消息中有字段包含相同的编码值。

### 3.5 默认值

当在传递数据时，对于required数据类型，如果用户没有设置值，则使用默认值传递到对端。当接受数据是，对于optional字段，如果没有接收到optional字段，则设置为默认值。

参考：

> 官方文档 [https://developers.google.com/protocol-buffers/docs/javatutorial](https://developers.google.com/protocol-buffers/docs/javatutorial)
>
> Java中使用Protocol Buffer [https://blog.csdn.net/yubo\_725/article/details/52219489](https://blog.csdn.net/yubo_725/article/details/52219489)
