Sayi / sayi.github.com

个人博客已切换到公众号Evoopeed,请搜索:deepoove
44 stars 7 forks source link

写一个极简的RPC和Hessian的设计 #26

Open Sayi opened 6 years ago

Sayi commented 6 years ago

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端取得返回值 这里有个重点,就是网络传输数据的序列化和反序列化问题

image

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框架。接下来看看我们怎么使用:

  1. 编写服务 简单的服务,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);

}

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;
    }

}
  1. 暴露服务其实就是配置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>
  1. 写个客户端,调用服务 客户端依赖服务接口,通过服务接口和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等),正在成为佼佼者。