写一个极简的RPC和Hessian的设计
RPC(Remote Procedure Call)远程过程调用,也可以称作RMI(Remote Method Invoker),是一种client-server的形式,即一台机器调用远程机器的方法,就像执行本地方法一样。目前的远程技术有:
- Facebook开源的thrift
- Spring’s HTTP invoker
- Hessian
- JDK RMI
- WebServices
- ...
本文以一个极简的RPC实现和Spring’s HTTP invoker开始,对RPC的基本原理进行介绍,并进一步分析Hessian的的设计。
写一个极简的RPC
编写一个远程方法调用,我们可能需要做以下几件事:
1. 编写服务
从代码层面上说,我们可以把服务理解成接口,所以我们只要根据业务编写普通的Service Interface即可。在RPC的server端将实现此接口,在client端依赖此接口。
2. server端暴露服务、client端连接服务
这里就涉及到网络通信,我们可以基于传输层协议TCP、UDP,也可以基于应用层协议HTTP,这里我们实现基于HTTP协议的RPC框架。因为每一个不同的URL可以代表一个服务,所以处理方式就变为: server暴露服务对应server暴露一系列指定的URL,每个URL关联一个服务。 client连接服务即是client发送HTTP请求,调用指定的URL提供的服务。
3. client端像执行本地方法一样,调用服务
因为客户端没有实现类,只依赖服务接口,所以自然的想到使用动态代理可以解决类似本地调用的执行方式。通过URL可以指定服务,那么如何指定服务的具体方法和参数呢?答案是通过HTTP POST的RequestBody: 1):client端将方法和参数写入Body中 2):server端从Body中解析方法和参数,调用服务的实现,响应返回值 3):client端取得返回值 这里有个重点,就是网络传输数据的序列化和反序列化问题。

