Open Sayi opened 6 years ago
RPC(Remote Procedure Call)远程过程调用,也可以称作RMI(Remote Method Invoker),是一种client-server的形式,即一台机器调用远程机器的方法,就像执行本地方法一样。目前的远程技术有: Facebook开源的thrift Spring’s HTTP invoker Hessian JDK RMI WebServices ...
RPC(Remote Procedure Call)远程过程调用,也可以称作RMI(Remote Method Invoker),是一种client-server的形式,即一台机器调用远程机器的方法,就像执行本地方法一样。目前的远程技术有:
本文以一个极简的RPC实现和Spring’s HTTP invoker开始,对RPC的基本原理进行介绍,并进一步分析Hessian的的设计。
编写一个远程方法调用,我们可能需要做以下几件事:
从代码层面上说,我们可以把服务理解成接口,所以我们只要根据业务编写普通的Service Interface即可。在RPC的server端将实现此接口,在client端依赖此接口。
这里就涉及到网络通信,我们可以基于传输层协议TCP、UDP,也可以基于应用层协议HTTP,这里我们实现基于HTTP协议的RPC框架。因为每一个不同的URL可以代表一个服务,所以处理方式就变为: server暴露服务对应server暴露一系列指定的URL,每个URL关联一个服务。 client连接服务即是client发送HTTP请求,调用指定的URL提供的服务。
因为客户端没有实现类,只依赖服务接口,所以自然的想到使用动态代理可以解决类似本地调用的执行方式。通过URL可以指定服务,那么如何指定服务的具体方法和参数呢?答案是通过HTTP POST的RequestBody: 1):client端将方法和参数写入Body中 2):server端从Body中解析方法和参数,调用服务的实现,响应返回值 3):client端取得返回值 这里有个重点,就是网络传输数据的序列化和反序列化问题。
服务端我们采用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(); } } }
客户端需要指定服务的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; } } }
我们使用了上述两个类即完成了一个极其基础的RPC框架。接下来看看我们怎么使用:
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);
}
2. 编写服务实现 这也是个简单的服务实现。 ```java 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; } }
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>
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也是基于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同样是基于应用层HTTP协议进行传输的,序列化采用了自有的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信息。
在包com.caucho.hessian.io下包含了大量有关的类,它们实现了一个自描述、语言无关的序列化方案。它们也是仅仅依赖JDK的,所以可以在任何地方仅仅使用Hessian的序列化功能。
com.caucho.hessian.io
Hessian的实现无非就是hessian协议的实现、序列化反序列化的实现、以及RPC的实现。HessianSkeleton定义了服务端主要的功能,它负责调用指定方法。HessianProxyFactory工厂则提供了获得服务接口动态代理的功能,默认是不支持方法重载的,可以调用factory.setOverloadEnabled(true);方法,支持命名修饰(name mangle)。
HessianSkeleton
HessianProxyFactory
factory.setOverloadEnabled(true);
我们知道,大多数框架具有普适性,它定义了大多数人要使用的功能,为一部分人提供了功能的配置,而没有为少数人提供个性化的功能。Hessian本身服务端是基于Servlet的,Spring的org.springframework.remoting.caucho.HessianServiceExporter可以透明的暴露服务,org.springframework.remoting.caucho.HessianProxyFactoryBean可以方便的建立对应的服务代理Bean。
org.springframework.remoting.caucho.HessianServiceExporter
org.springframework.remoting.caucho.HessianProxyFactoryBean
HessianServiceExporter的父类RemoteExporter为服务端提供了拦截器的功能,这样我们可以实现自己的拦截器,打印参数日志,耗时,如果公司内部返回值含有code,也可以打印返回值code等信息。我相信,大部分使用Hessian的公司都会定制这方面的功能。
HessianServiceExporter
RemoteExporter
Hessian缺乏一个重试机制,我见过很多使用Hessian的代码都是在业务系统写满了while循坏,以达到超时重试的目的,这份工作我们可以自定义Hessian客户端的HTTP请求来实现。
本文所述的RPC都是基于HTTP协议的,HTTP本身是个应用层协议,我们可以基于传输层TCP协议实现,技术选型可以使用Netty。序列化的选型我们可以类比下Hessian序列化、protobuf和Java 序列化的优缺点。
随着业务系统,微服务的增加,我们发现这样的RPC有着明显的缺点,服务分散在各地,缺乏统一管理,服务注册服务治理的技术越来越流行。国内的Dubbo支持多transporter(mina, netty, grizzy)和多protocol(dubbo、hessian、http、thrift、rmi等),正在成为佼佼者。
本文以一个极简的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。
Client端实现
客户端需要指定服务的URL和服务接口,在调用服务接口指定方法时,将方法名和参数写入request body中,获得返回值后,反序列化读取返回值。
demo
我们使用了上述两个类即完成了一个极其基础的RPC框架。接下来看看我们怎么使用:
import com.deepoove.hessian.api.pojo.User;
public interface UserService {
}
http://127.0.0.1:8077/userService
至此,一个极简的RPC已经完成,它是基于HTTP协议进行网络传输的,并且使用JDK自带的序列化工具进行数据的序列化和反序列化,数据结构的协议格式可以认为就是方法名和参数。
Spring’s HTTP invoker
上文中的代码是个极其简陋,而Spring’s HTTP invoker也是基于HTTP协议和Java序列化的,它用spring的风格设计了远程调用模块。我们先看下Server端的核心源码:
Spring’s HTTP invoker对方法名、参数和返回值的数据结构进行了封装,分别是RemoteInvocation和RemoteInvocationResult。从源码中可以看出,服务端也是从request反序列化数据RemoteInvocation,然后调用服务的具体方法,最后序列化返回值RemoteInvocationResult到response流中。
在网络传输中,Java序列化的性能是无法满意的,我们有理由选择更优秀的序列化方案。
Hessian的设计
Hessian同样是基于应用层HTTP协议进行传输的,序列化采用了自有的Hessian二进制序列化,数据格式上使用了Hessian协议。
Hessian的协议:
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等),正在成为佼佼者。