7.1 详解粘包和拆包

7.1.1 半包问题的实战案例

  1. package com.crazymakercircle.netty.echoServer;
  2. //…
  3. public class NettyDumpSendClient {
  4. private int serverPort;
  5. private String serverIp;
  6. Bootstrap b = new Bootstrap();
  7. public NettyDumpSendClient(String ip, int port) {
  8. this.serverPort = port;
  9. this.serverIp = ip;
  10. }
  11. public void runClient() {
  12. //创建反应器线程组
  13. //省略启动客户端Bootstrap引导类配置和启动
  14. //阻塞,直到连接完成
  15. f.sync();
  16. Channel channel = f.channel();
  17. //发送大量的文字
  18. String content= "疯狂创客圈:高性能学习者社群!";
  19. byte[] bytes =content.getBytes(Charset.forName("utf-8"));
  20. for (int i = 0; i< 1000; i++) {
  21. //发送ByteBuf
  22. ByteBuf buffer = channel.alloc().buffer();
  23. buffer.writeBytes(bytes);
  24. channel.writeAndFlush(buffer);
  25. }
  26. //省略优雅关闭客户端
  27. }
  28. public static void main(String[] args) throws InterruptedException {
  29. int port = NettyDemoConfig.SOCKET_SERVER_PORT;
  30. String ip = NettyDemoConfig.SOCKET_SERVER_IP;
  31. new NettyDumpSendClient(ip, port).runClient();
  32. }
  33. }

7.1.2 什么是半包问题

  1. 粘包:接收端(Receiver)收到一个ByteBuf,包含了发送端(Sender)的多个ByteBuf,发送端的多个ByteBuf在接收端“粘”在了一起
  2. 半包:Receiver将Sender的一个ByteBuf“拆”开了收,收到多个破碎的包。换句话说,Receiver收到了Sender的一个ByteBuf的一小部分。

image.png

7.2 使用JSON协议通信

