自研RPC-协议与序列化

50 阅读8分钟

跟孙哥学java

孙哥主页

什么是协议?

  1. 网络传输数据过程中的数据格式,一种约定。

  2. 协议是分层的

  1. 不同的协议 端口是可以冲突的?

TCP:80 和 UPD:80 不会冲突 若Http :80 pop3 :80 会端口冲突

  1. 共有协议/私有协议 :::info 共有协议(Public Protocol)和私有协议(Private Protocol)是指在计算机通信中的两种协议类型。

  2. 共有协议(Public Protocol): 共有协议是一种公开标准或规范,通常由行业组织、标准制定机构或开放社区制定和维护。这些协议是公开的,任何人都可以访问其规范,并且在实现和使用时具有普适性。共有协议通常用于确保不同系统和设备之间的互操作性,以促进信息交换和通信。 例子:

  • HTTP/HTTPS: 超文本传输协议,用于在网络上传输超文本。
  • TCP/IP: 传输控制协议/互联网协议,用于在互联网上进行数据传输。
  • SMTP/POP3/IMAP: 用于电子邮件传输和接收的协议。
  1. 私有协议(Private Protocol): 私有协议是由个体、组织或公司独立定义的通信协议,通常没有公开的规范。这些协议可能是特定于某个应用、服务或产品的定制协议。私有协议在特定的环境中使用,可能不被外部系统或应用程序支持。 例子:
  • 企业内部通信协议: 一些公司可能会为其内部系统和服务定义专有的通信协议,以满足其特定需求。
  • 特定硬件设备通信协议: 一些硬件制造商可能使用特定于其产品的私有协议,用于设备间的通信。
  • 定制化的通信协议: 某些应用程序或服务可能选择使用私有协议,以满足其独特的功能和性能要求
基于自己软件定义的一种数据传输的格式,只限定于自己的软件网络通信过程中使用。
Netty聊天室 协议 私有协议
Dubbo   dubbo协议 私有
自研的RPC 私有的协议 
jdbc 私有的协议   
传输层 多 

:::

  1. 通信组合

应用层通信+应用层协议 ---> 应用层通信 【Http协议+文本---> springcloud体系 传输效率 低 应用广泛性 高

传输层通信+私有协议 ----> 传输层通信 【私有协议+二进制】---> dobbo 传输效率 高 应用广泛性 低

 grpc 应用层协议(Http2+二进制)  

协议和序列化

序列化的目的:更加方便的在网络中传输数据,有利于编程的处理。

常见的序列化

  1. JSON字符串

Object--->json json-->User 好处:可读性好 存在问题: 体量大 同等数据体量的情况下: 字符串比二进制(byte)大 {name="zhangsan",age=10}

  1. 二进制形式

体量小,可读性差 JDK 序列化,Hanssian序列化,Thrift序列化,Protobuf 序列化 数据体量大小 从左到右,越来越小,Prorobuf最小,JDK序列化最大

底层传输

二进制/字符串 在底层传输的都是二进制 image.png

常见的序列化方式

网络传输数据把 Object---序列化成 网络传输数据的个数

  1. 二进制
    1. JDK
      1. 序列化:ObjectOutputStream
      2. 反序列化: ObjectInputStream
    2. Hessain [Dubbo]
      1. 序列化 Hessian2Output
      2. 反序列化 Hessian2Input
    3. Thrfit
    4. Protobuf
  2. 字符串
    1. JSON

jdk序列化

   @Test
   public void test3() throws IOException, ClassNotFoundException {
      User user=new User("LISI",19);
      ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
      ObjectOutputStream objectOutputStream=new ObjectOutputStream(outputStream);
      objectOutputStream.writeObject(user);
      byte[] byteArray = outputStream.toByteArray();
      System.out.println("byteArray.length"+byteArray.length);

      ByteArrayInputStream inputStream=new ByteArrayInputStream(byteArray);
      ObjectInputStream objectInputStream=new ObjectInputStream(inputStream);
      Object o = objectInputStream.readObject();
      System.out.println((User)o);
   }

image.png 序列化的大小为79

public class TestJDK {
   @Test
   public void test1() throws IOException {
      //序列化   Object--->数据格式
      // 反序列化  数据格式--->Object
      User user=new User("zhangsan",11);
      FileOutputStream fileOutputStream=new FileOutputStream("test");
      ObjectOutputStream outputStream=new ObjectOutputStream(fileOutputStream);
      outputStream.writeObject(user);
   }