Server端实现
服务端我们采用Sevlet的形式,每配置一个Servlet就代表一个服务,每个Servlet需要初始化两个参数:service服务实现类和serviceInterface服务接口。序列化和反序列化采用JDK自带的Object**putStream。
package com.deepoove.onerpc.server;
import static com.deepoove.onerpc.util.NameMangle.mangleName;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class OneRpcServlet extends HttpServlet {
private static final long serialVersionUID = -6904007509665776812L;
private Class<?> serviceInterface;
private Object service;
private Map<String, Method> methodNameMaping = new HashMap<>();
@Override
public void init(ServletConfig config) throws ServletException {
String serviceInterfaceName = config.getInitParameter("serviceInterface");
String serviceClassName = config.getInitParameter("service");
try {
//根据servlet参数初始化服务和服务实现类
serviceInterface = loadClass(serviceInterfaceName);
Class<?> serviceClass = loadClass(serviceClassName);
service = serviceClass.newInstance();
//构造服务方法名称和方法的映射
Method[] methods = serviceInterface.getMethods();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
methodNameMaping.put(mangleName(method), method);
}
} catch (Exception e) {
throw new ServletException(e);
}
}
private Class<?> loadClass(String serviceInterfaceName) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (null != classLoader) {
return Class.forName(serviceInterfaceName, false, classLoader);
} else {
return Class.forName(serviceInterfaceName);
}
}
@Override
public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
//限定为post方法
String httpmethod = req.getMethod();
if (!"POST".equals(httpmethod)) {
res.setStatus(500);
PrintWriter out = res.getWriter();
res.setContentType("text/html");
out.println("<h1>Requires POST</h1>");
out.close();
return;
}
ServletInputStream inputStream = req.getInputStream();
ServletOutputStream outputStream = res.getOutputStream();
ObjectInputStream ois = new ObjectInputStream(inputStream);
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
try {
//使用JDK自带的反序列化:读取方法名和参数
String methodName = (String) ois.readObject();
int length = ois.readInt();
Object[] args = new Object[length];
for (int i = 0; i < length; i++) {
args[i] = ois.readObject();
}
//调用指定方法,获得结果
Method method = methodNameMaping.get(methodName);
Object result = method.invoke(service, args);
//序列化方法返回值,写入response流
oos.writeObject(result);
oos.flush();
} catch (Exception e) {
throw new ServletException(e);
}
finally {
ois.close();
oos.close();
}
}
}
Client端实现
客户端需要指定服务的URL和服务接口,在调用服务接口指定方法时,将方法名和参数写入request body中,获得返回值后,反序列化读取返回值。
package com.deepoove.onerpc.client;
import static com.deepoove.onerpc.util.NameMangle.mangleName;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
public class OneRpcClient implements InvocationHandler {
private URL url;
private Class<?> serviceInterface;
private OneRpcClient() {}
/**
* 采用动态代理,获取服务接口的实例
*/
public static <T> T create(Class<T> serviceInterface, String url) throws MalformedURLException {
OneRpcClient client = new OneRpcClient();
client.url = new URL(url);
client.serviceInterface = serviceInterface;
ClassLoader loader = Thread.currentThread().getContextClassLoader();
return (T) Proxy.newProxyInstance(loader, new Class<?>[] { client.serviceInterface },
client);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
URLConnection conn = url.openConnection();
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "x-application/onerpc");
conn.setRequestProperty("Accept-Encoding", "deflate");
OutputStream outputStream = conn.getOutputStream();
// 将方法名和参数序列化
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
oos.writeObject(mangleName(method));
oos.writeInt(args.length);
for (int i = 0; i < args.length; i++) {
oos.writeObject(args[i]);
}
oos.flush();
// 获得HTTP返回信息
HttpURLConnection httpConn = (HttpURLConnection) conn;
int _statusCode = 500;
try {
_statusCode = httpConn.getResponseCode();
} catch (Exception e) {}
if (_statusCode != 200) {
throw new RuntimeException("code:" + _statusCode);
} else {
// 将返回值反序列化
InputStream inputStream = conn.getInputStream();
ObjectInputStream ois = new ObjectInputStream(inputStream);
Object readObject = ois.readObject();
ois.close();
return readObject;
}
}
}
demo
我们使用了上述两个类即完成了一个极其基础的RPC框架。接下来看看我们怎么使用:
- 编写服务 简单的服务,User也是个简单的对象,注意要实现Serializable接口。服务端和客户端都依赖此服务接口。
package com.deepoove.hessian.api.service;
import com.deepoove.hessian.api.pojo.User;
public interface UserService {
User get(String id);
User get(int id);
void add(User user);
}
- 编写服务实现 这也是个简单的服务实现。
package com.deepoove.hessian.example.service;
import com.deepoove.hessian.api.pojo.User;
import com.deepoove.hessian.api.service.UserService;
public class UserServiceImpl implements UserService {
@Override
public User get(String id) {
User user = new User();
user.setName("Sayi");
System.out.println("string method");
return user;
}
@Override
public void add(User user) {}
@Override
public User get(int id) {
System.out.println("int method");
return null;
}
}
- 暴露服务其实就是配置Servlet即可,并且制定服务接口和服务实现类。启动web容器,服务地址如:
http://127.0.0.1:8077/userService
<servlet>
<servlet-name>userService</servlet-name>
<servlet-class>com.deepoove.onerpc.server.OneRpcServlet</servlet-class>
<init-param>
<param-name>serviceInterface</param-name>
<param-value>com.deepoove.hessian.api.service.UserService</param-value>
</init-param>
<init-param>
<param-name>service</param-name>
<param-value>com.deepoove.hessian.example.service.UserServiceImpl</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>userService</servlet-name>
<url-pattern>/userService</url-pattern>
</servlet-mapping>
- 写个客户端,调用服务 客户端依赖服务接口,通过服务接口和URL创建接口实例。
public static void main(String[] args) throws MalformedURLException {
UserService userService = OneRpcClient.create(UserService.class, "http://127.0.0.1:8077/userService");
System.out.println(userService.get("").getName());
}
至此,一个极简的RPC已经完成,它是基于HTTP协议进行网络传输的,并且使用JDK自带的序列化工具进行数据的序列化和反序列化,数据结构的协议格式可以认为就是方法名和参数。
Spring’s HTTP invoker
上文中的代码是个极其简陋,而Spring’s HTTP invoker也是基于HTTP协议和Java序列化的,它用spring的风格设计了远程调用模块。我们先看下Server端的核心源码:
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
RemoteInvocation invocation = readRemoteInvocation(request);
RemoteInvocationResult result = invokeAndCreateResult(invocation, getProxy());
writeRemoteInvocationResult(request, response, result);
}
catch (ClassNotFoundException ex) {
throw new NestedServletException("Class not found during deserialization", ex);
}
}
Spring’s HTTP invoker对方法名、参数和返回值的数据结构进行了封装,分别是RemoteInvocation和RemoteInvocationResult。从源码中可以看出,服务端也是从request反序列化数据RemoteInvocation,然后调用服务的具体方法,最后序列化返回值RemoteInvocationResult到response流中。
在网络传输中,Java序列化的性能是无法满意的,我们有理由选择更优秀的序列化方案。
Hessian的设计
Hessian同样是基于应用层HTTP协议进行传输的,序列化采用了自有的Hessian二进制序列化,数据格式上使用了Hessian协议。
Hessian的协议:
top ::= version content
::= call-1.0
::= reply-1.0
# RPC-style call
call ::= 'C' string int value*
call-1.0 ::= 'c' x01 x00 <hessian-1.0-call>
content ::= call # rpc call
::= fault # rpc fault reply
::= reply # rpc value reply
::= packet+ # streaming packet data
::= envelope+ # envelope wrapping content
envelope ::= 'E' string env-chunk* 'Z'
env-chunk ::= int (string value)* packet int (string value)*
# RPC fault
fault ::= 'F' (value value)* 'Z'
# message/streaming message
packet ::= (x4f b1 b0 <data>)* packet
::= 'P' b1 b0 <data>
::= [x70 - x7f] <data>
::= [x80 - xff] <data>
# RPC reply
reply ::= 'R' value
reply-1.0 ::= 'r' x01 x00 <hessian-1.0-reply>
version ::= 'H' x02 x00
Hessian协议的设计目标首先它是不依赖任何IDL或者Scheme的,协议对于应用应该是透明的。其次是语言无关的,这样便于支持多语言的RPC。更多协议信息参见官网http://hessian.caucho.com/doc/hessian-ws.html
Hessian协议带有版本信息,区分Hessian的不同版本。协议中也规定了Call和Reply信息。
Hessian的序列化反序列化
在包com.caucho.hessian.io下包含了大量有关的类,它们实现了一个自描述、语言无关的序列化方案。它们也是仅仅依赖JDK的,所以可以在任何地方仅仅使用Hessian的序列化功能。
Hessian的实现
Hessian的实现无非就是hessian协议的实现、序列化反序列化的实现、以及RPC的实现。HessianSkeleton定义了服务端主要的功能,它负责调用指定方法。HessianProxyFactory工厂则提供了获得服务接口动态代理的功能,默认是不支持方法重载的,可以调用factory.setOverloadEnabled(true);方法,支持命名修饰(name mangle)。
Hessian与Spring Remoting
我们知道,大多数框架具有普适性,它定义了大多数人要使用的功能,为一部分人提供了功能的配置,而没有为少数人提供个性化的功能。Hessian本身服务端是基于Servlet的,Spring的org.springframework.remoting.caucho.HessianServiceExporter 可以透明的暴露服务,org.springframework.remoting.caucho.HessianProxyFactoryBean 可以方便的建立对应的服务代理Bean。
HessianServiceExporter的父类RemoteExporter为服务端提供了拦截器的功能,这样我们可以实现自己的拦截器,打印参数日志,耗时,如果公司内部返回值含有code,也可以打印返回值code等信息。我相信,大部分使用Hessian的公司都会定制这方面的功能。
Hessian缺乏一个重试机制,我见过很多使用Hessian的代码都是在业务系统写满了while循坏,以达到超时重试的目的,这份工作我们可以自定义Hessian客户端的HTTP请求来实现。
More 更多
本文所述的RPC都是基于HTTP协议的,HTTP本身是个应用层协议,我们可以基于传输层TCP协议实现,技术选型可以使用Netty。序列化的选型我们可以类比下Hessian序列化、protobuf和Java 序列化的优缺点。
随着业务系统,微服务的增加,我们发现这样的RPC有着明显的缺点,服务分散在各地,缺乏统一管理,服务注册服务治理的技术越来越流行。国内的Dubbo支持多transporter(mina, netty, grizzy)和多protocol(dubbo、hessian、http、thrift、rmi等),正在成为佼佼者。