JSON(JavaScript Object Notation,JS对象简谱)是一种轻量级的数据交换格式。它是基于ECMAScript(欧洲计算机协会制定的JS规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得JSON成为理想的数据交换语言。
JSON协议是一种文本协议,易于人阅读和编写,同时也易于机器解析和生成,并能有效地提升网络传输效率。

7.2.1 JSON的核心优势

XML是一种常用的文本协议,和JSON一样都使用结构化方法来标记数据。和XML相比,JSON作为数据包格式传输的时候具有更高的效率。这是因为JSON不像XML那样需要有严格的闭合标签,让有效数据量与总数据包比大大提升,从而在同等数据流量的情况下减少了网络的传输压力。

7.2.2 JSON序列化与反序列化开源库

Java处理JSON数据有三个比较流行的开源类库:阿里巴巴的FastJson、谷歌的Gson和开源社区的Jackson。
在实际开发中,目前主流的策略是Gson和FastJson结合使用。在POJO序列化成JSON字符串的应用场景下,使用谷歌的Gson库;在JSON字符串反序列化成POJO的应用场景下,使用阿里巴巴的FastJson库。

  1. package com.crazymakercircle.util;
  2. //省略import
  3. public class JsonUtil {
  4. //谷歌GsonBuilder构造器
  5. static GsonBuilder gb = new GsonBuilder();
  6. static {
  7. //不需要html escape
  8. gb.disableHtmlEscaping();
  9. }
  10. //序列化:使用Gson将 POJO 转成字符串
  11. public static String pojoToJson(java.lang.Object obj) {
  12. String json = gb.create().toJson(obj);
  13. return json;
  14. }
  15. //反序列化:使用Fastjson将字符串转成 POJO对象
  16. public static <T> T jsonToPojo(String json, Class<T>tClass) {
  17. T t = JSONObject.parseObject(json, tClass);
  18. return t;
  19. }
  20. }

7.2.3 JSON序列化与反序列化的实战案例

首先定义一个POJO类,名称为JsonMsg,包含id和content两个属性,然后使用lombok开源库的@Data注解为属性加上getter()和setter()方法。POJO类的源码如下:

  1. @Data
  2. public class JsonMsg {
  3. private int id;
  4. private String content;
  5. public String convertToJson() {
  6. return JsonUtil.pojoToJson(this);
  7. }
  8. public static JsonMsg parseFromJson(String json) {
  9. return JsonUtil.jsonToPojo(json, JsonMsg.class);
  10. }
  11. }

使用POJO类JsonMsg的序列化、反序列化的实战案例代码如下:

  1. public class JsonMsgDemo {
  2. public JsonMsg buildMsg() {
  3. JsonMsg user = new JsonMsg();
  4. user.setId(1000);
  5. user.setContent("弯弯入我心,秋凉知我意!");
  6. return user;
  7. }
  8. @Test
  9. public void serAndDesr() {
  10. JsonMsg message = buildMsg();
  11. String json = message.convertToJson();
  12. Logger.info("json:=" + json);
  13. JsonMsg msg = JsonMsg.parseFromJson(json);
  14. Logger.info("id:=" + msg.getId());
  15. Logger.info("content:=" + msg.getContent());
  16. }
  17. }
  18. [main|JsonMsgDemo.serAndDesr] |> json:={"id":1000,"content":"弯弯入我心,秋凉知我意!"}
  19. [main|JsonMsgDemo.serAndDesr] |> id:=1000
  20. [main|JsonMsgDemo.serAndDesr] |> content:=弯弯入我心,秋凉知我意!

7.2.4 JSON传输的编码器和解码器

Head-Content数据包的解码过程
image.png
Head-Content数据包的编码过程
image.png
Netty内置LengthFieldPrepender编码器的作用是在数据包的前面加上内容的二进制字节数组的长度。这个编码器和LengthFieldBasedFrameDecoder解码器是天生的一对,常常配套使用。这组“天仙配”属于Netty所提供的一组非常重要的编码器和解码器,常常用于Head-Content数据包的传输。

  1. //构造器一
  2. public LengthFieldPrepender(int lengthFieldLength) {
  3. this(lengthFieldLength, false);
  4. }
  5. //构造器二
  6. public LengthFieldPrepender(int lengthFieldLength, Boolean lengthIncludesLengthFieldLength)
  7. {
  8. this(lengthFieldLength, 0, lengthIncludesLengthFieldLength);
  9. }
  10. //省略其他的构造器
  1. 在上面的构造器中,第一个参数lengthFieldLength表示Head长度字段所占用的字节数,第二个参数lengthIncludesLengthFieldLength表示Head字段的总长度值是否包含长度字段自身的字节数,如果该参数的值为true,表示长度字段的值(总长度)包含了自己的字节数。如果该参数的值为false,表示长度值只包含内容的二进制数据的长度。lengthIncludesLengthFieldLength值一般设置为false

7.2.5 JSON传输的服务端的实战案例

服务端的程序仅仅读取客户端数据包并完成解码,服务端的程序没有写出任何输出数据包到对端(客户端)

  1. package com.crazymakercircle.netty.protocol;
  2. //…
  3. public class JsonServer {
  4. //省略成员属性、构造器
  5. public void runServer() {
  6. //创建反应器线程组
  7. EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
  8. EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
  9. try {
  10. //省略引导类的反应器线程、设置配置项等
  11. //5 装配子通道流水线
  12. b.childHandler(new ChannelInitializer<SocketChannel>() {
  13. //有连接到达时会创建一个通道
  14. protected void initChannel(SocketChannel ch)…{
  15. //管理子通道中的Handler
  16. //向子通道流水线添加3个Handler
  17. ch.pipeline().addLast(
  18. new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
  19. ch.pipeline().addLast(new
  20. StringDecoder(CharsetUtil.UTF_8));
  21. ch.pipeline().addLast(new JsonMsgDecoder());
  22. }
  23. });
  24. //省略端口绑定、服务监听、优雅关闭
  25. }
  26. //服务端业务处理器
  27. static class JsonMsgDecoderextends ChannelInboundHandlerAdapter {
  28. @Override
  29. public void channelRead(ChannelHandlerContext ctx, Object msg)…{
  30. String json = (String) msg;
  31. JsonMsg jsonMsg = JsonMsg.parseFromJson(json);
  32. Logger.info("收到一个 Json 数据包 =>>" + jsonMsg);
  33. }
  34. }
  35. public static void main(String[] args) throws InterruptedException {
  36. int port = NettyDemoConfig.SOCKET_SERVER_PORT;
  37. new JsonServer(port).runServer();
  38. }
  39. }

7.2.6 JSON传输的客户端的实战案例

  1. 通过谷歌的Gson框架,将POJO序列化成JSON字符串。
  2. 使用StringEncoder编码器(Netty内置)将JSON字符串编码成二进制字节数组。
  3. 使用LengthFieldPrepender编码器(Netty内置)将二进制字节数组编码成Head-Content格式的二进制数据包。 ```java package com.crazymakercircle.netty.protocol; //… public class JsonSendClient { static String content = “疯狂创客圈:高性能学习社群!”; //省略成员属性、构造器 public void runClient() {

    1. //创建反应器线程组
    2. EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
    3. try {
    4. //省略引导类的反应器线程、设置配置项等
    5. //5 装配通道流水线
    6. b.handler(new ChannelInitializer<SocketChannel>() {
    7. //初始化客户端通道
    8. protected void initChannel(SocketChannel ch) …{
    9. //客户端通道流水线添加2个Handler
    10. ch.pipeline().addLast(new LengthFieldPrepender(4));
    11. ch.pipeline().addLast(new
    12. StringEncoder(CharsetUtil.UTF_8));
    13. }
    14. });
    15. ChannelFuture f = b.connect();
    16. //…
    17. //阻塞,直到连接完成
    18. f.sync();
    19. Channel channel = f.channel();
    20. //发送 JSON 字符串对象
    21. for (int i = 0; i< 1000; i++) {
    22. JsonMsg user = build(i, i + "->" + content);
  1. <a name="tMapu"></a>
  2. ## 7.3 使用Protobuf协议通信
  3. Protobuf(Protocol Buffer)是Google提出的一种数据交换格式,是一套类似JSON或者XML的数据传输格式和规范,用于不同应用或进程之间的通信。Protobuf具有以下特点:
  4. 1. 语言无关,平台无关
  5. 1. 高效
  6. 1. 扩展性、兼容性好
  7. <a name="Ui25f"></a>
  8. ## 7.3.1 一个简单的proto文件的实战案例
  9. 下面介绍一个非常简单的proto文件:仅仅定义一个消息结构体,并且该消息结构体也非常简单,仅包含两个字段。实例如下:
  10. ```java
  11. //[开始头部声明]
  12. syntax = "proto3";
  13. package com.crazymakercircle.netty.protocol;
  14. //[结束头部声明]
  15. //[开始 Java选项配置]
  16. option java_package = "com.crazymakercircle.netty.protocol";
  17. option java_outer_classname = "MsgProtos";
  18. //[结束 Java选项配置]
  19. //[开始消息定义]
  20. message Msg {
  21. uint32 id = 1; //消息ID
  22. string content = 2; //消息内容
  23. }
  24. //[结束消息定义]

7.3.2 通过控制台命令生成POJO和Builder

7.3.3 通过Maven插件生成POJO和Builder

  1. <plugin>
  2. <groupId>org.xolstice.maven.plugins</groupId>
  3. <artifactId>protobuf-maven-plugin</artifactId>
  4. <version>0.5.0</version>
  5. <extensions>true</extensions>
  6. <configuration>
  7. <!--proto文件路径-->
  8. <protoSourceRoot>${project.basedir}/protobuf</protoSourceRoot>
  9. <!--目标路径-->
  10. <outputDirectory>${project.build.sourceDirectory}</outputDirectory>
  11. <!--设置是否在生成java文件之前清空outputDirectory的文件-->
  12. <clearOutputDirectory>false</clearOutputDirectory>
  13. <!--临时目录-->
  14. <temporaryProtoFileDirectory>${project.build.directory}/protoc-temp</temporaryProtoFileDirectory>
  15. <!--protoc 可执行文件路径-->
  16. <protocExecutable>${project.basedir}/protobuf/protoc3.6.1.exe</protocExecutable>
  17. </configuration>
  18. <executions>
  19. <execution>
  20. <goals>
  21. <goal>compile</goal>
  22. <goal>test-compile</goal>
  23. </goals>
  24. </execution>
  25. </executions>
  26. </plugin>

7.3.4 Protobuf序列化与反序列化的实战案例

在Maven的pom.xml文件中加上protobuf的Java运行包的依赖,代码如下

  1. <dependency>
  2. <groupId>com.google.protobuf</groupId>
  3. <artifactId>protobuf-java</artifactId>
  4. <version>3.6.1</version>
  5. </dependency>
  1. 使用Builder构造POJO消息对象 ```java package com.crazymakercircle.netty.protocol; //… public class ProtobufDemo { public static MsgProtos.Msg buildMsg() {
    1. MsgProtos.Msg.Builder personBuilder = MsgProtos.Msg.newBuilder();
    2. personBuilder.setId(1000);
    3. personBuilder.setContent("疯狂创客圈:高性能学习社群");
    4. MsgProtos.Msg message = personBuilder.build();
    5. return message;
    } //… }
  1. 2. 序列化与反序列化的方式一
  2. 方式一为调用Protobuf POJO对象的toByteArray()方法将POJO对象序列化成字节数组,具体的代码如下:
  3. ```java
  4. package com.crazymakercircle.netty.protocol;
  5. //…
  6. public class ProtobufDemo {
  7. //第1种方式:序列化与反序列化
  8. @Test
  9. public void serAndDesr1() throws IOException {
  10. MsgProtos.Msg message = buildMsg();
  11. //将Protobuf对象序列化成二进制字节数组
  12. byte[] data = message.toByteArray();
  13. //可以用于网络传输,保存到内存或外存
  14. ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
  15. outputStream.write(data);
  16. data = outputStream.toByteArray();
  17. //二进制字节数组反序列化成Protobuf对象
  18. MsgProtos.Msg inMsg = MsgProtos.Msg.parseFrom(data);
  19. Logger.info("id:=" + inMsg.getId());
  20. Logger.info("content:=" + inMsg.getContent());
  21. }
  22. //…
  23. }
  1. 序列化与反序列化的方式二 ```java package com.crazymakercircle.netty.protocol; //… public class ProtobufDemo {

    1. //…

    //第2种方式:序列化与反序列化 @Test public void serAndDesr2() throws IOException {

    1. MsgProtos.Msg message = buildMsg();
    2. //序列化到二进制码流
    3. ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    4. message.writeTo(outputStream);
    5. ByteArrayInputStream inputStream =
    6. new ByteArrayInputStream(outputStream.toByteArray());
    7. //从二进码流反序列化成Protobuf对象
    8. MsgProtos.Msg inMsg = MsgProtos.Msg.parseFrom(inputStream);
    9. Logger.info("id:=" + inMsg.getId());
    10. Logger.info("content:=" + inMsg.getContent());

    } }

  1. 4. 序列化与反序列化的方式三
  2. ```java
  3. package com.crazymakercircle.netty.protocol;
  4. //…
  5. public class ProtobufDemo {
  6. //…
  7. //第3种方式:序列化与反序列化
  8. //带字节长度:[字节长度][字节数据],用于解决粘包/半包问题
  9. @Test
  10. public void serAndDesr3() throws IOException {
  11. MsgProtos.Msg message = buildMsg();
  12. //序列化到二进制码流
  13. ByteArrayOutputStream outputStream =
  14. new ByteArrayOutputStream();
  15. message.writeDelimitedTo(outputStream);
  16. ByteArrayInputStream inputStream =
  17. new ByteArrayInputStream(outputStream.toByteArray());
  18. //从二进制码字节流反序列化成Protobuf对象
  19. MsgProtos.Msg inMsg =
  20. MsgProtos.Msg.parseDelimitedFrom(inputStream);
  21. Logger.info("id:=" + inMsg.getId());
  22. Logger.info("content:=" + inMsg.getContent());
  23. }
  24. }

反序列化时,调用Protobuf生成的POJO类的parseDelimitedFrom(InputStream)静态方法,从输入流中先读取varint32类型的长度值,然后根据长度值读取此消息的二进制字节,再反序列化得到POJO新的实例。

7.4 Protobuf编解码的实战案例

Netty默认支持Protobuf的编码与解码,内置了一套基础的Protobuf编码和解码器。

7.4.1 Netty内置的Protobuf基础编码器/解码器

  1. ProtobufEncoder编码器

直接调用了Protobuf POJO实例的toByteArray()方法将自身编码成二进制字节,然后放入Netty的ByteBuf缓冲区中,接着会被发送到下一站编码器。

  1. package io.netty.handler.codec.protobuf;
  2. @Sharable
  3. public class ProtobufEncoder extends MessageToMessageEncoder<MessageLiteOrBuilder> {
  4. @Override
  5. protected void encode(ChannelHandlerContext ctx, MessageLiteOrBuilder msg, List<Object> out) throws Exception {
  6. if (msg instanceof MessageLite) {
  7. out.add(Unpooled.wrappedBuffer( ((MessageLite) msg).toByteArray()));
  8. return;
  9. }
  10. if (msg instanceof MessageLite.Builder) {
  11. out.add(Unpooled.wrappedBuffer(( (MessageLite.Builder) msg).build().toByteArray()));
  12. }
  13. }
  14. }
  1. ProtobufDecoder解码器

ProtobufDecoder和ProtobufEncoder相互对应,只不过在使用的时候ProtobufDecoder解码器需要指定一个Protobuf POJO实例作为解码的参考原型(prototype),解码时会根据原型实例找到对应的Parser解析器,将二进制的字节解码为Protobuf POJO实例。

  1. ProtobufVarint32LengthFieldPrepender长度编码器

这个编码器的作用是在ProtobufEncoder生成的字节数组之前前置一个varint32数字,表示序列化的二进制字节数量或者长度。

  1. ProtobufVarint32FrameDecoder长度解码器

    7.4.2 Protobuf传输的服务端的实战案例

    服务端的实战案例程序代码如下

    1. package com.crazymakercircle.netty.protocol;
    2. //…
    3. public class ProtoBufServer
    4. {
    5. //省略成员属性、构造器
    6. public void runServer()
    7. {
    8. //创建反应器线程组
    9. EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
    10. EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
    11. try
    12. {
    13. //省略引导类的反应器线程、设置配置项
    14. //5 装配子通道流水线
    15. b.childHandler(new ChannelInitializer<SocketChannel>()
    16. {
    17. //有连接到达时会创建一个通道
    18. protected void initChannel(SocketChannel ch)
    19. {
    20. //流水线管理子通道中的Handler业务处理器
    21. //向子通道流水线添加3个Handler业务处理器
    22. ch.pipeline().addLast(
    23. new ProtobufVarint32FrameDecoder());
    24. ch.pipeline().addLast(
    25. new ProtobufDecoder(MsgProtos.Msg.getDefaultInstance()));
    26. ch.pipeline().addLast(new ProtobufBussinessDecoder());
    27. }
    28. });
    29. //省略端口绑定、服务监听、优雅关闭
    30. }
    31. //服务端的Protobuf业务处理器
    32. static class ProtobufBussinessDecoder
    33. extends ChannelInboundHandlerAdapter
    34. {
    35. @Override
    36. public void channelRead(
    37. ChannelHandlerContext ctx, Object msg) {
    38. MsgProtos.Msg protoMsg = (MsgProtos.Msg) msg;
    39. //经过流水线的各个解码器取得了POJO实例
    40. Logger.info("收到一个ProtobufPOJO =>>");
    41. Logger.info("protoMsg.getId():=" + protoMsg.getId());
    42. Logger.info("protoMsg.getContent():=" +
    43. protoMsg.getContent());
    44. }
    45. }
    46. }
    47. public static void main(String[] args) throws InterruptedException
    48. {
    49. int port = NettyDemoConfig.SOCKET_SERVER_PORT;
    50. new ProtoBufServer(port).runServer();
    51. }
    52. }

    7.4.3 Protobuf传输的客户端的实战案例

  2. 使用Netty内置的ProtobufEncoder将Protobuf POJO对象编码成二进制的字节数组。

  3. 使用Netty内置的ProtobufVarint32LengthFieldPrepender编码器,加上varint32格式的可变长度。Netty会将完成了编码后的Length+Content格式的二进制字节码发送到服务端。

image.png

  1. package com.crazymakercircle.netty.protocol;
  2. //…
  3. public class ProtoBufSendClient {
  4. static String content = "疯狂创客圈:高性能学习社群!";
  5. //省略成员属性、构造器
  6. public void runClient() {
  7. //创建反应器线程组
  8. EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
  9. try {
  10. //省略反应器组、IO通道、通道参数等设置
  11. //5 装配通道流水线
  12. b.handler(new ChannelInitializer<SocketChannel>() {
  13. //初始化客户端通道
  14. protected void initChannel(SocketChannel ch) …{
  15. //客户端流水线添加2个Handler业务处理器
  16. ch.pipeline().addLast(
  17. new ProtobufVarint32LengthFieldPrepender());
  18. ch.pipeline().addLast(new ProtobufEncoder());
  19. }
  20. });
  21. ChannelFuture f = b.connect();
  22. //…
  23. //阻塞,直到连接完成
  24. f.sync();
  25. Channel channel = f.channel();
  26. //发送Protobuf对象
  27. for (int i = 0; i< 1000; i++) {
  28. MsgProtos.Msg user = build(i, i + "->" + content);
  29. channel.writeAndFlush(user);
  30. Logger.info("发送报文数:" + i);
  31. }
  32. channel.flush();
  33. //省略关闭等待、优雅关闭
  34. }
  35. //构建ProtoBuf对象
  36. public MsgProtos.Msgbuild(int id, String content) {
  37. MsgProtos.Msg.Builder builder = MsgProtos.Msg.newBuilder();
  38. builder.setId(id);
  39. builder.setContent(content);
  40. return builder.build();
  41. }
  42. public static void main(String[] args) throws InterruptedException {
  43. int port = NettyDemoConfig.SOCKET_SERVER_PORT;
  44. String ip = NettyDemoConfig.SOCKET_SERVER_IP;
  45. new ProtoBufSendClient(ip, port).runClient();
  46. }
  47. }

7.5 详解Protobuf协议语法

在Protobuf中,通信协议的格式是通过proto文件定义的。一个proto文件有两大组成部分:头部声明、消息结构体的定义。头部声明部分主要包含了协议的版本、包名、特定语言的选项设置等;消息结构体部分可以定义一个或者多个消息结构体。

7.5.1 proto文件的头部声明

  1. //[开始声明]
  2. syntax = "proto3";
  3. //定义Protobuf的包名称空间
  4. package com.crazymakercircle.netty.protocol;
  5. //[结束声明]
  6. //[开始 Java 选项配置]
  7. option java_package = "com.crazymakercircle.netty.protocol";
  8. option java_outer_classname = "MsgProtos";
  9. //[结束 Java 选项配置]
  1. syntax版本号
  2. package包

    和java类似,通过package指定包名,用来避免消息名字相冲突,如果两个消息名称相同,但package包名不同,那么可以共存 在java中,会以package指定的包名作为生成的pojo类的包名

  3. option配置选项

    与proto文件使用的一些特定语言场景有关,在java中以java_开头的option选项会生效 选项option java_package表示protobuf编译器在生成java pojo消息类时,生成在此选项所配置的java包名下,如果没有该选项,则会以头部声明的package作为java包名 选项option java_multiple_file表示生成java类时的打包方式

    1. 一个消息对应一个独立的java类
    2. 所有的消息都作为内部类,打包到一个外部类中(默认)

    选项option java_outer_classname

7.5.2 Protobuf的消息结构体与消息字段

  1. //[开始消息定义]
  2. message Msg {
  3. uint32 id = 1; //消息ID
  4. string content = 2; //消息内容
  5. }
  6. //[结束消息定义]
  1. 消息字段的限定修饰符
    1. repeated: 表示该字段可以包含0-N个元素值,相当于Java中的List
    2. singular:表示该字段可以包含0-1个元素值,是默认的修饰符
    3. reserved:指定保留字段名称和分配标识号
  2. 消息字段的数据类型
  3. 消息字段的字段名称
  4. 消息字段的分配标识号

    1. 在消息定义中,每个字段都有唯一的一个数字表示符,叫做分配标识号

      7.5.3 Protobuf字段的数据类型

      image.png

      7.5.4 proto文件的其他语法规范

  5. 声明

  6. 嵌套消息
  7. 枚举