   @Test
   public void test2() throws IOException, ClassNotFoundException {
      //序列化   Object--->数据格式
      // 反序列化  数据格式--->Object
      User user=new User("zhangsan",11);
      FileInputStream fileInputStream=new FileInputStream("test");
      ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
      Object o = objectInputStream.readObject();
      System.out.println((User)o);
   }
}

hessian 序列化

引入依赖

        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>4.0.38</version>
        </dependency>
   @Test
   public void test1() throws IOException {
      User user=new User("LISI",19);

      //序列化
      ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
      Hessian2Output hessian2Output=new Hessian2Output(outputStream);
      hessian2Output.writeObject(user);
      //Hessian 必须要flush
      hessian2Output.flush();
      byte[] byteArray = outputStream.toByteArray();
      System.out.println("byteArray.length="+byteArray.length);

      //反序列化
      ByteArrayInputStream inputStream=new ByteArrayInputStream(byteArray);
      Hessian2Input hessian2Input=new Hessian2Input(inputStream);
      User newUser = (User) hessian2Input.readObject(User.class);
      System.out.println(newUser);
   }

image.png Hessian序列化的大小为32 比JDK序列化的大小小了一倍

Thrift序列化

引入依赖

<dependency>
   <groupId>org.apache.thrift</groupId>
   <artifactId>libthrift</artifactId>
   <version>0.13.0</version>
</dependency>
namespace java com.suns.thrift

struct User{
  1:string name,
  2:i32 age
}

生成java代码 thrift --gen java test.thrift

maven 插件 maven-thrift-plug

    <build>
        <plugins>
            <plugin>
                    <groupId>org.apache.thrift.tools</groupId>
                    <artifactId>maven-thrift-plugin</artifactId>
                    <version>0.1.11</version>
                    <configuration>
                        <thriftSourceRoot>${project.basedir}/src/main/thrift</thriftSourceRoot>
                        <outputDirectory>${project.build.directory}/generated-sources/thrift</outputDirectory>
                        <generator>java</generator>
                    </configuration>
            </plugin>
        </plugins>
    </build>

image.png image.png Thrift 网络传输的时候不认可我们自定义的Object,只认可他在IDL创建的结构

  1. IDL定义 通过Thrfit 命令生成 User代码
   @Test
   public void test1() throws TException {
      com.suns.User user=new User("LISI",19);

      //序列化
      ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
      TBinaryProtocol outProtocol=new TBinaryProtocol(new TIOStreamTransport(outputStream));
      user.write(outProtocol);
      byte[] byteArray = outputStream.toByteArray();
      System.out.println("byteArray.length="+byteArray.length);

      //反序列化
      ByteArrayInputStream inputStream=new ByteArrayInputStream(byteArray);
      TBinaryProtocol inProtocol=new TBinaryProtocol(new TIOStreamTransport(inputStream));
      com.suns.User newUser=new User();
      newUser.read(inProtocol);
      System.out.println(newUser);
   }

image.png 序列化的数据 才19

Protobuf

依赖

<dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty-shaded</artifactId>
            <version>1.51.0</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
            <version>1.51.0</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
            <version>1.51.0</version>
        </dependency>
        <dependency> <!-- necessary for Java 9+ -->
            <groupId>org.apache.tomcat</groupId>
            <artifactId>annotations-api</artifactId>
            <version>6.0.53</version>
            <scope>provided</scope>
        </dependency>

插件

  <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.7.1</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:3.21.7:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.52.1:exe:${os.detected.classifier}</pluginArtifact>
                    <!--修改生成位置-->
                    <outputDirectory>${basedir}/src/main/java</outputDirectory>
                    <!--生成文件不清空之前的文件-->
                    <clearOutputDirectory>false</clearOutputDirectory>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

编写proto文件

syntax="proto3";

package com.suns;

option java_multiple_files=false;
option java_package="com.suns";
option java_outer_classname="HelloProto";

message HelloRequest{
  string name=1;
  int32 age=2;
}

生成java文件 image.png

public class TestProtobuf {
   @Test
   public void test1() throws InvalidProtocolBufferException {
      HelloProto.HelloRequest helloRequest = HelloProto.HelloRequest.newBuilder().setName("LISI").setAge(19).build();
      //序列化
      byte[] byteArray = helloRequest.toByteArray();
      System.out.println("byteArray.length="+byteArray.length);
      //反序列化
      HelloProto.HelloRequest helloRequest1 = HelloProto.HelloRequest.parseFrom(byteArray);
      System.out.println("helloRequest1.getName()="+helloRequest1.getName()+"helloRequest1.getAge()="+helloRequest1.getAge());

   }
}

