【RPC高性能框架总结】8.手写rpc框架-代码实现(一)

接上一篇《7.手写rpc框架-整体设计思路
上一篇我们介绍了整个手写RPC的设计思路,本篇我们来动手实现手写rpc框架的代码。这里我们先从框架层开始写起。

我们先来回顾一下之前的工程结构:

我们分别从客户端框架、服务端框架、公共模块,一直到应用层模块的顺序开始编写。
注:代码参考http://git.oschina.net/huangyong/rpc(作者:黄勇)

一、rpc顶级父工程编写

我们的工程使用maven结构,框架层的所有工程需要依赖一个父级工程,该父级工程不编写任何代码,只用来设置框架所有工程需要的pom依赖,以及设定依赖的相关版本、打包编译的jdk版本和规则。
首先打开MyEclipse,新建名为rpc-framework的maven工程:



这里要注意的是,因为该工程为引用的父类工程,所以打包类型选择为“pom”类型。
新建成功之后,作为父级工程,最重要的就是指定后面需要的各类依赖的具体版本,所以该工程不通过maven实际拉取jar包,而是通过dependencyManagement进行依赖版本的管理。
对于我们要编写的整个框架,需要以下功能:Spring类加载机制、netty框架进行网络交互、log4j的日志功能,Apache的common工具包、protostuff序列化工具、objenesis实例化class对象、cglib实现动态代理、zookeeper注册中心的连接机制,所以在pom.xml中,我们引入以下依赖的版本控制:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.xxx.rpc</groupId>
  <artifactId>rpc-framework</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>pom</packaging>
  
  <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <version.slf4j>1.7.10</version.slf4j>
        <version.spring>4.1.5.RELEASE</version.spring>
        <version.netty>4.0.26.Final</version.netty>
        <version.protostuff>1.0.9</version.protostuff>
        <version.objenesis>2.1</version.objenesis>
        <version.cglib>3.1</version.cglib>
        <version.zookeeper>3.4.6</version.zookeeper>
        <version.zkclient>0.4</version.zkclient>
        <version.commons-lang>3.3.2</version.commons-lang>
        <version.commons-collections>4.0</version.commons-collections>
        <version.maven-compiler-plugin>3.2</version.maven-compiler-plugin>
        <version.maven-surefire-plugin>2.18.1</version.maven-surefire-plugin>
        <version.maven-source-plugin>2.4</version.maven-source-plugin>
        <version.maven-javadoc-plugin>2.10.3</version.maven-javadoc-plugin>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- SLF4J -->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
                <version>${version.slf4j}</version>
            </dependency>
            <!-- Spring -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>${version.spring}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <version>${version.spring}</version>
                <scope>test</scope>
            </dependency>
            <!-- Netty -->
            <dependency>
                <groupId>io.netty</groupId>
                <artifactId>netty-all</artifactId>
                <version>${version.netty}</version>
            </dependency>
            <!-- Protostuff -->
            <dependency>
                <groupId>com.dyuproject.protostuff</groupId>
                <artifactId>protostuff-core</artifactId>
                <version>${version.protostuff}</version>
            </dependency>
            <dependency>
                <groupId>com.dyuproject.protostuff</groupId>
                <artifactId>protostuff-runtime</artifactId>
                <version>${version.protostuff}</version>
            </dependency>
            <!-- Objenesis -->
            <dependency>
                <groupId>org.objenesis</groupId>
                <artifactId>objenesis</artifactId>
                <version>${version.objenesis}</version>
            </dependency>
            <!-- CGLib -->
            <dependency>
                <groupId>cglib</groupId>
                <artifactId>cglib</artifactId>
                <version>${version.cglib}</version>
            </dependency>
            <!-- ZooKeeper -->
            <dependency>
                <groupId>org.apache.zookeeper</groupId>
                <artifactId>zookeeper</artifactId>
                <version>${version.zookeeper}</version>
            </dependency>
            <!-- ZkClient -->
            <dependency>
                <groupId>com.101tec</groupId>
                <artifactId>zkclient</artifactId>
                <version>${version.zkclient}</version>
            </dependency>
            <!-- Apache Commons Lang -->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${version.commons-lang}</version>
            </dependency>
            <!-- Apache Commons Collections -->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-collections4</artifactId>
                <version>${version.commons-collections}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <!-- Compiler -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${version.maven-compiler-plugin}</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <!-- Test -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${version.maven-surefire-plugin}</version>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>release</id>
            <build>
                <plugins>
                    <!-- Source -->
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-source-plugin</artifactId>
                        <version>${version.maven-source-plugin}</version>
                        <executions>
                            <execution>
                                <phase>package</phase>
                                <goals>
                                    <goal>jar-no-fork</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                    <!-- Javadoc -->
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-javadoc-plugin</artifactId>
                        <version>${version.maven-javadoc-plugin}</version>
                        <executions>
                            <execution>
                                <phase>package</phase>
                                <goals>
                                    <goal>jar</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

    <modules>
        <module>rpc-common</module>
        <module>rpc-server</module>
        <module>rpc-client</module>
        <module>rpc-registry</module>
        <module>rpc-registry-zookeeper</module>
    </modules>
  
