接上一篇《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