image.png 与前面的序列化对比,protobuf序列化的数据最小

JSON

引入依赖

<dependency>
   <groupId>cn.hutool</groupId>
   <artifactId>hutool-all</artifactId>
   <version>5.8.20</version>
</dependency>
   @Test
   public void  test1(){
      User1 user1=new User1("LISI",19);
      //序列化
      String jsonStr = JSONUtil.toJsonStr(user1);
      byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8);
      System.out.println("bytes.length="+bytes.length);
      //反序列化
      User1 newUser = JSONUtil.toBean(jsonStr, User1.class);
      System.out.println(newUser);


   }

image.png 这个序列化的的数据大小为什么比 二进制序列化方式: jdk序列化和hessian序列化小?? 因为这个User对象的属性太少了 对于属性较多的情况下,hessian序列化的大小不会改变很多,而json会增加较多

增加字段
Hessian image.png json image.png

序列化方式的封装

public interface Serializar {
   //1. 序列化
   public byte[] serializar(Object obj) throws Exception;
   //2.反序列化
    public Object deserializar(byte[] bytes) throws Exception;

}

public class HessainSerializarImpl implements Serializar {
    @Override
    public byte[] serializar(Object obj) throws Exception {
        ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
        Hessian2Output hessian2Output=new Hessian2Output(outputStream);
        hessian2Output.writeObject(obj);
        hessian2Output.flush();
        return outputStream.toByteArray();
    }

    @Override
    public Object deserializar(byte[] bytes) throws Exception {
        ByteArrayInputStream inputStream=new ByteArrayInputStream(bytes);
        Hessian2Input hessian2Input=new Hessian2Input(inputStream);

        return hessian2Input.readObject();
    }
}

public class JDKSerializarImpl implements Serializar {
    @Override
    public byte[] serializar(Object obj) throws Exception {
        ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream=new ObjectOutputStream( outputStream);
        objectOutputStream.writeObject(obj);

        return outputStream.toByteArray();
    }

    @Override
    public Object deserializar(byte[] bytes) throws Exception {
        ByteArrayInputStream inputStream=new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream=new ObjectInputStream(inputStream);
        return   objectInputStream.readObject();
    }
}

public class JSONSerializarImpl implements Serializar {
    @Override
    public byte[] serializar(Object obj) throws Exception {
        String jsonStr = JSONUtil.toJsonStr(obj);
        return jsonStr.getBytes(Charset.forName("UTF-8"));
    }

    @Override
    public Object deserializar(byte[] bytes) throws Exception {
        String s = new String(bytes);

        return JSONUtil.toBean(s, Object.class);
    }
}

public class ProtoBufSerializarImpl implements Serializar {
    @Override
    public byte[] serializar(Object obj) throws Exception {
        //TODO User1写死了
        User1 user1=(User1) obj;
        HelloProto.HelloRequest helloRequest= HelloProto.HelloRequest.newBuilder().setName(user1.getName()).setAge(user1.getAge()).build();
        return helloRequest.toByteArray();
    }

    @Override
    public Object deserializar(byte[] bytes) throws Exception {
        HelloProto.HelloRequest helloRequest= HelloProto.HelloRequest.parseFrom(bytes);
        User1 user1=new User1(helloRequest.getName(),helloRequest.getAge());
        return user1;
    }
}

public class ThriftSerializarImpl implements Serializar {
    @Override
    public byte[] serializar(Object obj) throws Exception {
        //TODO User1写死了
        User1 user1=(User1) obj;

        com.suns.User u=new com.suns.User();
        u.setName(user1.getName());
        u.setAge(user1.getAge());
        ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
        TBinaryProtocol outProtocol=new TBinaryProtocol(new TIOStreamTransport(outputStream));
        u.write(outProtocol);
        return outputStream.toByteArray();
    }

    @Override
    public Object deserializar(byte[] bytes) throws Exception {
        ByteArrayInputStream inputStream=new ByteArrayInputStream(bytes);
        TBinaryProtocol inProtocol=new TBinaryProtocol(new TIOStreamTransport(inputStream));
        com.suns.User u=new com.suns.User();
        u.read(inProtocol);
        User1 user1=new User1();
        user1.setName(u.getName());
        user1.setAge(u.getAge());
        return user1;
    }
}