</project>

后面的build设置了整个工程群的编译版本以及跳过单元测试的规则。在profiles这只了相关的源码以及文档的打包规则,最后的modules设置了父类包含的子集工程信息。
工程结构:

此时父级工程设置完毕,可以编写下面的子集工程了。

二、rpc-common编写

为什么先来写rpc-common,因为该工程作为公共模块,除了做了信息的编码以及解码工作外,还提供了一些常用的工具类()。
废话不多说,开始编写rpc-common的代码。
首先打开MyEclipse,新建名为rpc-common的maven工程:

新建成功之后,因为我们需要netty框架进行网络交互、log4j的日志功能,Apache的common工具包、protostuff序列化工具、objenesis实例化class对象,以及父级工程的版本控制,所以在pom.xml中,我们引入以下依赖:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  
  <modelVersion>4.0.0</modelVersion>
  
  <parent>
      <groupId>com.xxx.rpc</groupId>
      <artifactId>rpc-framework</artifactId>
      <version>0.0.1-SNAPSHOT</version>
  </parent>
  
  <artifactId>rpc-common</artifactId>
  
  <dependencies>
       <!-- SLF4J -->
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-log4j12</artifactId>
       </dependency>
       <!-- Netty -->
       <dependency>
           <groupId>io.netty</groupId>
           <artifactId>netty-all</artifactId>
       </dependency>
       <!-- Protostuff -->
       <dependency>
           <groupId>com.dyuproject.protostuff</groupId>
           <artifactId>protostuff-core</artifactId>
       </dependency>
       <dependency>
           <groupId>com.dyuproject.protostuff</groupId>
           <artifactId>protostuff-runtime</artifactId>
       </dependency>
       <!-- Objenesis -->
       <dependency>
           <groupId>org.objenesis</groupId>
           <artifactId>objenesis</artifactId>
       </dependency>
       <!-- Apache Commons Lang -->
       <dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-lang3</artifactId>
       </dependency>
       <!-- Apache Commons Collections -->
       <dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-collections4</artifactId>
       </dependency>
   </dependencies>
</project>

其中parent是设置该工程所属的父级工程,dependencies是本工程需要的依赖。
工程结构:

然后我们开始编写代码,首先在src/main/java中创建com.xxx.rpc.common.bean包下的RpcRequest和RpcResponse:

这两个类是用来封装在RPC的情况下,Netty需要传输和接收的数据集对象:
RpcRequest:

package com.xxx.rpc.common.bean;

public class RpcRequest {
    private String requestId;//请求ID
    private String interfaceName;//接口名称
    private String serviceVersion;//服务版本
    private String methodName;//需要调用的方法名
    private Class<?>[] paramterTypes;//方法参数类型
    private Object[] parameters;//方法参数
    public String getRequestId() {
        return requestId;
    }
    public void setRequestId(String requestId) {
        this.requestId = requestId;
    }
    public String getInterfaceName() {
        return interfaceName;
    }
    public void setInterfaceName(String interfaceName) {
        this.interfaceName = interfaceName;
    }
    public String getServiceVersion() {
        return serviceVersion;
    }
    public void setServiceVersion(String serviceVersion) {
        this.serviceVersion = serviceVersion;
    }
    public String getMethodName() {
        return methodName;
    }
    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }
    public Class<?>[] getParamterTypes() {
        return paramterTypes;
    }
    public void setParamterTypes(Class<?>[] paramterTypes) {
        this.paramterTypes = paramterTypes;
    }
    public Object[] getParameters() {
        return parameters;
    }
    public void setParameters(Object[] parameters) {
        this.parameters = parameters;
    }
}

这里分别有requestId、interfaceName、serviceVersion、methodName、paramterTypes以及parameters参数,因为要区分每一次的调用,所以每一次远程调用有一个独一无二的requestId;因为我们需要实现调用远程方法,所以这里要指定需要调用的接口名称interfaceName以及版本serviceVersion,而我们调用的是某个接口的具体方法,所以要制定调用的方法名methodName,调用方法时我们可能还需要传入参数,所以这里要指定传输参数的类型,以及传输参数的具体对象。

RpcResponse:

package com.xxx.rpc.common.bean;