序列化与网络传输的集成

网路通信的开发: BIO,NIO,Netty

Netty 如何与 序列化方式 进行集成?

序列化--->编码 反序列化-->解码

Netty 编解码---> Handler ByteToMessageDecode 编码 MessageToByteEncode 解码 上述两个都是偏底层的编解码器

我们主要用 MessageToMessageDecoder 反序列化 MessageToMessageEncoder 序列化

注意:应用的层的编解码器 不会解决封帧的问题 所以我们在前置handler使用封帧解码器 解决半包 粘包的问题 LengthFieldBaseFrameDecoder

Netty与自定义编解码的整合 (Client ,Server)

服务端

通过Netty的ServerBootstrap 开发服务端:

public class RPCServer {
   public static void main(String[] args) {
      ServerBootstrap serverBootstrap=new ServerBootstrap();
      serverBootstrap.channel(NioServerSocketChannel.class);
      serverBootstrap.group(new NioEventLoopGroup());
      serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
         @Override
         protected void initChannel(NioSocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            //封帧
            pipeline.addLast(new LengthFieldBasedFrameDecoder(1024,0,4,0,0));
             //日志输出
            pipeline.addLast(new LoggingHandler());
             //解码器
            pipeline.addLast(new RPCMessageToMessageCodec());
            pipeline.addLast(new ChannelInboundHandlerAdapter(){
               @Override
                //处理信息
               public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                  User user=(User) msg;
                  System.out.println("user="+user);
                  super.channelRead(ctx, msg);
               }
            });
         }
      });
      serverBootstrap.bind(8000);
   }
}

客户端

通过Bootstrap开发客户端:调用服务端

public class RPCClient {

   public static void main(String[] args) throws InterruptedException {
      EventLoopGroup eventLoopGroup=new NioEventLoopGroup();

      Bootstrap bootstrap=new Bootstrap();
      bootstrap.channel(NioSocketChannel.class);
      bootstrap.group(eventLoopGroup);

      bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
         @Override
         protected void initChannel(NioSocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast(new LengthFieldBasedFrameDecoder(1024,0,4,0,0));
            pipeline.addLast(new LoggingHandler());
            pipeline.addLast(new RPCMessageToMessageCodec());
            pipeline.addLast(new ChannelInboundHandlerAdapter(){
               @Override
               public void channelActive(ChannelHandlerContext ctx) throws Exception {
                  ctx.writeAndFlush(new User("xiaohei",19));
               }
            });
         }
      });
      ChannelFuture connect = bootstrap.connect(new InetSocketAddress(8000));
      connect.sync();
   }
}

其中解码,编码handler为:

/**
 * MessageToMessageCodec<ByteBuf, User>
 *     第一个泛型: 解码器 接收的数据类型
 *             网络传输的格式[{@link ByteBuf} ]  --> java {@link Object}
 *     第二个泛型: 编码器 接收的数据类型
 *             java object--->网络传输的格式 {@link ByteBuf}
 */
@Slf4j
public class RPCMessageToMessageCodec extends MessageToMessageCodec<ByteBuf, User> {


   /**
    *  java object---> 网络传输的格式
    *  1. java  object  msg 参数获得
    *  2. 序列化  自己编写 {@link com.suns.serilizazar.Serializar}
    *  3. 把序列化后的byte[]  交给netty进行传输
    *        byte[]  --->ByteBuf --->out.add()
    */
   @Override
   protected void encode(ChannelHandlerContext ctx, User msg, List<Object> out) throws Exception {
      log.debug("编码器运行......");
      ByteBufAllocator alloc = ctx.alloc();
      ByteBuf buffer = alloc.buffer();

      Serializar serializar=new JDKSerializarImpl();
      byte[] bytes = serializar.serializar(msg);
      //数据的长度
      buffer.writeInt(bytes.length);
      buffer.writeBytes(bytes);
      out.add(buffer);

   }


   /**
    *解码器
    * 网络传输的格式---> java  object
    *       反序列化
    *       1.网络传输的格式   ByteBuf    msg ----> 解码 object
    *       2. 反序列化  {@link Serializar}  jdk
    */
   @Override
   protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
      int protocolLength= msg.readInt();
      byte[] bytes=new byte[protocolLength];
      msg.readBytes(bytes);

      Serializar serializar=new JDKSerializarImpl();
      User user = (User) serializar.deserializar(bytes);
      out.add(user);


   }
}