public class RpcResponse {
    private String requestId;//请求ID
    private Exception exception;//反馈的异常对象
    private Object result;//调用远程方法返回的具体对象
    
    public boolean hasException() {
        return exception != null;
    }
    public String getRequestId() {
        return requestId;
    }
    public void setRequestId(String requestId) {
        this.requestId = requestId;
    }
    public Exception getException() {
        return exception;
    }
    public void setException(Exception exception) {
        this.exception = exception;
    }
    public Object getResult() {
        return result;
    }
    public void setResult(Object result) {
        this.result = result;
    }
}

反馈的信息简单一些,分别是每次远程调用独一无二的requestId,以及在调用过程中,产生的异常信息对象exception,和最终调用远程方法反馈的结果对象result。编写完请求和接受的传输对象后,就要来编写编码和解码的相关类了。
在src/main/java中创建com.xxx.rpc.common.codec包下的RpcDecoder和RpcEncoder:

首先是解码器RpcDecoder,用来将传输回来的信息进行解码、反序列化操作:

package com.xxx.rpc.common.codec;

import java.util.List;

import org.apache.commons.lang3.SerializationUtils;

import com.xxx.rpc.common.utils.SerializationUtil;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

//rpc 解码器
public class RpcDecoder extends ByteToMessageDecoder{

	private Class<?> genericClass;//需要进行解码的类
	
	public RpcDecoder(Class<?> genericClass){
		this.genericClass = genericClass;
	}
	
	@Override
	protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
		if(in.readableBytes() < 4){
			/*ByteBuf分为两部分,一部分为消息头head,一部分为消息体body,消息头head中保存的是可读信息的长度(一个int类型的值),
			 * 当可读信息小于4byte(一个int类型的值)时,认为消息头中没有刻可读信息的长度,即消息体body长度不存在,或者ByteBuf结构异常。放弃解码*/
			return;
		}
		in.markReaderIndex();//标记一下当前的读索引的位置
		int dataLength = in.readInt();//获取信息的总长度,这里需要注意的是ByteBuf的readInt()方法会让他的readIndex增加4
		if(in.readableBytes() < dataLength){
			//由于ByteBuf的readInt()方法会让他的readIndex增加4,此时可读区域就会比原来小4,这个时候就需要将可读索引恢复到4之前的位置
			//如果可读信息的长度小于信息的总长度,还原读索引的位置(resetReaderIndex这个方法是配合markReaderIndex使用的。把readIndex重置到mark的地方)
			in.resetReaderIndex();
			return;
		}
		byte[] data = new byte[dataLength];
		in.readBytes(data);
		out.add(SerializationUtil.deserialize(data,genericClass));
	}

}

这里继承了netty的ByteToMessageDecoder解码器类,和之前几篇我们讲解netty时使用的一样,它是一个InHandler,我们在公共类编写,为了让子工程可以将解码、编码器类方便的引入到pipeline的处理器链中。
少量数据我们可以直接读,但是大量数据,就涉及到断点续读的情况(多线程情况分次读取),此时就需要根据标记进行续读,这里我们在decode对ByteBuf进行了一系列断点续读的操作。
在该类中一开始判断了in.readableBytes()可读信息的大小,当小于4byte(一个int类型)的值的时候,不处理。
然后标记当前“读索引”的位置(即数据读到了byte字节码的哪个位置,续读的时候,需要从上次结束的地方继续读,这里“读索引”就是上次标记的位置),当前“读索引”位置在ByteBuf中的结构如下(详细说明见《6.高性能nio框架netty(下)》):

这里进行markReaderIndex主要为了后面如果不符合读的条件时,恢复之前“读索引”的位置。

通过readInt()方法获取送过来的消息的长度,将可读的byte大小与传送过来的信息的总长度对比,如果小于,则将索引重置到mark的地方。
后面就是创建dataLength大小的byte数组,将信息从ByteBuf读入ByteBuf,然后将byte数据转化为我们需要的对象。

最后的SerializationUtil是后面我们在工具包utils中编写的工具类,用来类的序列化和序列化的具体操作。然后是解码器RpcEncoder,用来将将需要传输的信息进行编码、序列化操作:

package com.xxx.rpc.common.codec;

import com.xxx.rpc.common.utils.SerializationUtil;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

// rpc 编码器
public class RpcEncoder extends MessageToByteEncoder{
    private Class<?> genericClass;
    
    public RpcEncoder(Class<?> genericClass){
        this.genericClass = genericClass;
    }

    @Override
    protected void encode(ChannelHandlerContext ctx, Object in, ByteBuf out) throws Exception {
        if(genericClass.isInstance(in)){//检测Object这个对象能不能被转化为这个类
            byte[] data = SerializationUtil.serialize(in);//将Object序列化为byte
            out.writeInt(data.length);//标记写入信息的长度
            out.writeBytes(data);//写入信息
        }
    }
}

这里继承了netty的MessageToByteEncoder编码器类,它是一个OutHandler,同样可以让子工程将该编码器引入到pipeline的处理器链中。

上面用到了SerializationUtil进行序列化以及反序列化的操作,我们在src/main/java中创建com.xxx.rpc.common.utils包,在下面放置SerializationUtil、StringUtil、CollectionUtil三个工具类,除了SerializationUtil外,StringUtil和CollectionUtil为分别用来处理String字符串和集合的工具类:

其中SerializationUtil:

package com.xxx.rpc.common.utils;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;

import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.Schema;
import com.dyuproject.protostuff.runtime.RuntimeSchema;

//序列化工具类(基于 Protostuff 实现)
public class SerializationUtil {
    private static Map<Class<?>, Schema<?>> cachedSchema = new ConcurrentHashMap<>();

    private static Objenesis objenesis = new ObjenesisStd(true);

    private SerializationUtil() { }

    /**
     * 序列化(对象 -> 字节数组)
     */
    @SuppressWarnings("unchecked")
    public static <T> byte[] serialize(T obj) {
        Class<T> cls = (Class<T>) obj.getClass();
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        try {
            Schema<T> schema = getSchema(cls);
            return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        } finally {
            buffer.clear();
        }
    }

    /**
     * 反序列化(字节数组 -> 对象)
     */
    public static <T> T deserialize(byte[] data, Class<T> cls) {
        try {
            T message = objenesis.newInstance(cls);
            Schema<T> schema = getSchema(cls);
            ProtostuffIOUtil.mergeFrom(data, message, schema);
            return message;
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

    @SuppressWarnings("unchecked")
    private static <T> Schema<T> getSchema(Class<T> cls) {
        Schema<T> schema = (Schema<T>) cachedSchema.get(cls);
        if (schema == null) {
            schema = RuntimeSchema.createFrom(cls);
            cachedSchema.put(cls, schema);
        }
        return schema;
    }
}

在该类中,有三个方法,分别是serialize、deserialize以及getSchema。其中serialize和deserialize分别用来处理序列化以及反序列化的操作,getSchema用来获取类的概要信息。一个字节数组序列化成一个类,需要类本身的字节数组(byte)、类的类型(T),以及概要(Schema)。这里cachedSchema用来保存类的概要(Schema),是一个Map结构,通过class类型来获取其Schema,如果之前没有,就使用RuntimeSchema新建该类的Schema,如果之前存在,就直接取,即使用过的类对应的schema能被缓存起来。ProtostuffIOUtil就是最终进行字节流转换为类,类转换为字节流的工具类,和我们之前在《6.高性能nio框架netty(下)》中编写的ByteObjConverter功能类似。然后是StringUtil:

package com.xxx.rpc.common.utils;

import org.apache.commons.lang3.StringUtils;

//字符串工具类
public class StringUtil {
     /**
     * 判断字符串是否为空
     */
    public static boolean isEmpty(String str) {
        if (str != null) {
            str = str.trim();
        }
        return StringUtils.isEmpty(str);
    }

    /**
     * 判断字符串是否非空
     */
    public static boolean isNotEmpty(String str) {
        return !isEmpty(str);
    }

    /**
     * 分割固定格式的字符串
     */
    public static String[] split(String str, String separator) {
        return StringUtils.splitByWholeSeparator(str, separator);
    }
}

这里就很简单,编写了判断字符串是否为空的方法,以及分割固定格式字符串的方法。最后是集合工具类CollectionUtil:

package com.xxx.rpc.common.utils;

import java.util.Collection;
import java.util.Map;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;

//集合工具类
public class CollectionUtil {

    /**
     * 判断 Collection 是否为空
     */
    public static boolean isEmpty(Collection<?> collection) {
        return CollectionUtils.isEmpty(collection);
    }

    /**
     * 判断 Collection 是否非空
     */
    public static boolean isNotEmpty(Collection<?> collection) {
        return !isEmpty(collection);
    }

    /**
     * 判断 Map 是否为空
     */
    public static boolean isEmpty(Map<?, ?> map) {
        return MapUtils.isEmpty(map);
    }

    /**
     * 判断 Map 是否非空
     */
    public static boolean isNotEmpty(Map<?, ?> map) {
        return !isEmpty(map);
    }
}

里面分别编写了判断Collection类型和Map类型的容器是否为空的方法。

至此我们的rpc-common工程编写完毕,下一篇我们来编写rpc-client。
转载请注明出处:https://blog.csdn.net/acmman/article/details/88378912

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

光仔December

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值