深入理解RPC之手寫RPC框架

一. RPC是什麼

RPC全稱remote procedure call,翻譯過來就是遠程過程調用。在分佈式系統中,一個模塊像調用本地方法一樣調用遠程方法的過程,就叫RPC。
我們耳熟能詳的webservice、restful接口調用都是RPC,只是消息的組織方式和消息協議不同。
爲了加深對RPC的理解,我手寫了一個簡單的RPC框架,完整的代碼已上傳至https://github.com/RingWu2020/ym-rpc

二.RPC流程

在這裏插入圖片描述
RPC的流程大致如上圖所示:

  1. 客戶端調用client stub(client stub位於本地,就和調用本地方法一樣),傳遞參數
  2. clientstub將參數編組爲消息,然後通過系統調用向服務的發送消息;
  3. 客戶端本地操作系統將消息從客戶端機器發送到服務端機器;
  4. 服務的操作系統將接收到的數據包傳遞給Server stub;
  5. Server stub解組消息爲參數;
  6. Server stub再調用服務端的過程,過程執行結果以相同的方式回傳給客戶端。

三. RPC協議

涉及到通信,自然需要協議,比如tcp、udp、http、ftp、sftp等等;相信有很多小夥伴也馬上聯想到了netty,事實上,很多RPC框架都用到了netty。

常用的RPC協議有SOAP、XML-RPC、JSON-RPC、JSON-WSP,傳統的webservice框架apache cxf、apache axis2等大多基於標準的SOAP協議。而很多新興的框架,譬如dubbo支持多種協議。

四. 手寫RPC框架

在瞭解了RPC的基本原理後,嘗試來寫一個RPC框架。一方面可以加深對RPC的理解,另外一方面,也可以發現RPC過程中一些細節上的問題,對於閱讀開源RPC框架的源碼很有幫助。
在開發前,需要說明的是,用戶在使用RPC框架的時候,過程接口的定義,接口的調用,接口的實現都是用戶去完成的。

客戶端代碼開發

結合前面的RPC流程,rpc客戶端需要完成的功能的有:

  1. 生成接口的代理對象
  2. 從註冊中心發現服務
  3. 將請求編組爲消息發送到服務端

1.生成接口的代理對象

這裏使用jdk的動態代理生成接口的代理對象,生成代理對象的工廠類,代碼如下

package cn.wym.rpc.client;


import cn.wym.rpc.discovery.ServiceInfoDiscoverer;
import cn.wym.rpc.discovery.ZookeeperServiceInfoDiscoverer;
import lombok.Getter;
import lombok.Setter;

import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

@Getter@Setter
public class ClientStubProxyFactory {

    //服務發現
    private ServiceInfoDiscoverer serviceInfoDiscoverer= new ZookeeperServiceInfoDiscoverer();

    //代理對象緩存,避免每次都新建
    private Map<Class<?>, Object> objectCache = new HashMap<Class<?>, Object>();

    //通信客戶端,用於發送請求
    private NetClient netClient = new NettyClient();

    public <T> T getProxy(Class<T> interf) {
        T obj = (T) this.objectCache.get(interf);
        if (obj == null) {
            obj = (T) Proxy.newProxyInstance(interf.getClassLoader(), new Class<?>[] { interf },
                    new ClientStubInvocationHandler(interf, serviceInfoDiscoverer, netClient));
            this.objectCache.put(interf, obj);
        }
        return obj;
    }
}
package cn.wym.rpc.client;

import cn.wym.rpc.common.Request;
import cn.wym.rpc.common.Response;
import cn.wym.rpc.discovery.ServiceInfo;
import cn.wym.rpc.discovery.ServiceInfoDiscoverer;
import cn.wym.rpc.protocol.JSONRpcPRotocol;
import cn.wym.rpc.protocol.RpcProtocol;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Random;

public class ClientStubInvocationHandler implements InvocationHandler {

    private Class<?> interf;

    private ServiceInfoDiscoverer serviceInfoDiscoverer;

    private NetClient netClient;

    private Random random = new Random();

    public <T> ClientStubInvocationHandler(Class<T> interf, ServiceInfoDiscoverer serviceInfoDiscoverer, NetClient netClient) {
        this.interf = interf;
        this.serviceInfoDiscoverer = serviceInfoDiscoverer;
        this.netClient = netClient;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("toString")) {
            return proxy.getClass().toString();
        }

        if (method.getName().equals("hashCode")) {
            return 0;
        }
        //根據名稱去註冊中心找到對應的服務
        String serviceName = method.getName();
        List<ServiceInfo> serviceInfos = serviceInfoDiscoverer.getServiceInfo(serviceName);
        //緣分負載均衡
        ServiceInfo serviceInfo = serviceInfos.get(random.nextInt(serviceInfos.size()));

        //TODO: 將請求編組爲消息發送到服務端,並讀取服務端返回的結果
    }
}

2.從註冊中心發現服務

這裏使用zookeeper作爲註冊中心,從zookeeper上讀取服務端的信息,包括 服務名稱、協議、服務端地址;在上面的工廠類中,ServiceInfoDiscoverer就是服務發現類,相關代碼如下:

@Getter@Setter
public class ServiceInfo {

    private String name;

    private String protocol;

    private String address;
}
package cn.wym.rpc.discovery;

import java.util.List;

public interface ServiceInfoDiscoverer {
	List<ServiceInfo> getServiceInfo(String name);
}

package cn.wym.rpc.discovery;

import com.alibaba.fastjson.JSON;
import org.I0Itec.zkclient.ZkClient;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;

public class ZookeeperServiceInfoDiscoverer implements ServiceInfoDiscoverer{

	ZkClient client;

	private String centerRootPath = "/ym-rpc";

	public ZookeeperServiceInfoDiscoverer() {
		//示例代碼,配置信息直接寫代碼裏了,實際應該寫在配置文件裏
		String addr = "10.18.51.105:2181";
		client = new ZkClient(addr);
		client.setZkSerializer(new DefaultZkSerializer());
	}

	public List<ServiceInfo> getServiceInfo(String name) {
		String servicePath = centerRootPath + "/" + name + "/service";
		List<String> children = client.getChildren(servicePath);
		List<ServiceInfo> resources = new ArrayList<ServiceInfo>();
		for (String ch : children) {
			try {
				String deCh = URLDecoder.decode(ch, "UTF-8");
				ServiceInfo r = JSON.parseObject(deCh, ServiceInfo.class);
				resources.add(r);
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			}
		}
		return resources;
	}

}
package cn.wym.rpc.discovery;

import org.I0Itec.zkclient.exception.ZkMarshallingError;
import org.I0Itec.zkclient.serialize.ZkSerializer;

public class DefaultZkSerializer implements ZkSerializer {

    public byte[] serialize(Object o) throws ZkMarshallingError {
        return String.valueOf(o).getBytes();
    }

    public Object deserialize(byte[] bytes) throws ZkMarshallingError {
        return new String(bytes);
    }
}

3.將請求編組爲消息發送到服務端

涉及到服務器之間通信,毫無疑問,直接上netty;協議的話,簡單地來個json;

package cn.wym.rpc.protocol;

import cn.wym.rpc.common.Request;
import cn.wym.rpc.common.Response;

public interface RpcProtocol {

	//編碼請求
	byte[] marshallingRequest(Request req) throws Exception;

	//解碼請求
	Request unmarshallingRequest(byte[] data) throws Exception;

	//編碼響應
	byte[] marshallingResponse(Response rsp) throws Exception;

	//解碼響應
	Response unmarshallingResponse(byte[] data) throws Exception;
}
package cn.wym.rpc.protocol;

import cn.wym.rpc.common.Request;
import cn.wym.rpc.common.Response;
import com.alibaba.fastjson.JSONObject;

public class JSONRpcPRotocol implements RpcProtocol {

    public byte[] marshallingRequest(Request req) throws Exception {
        return JSONObject.toJSONBytes(req);
    }

    public Request unmarshallingRequest(byte[] data) throws Exception {
       return JSONObject.parseObject(data, Request.class);
    }

    public byte[] marshallingResponse(Response rsp) throws Exception {
        return JSONObject.toJSONBytes(rsp);
    }

    public Response unmarshallingResponse(byte[] data) throws Exception {
        return JSONObject.parseObject(data, Response.class);
    }
}

發送請求的客戶端的代碼如下:

package cn.wym.rpc.client;

import cn.wym.rpc.discovery.ServiceInfo;

public interface NetClient {

    byte[] sendRequest(byte[] data, ServiceInfo sinfo) throws Throwable;
}

package cn.wym.rpc.client;

import cn.wym.rpc.discovery.ServiceInfo;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyClient implements NetClient {

    public byte[] sendRequest(byte[] data, ServiceInfo sinfo) throws Throwable {
        String[] addInfoArray = sinfo.getAddress().split(":");

        final NettySendHandler sendHandler = new NettySendHandler(data);
        byte[] respData = null;
        // 配置客戶端
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();

            b.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(sendHandler);
                        }
                    });
            // 啓動客戶端連接
            b.connect(addInfoArray[0], Integer.valueOf(addInfoArray[1])).sync();
            respData = (byte[]) sendHandler.rspData();
        } finally {
            // 釋放線程組資源
            group.shutdownGracefully();
        }
        return respData;
    }
}

package cn.wym.rpc.client;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;

@Slf4j
public class NettySendHandler extends ChannelInboundHandlerAdapter {

    private CountDownLatch cdl = null;
    private Object readMsg = null;
    private byte[] data;

    public NettySendHandler(byte[] data) {
        cdl = new CountDownLatch(1);
        this.data = data;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("連接服務端成功:" + ctx);
        ByteBuf reqBuf = Unpooled.buffer(data.length);
        reqBuf.writeBytes(data);
        log.info("客戶端發送消息:" + reqBuf);
        ctx.writeAndFlush(reqBuf);
    }

    public Object rspData() {

        try {
            cdl.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return readMsg;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("客戶端讀取到的數據: " + msg);
        ByteBuf msgBuf = (ByteBuf) msg;
        byte[] resp = new byte[msgBuf.readableBytes()];
        msgBuf.readBytes(resp);
        readMsg = resp;
        cdl.countDown();
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        log.error("發生異常:" + cause.getMessage());
        ctx.close();
    }
}

服務端代碼開發

服務端需要完成的功能有:

  1. 向註冊中心註冊服務
  2. 監聽端口,接收來自客戶端的請求
  3. 調用用戶的實現類處理請求,結果回傳給客戶端

1. 向註冊中心註冊服務

將服務的信息(地址、端口、接口名稱、支持的協議)寫入到zookeeper

package cn.wym.rpc.server.registry;

public interface ServiceRegister {

	void register(ServiceObject so, String protocol, int port) throws Exception;

	ServiceObject getServiceObject(String name) throws Exception;
}

package cn.wym.rpc.server.registry;

import cn.wym.rpc.discovery.DefaultZkSerializer;
import cn.wym.rpc.discovery.ServiceInfo;
import cn.wym.rpc.util.PropertiesUtils;
import com.alibaba.fastjson.JSON;
import org.I0Itec.zkclient.ZkClient;

import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

/**
 * Zookeeper方式獲取遠程服務信息類。
 * 
 * ZookeeperServiceInfoDiscoverer
 */
public class ZookeeperExportServiceRegister implements ServiceRegister {

	private ZkClient client;

	private String centerRootPath = "/ym-rpc";

	private Map<String, ServiceObject> serviceMap = new HashMap<String, ServiceObject>();


	public ServiceObject getServiceObject(String name) {
		return this.serviceMap.get(name);
	}

	public ZookeeperExportServiceRegister() {
		String addr = PropertiesUtils.getProperties("zk.address");
		client = new ZkClient(addr);
		client.setZkSerializer(new DefaultZkSerializer());
	}


	public void register(ServiceObject so, String protocolName, int port) throws Exception {
		if (so == null) {
			throw new IllegalArgumentException("參數不能爲空");
		}

		this.serviceMap.put(so.getName(), so);

		ServiceInfo soInf = new ServiceInfo();

		String host = InetAddress.getLocalHost().getHostAddress();
		String address = host + ":" + port;
		soInf.setAddress(address);
		soInf.setName(so.getInterf().getName());
		soInf.setProtocol(protocolName);
		this.exportService(soInf);

	}

	private void exportService(ServiceInfo serviceResource) {
		String serviceName = serviceResource.getName();
		String uri = JSON.toJSONString(serviceResource);
		try {
			uri = URLEncoder.encode(uri, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		String servicePath = centerRootPath + "/" + serviceName + "/service";
		if (!client.exists(servicePath)) {
			client.createPersistent(servicePath, true);
		}
		String uriPath = servicePath + "/" + uri;
		if (client.exists(uriPath)) {
			client.delete(uriPath);
		}
		client.createEphemeral(uriPath);
	}
}

package cn.wym.rpc.server.registry;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Getter@Setter@AllArgsConstructor
public class ServiceObject {

	private String name;

	private Class<?> interf;

	private Object obj;
}

2.監聽端口,接收來自客戶端的請求

通過netty監聽固定端口來接收客戶端的請求,並使用ChannelHandler來處理請求

package cn.wym.rpc.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

@Getter@Setter@Slf4j
public class NettyRpcServer {

	protected int port;

	protected String protocol;

	protected RequestHandler handler;

	private Channel channel;

	public NettyRpcServer(int port, String protocol, RequestHandler handler) {
		this.port = port;
		this.protocol = protocol;
		this.handler = handler;
	}
	
	public void start() {
		// 配置服務器
		EventLoopGroup bossGroup = new NioEventLoopGroup(1);
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 100)
					.handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {
						@Override
						public void initChannel(SocketChannel ch) throws Exception {
							ChannelPipeline p = ch.pipeline();
							p.addLast(new ChannelRequestHandler(handler));
						}
					});

			// 啓動服務
			ChannelFuture f = b.bind(port).sync();
			log.info("完成服務端端口綁定與啓動");
			channel = f.channel();
			// 等待服務通道關閉
			f.channel().closeFuture().sync();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// 釋放線程組資源
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}

	public void stop() {
		this.channel.close();
	}

}

package cn.wym.rpc.server;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;

@Slf4j
public class ChannelRequestHandler  extends ChannelInboundHandlerAdapter {

    private RequestHandler handler;

    public ChannelRequestHandler(RequestHandler handler) {
        this.handler = handler;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("激活");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("服務端收到消息:" + msg);
        ByteBuf msgBuf = (ByteBuf) msg;
        byte[] req = new byte[msgBuf.readableBytes()];
        msgBuf.readBytes(req);
        byte[] res = handler.handleRequest(req);
        log.info("發送響應:" + msg);
        ByteBuf respBuf = Unpooled.buffer(res.length);
        respBuf.writeBytes(res);
        ctx.write(respBuf);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error(ExceptionUtils.getStackTrace(cause));
        ctx.close();
    }
}

3.調用用戶的實現類處理請求,結果回傳給客戶端

package cn.wym.rpc.server;

import cn.wym.rpc.common.Request;
import cn.wym.rpc.common.Response;
import cn.wym.rpc.common.Status;
import cn.wym.rpc.protocol.RpcProtocol;
import cn.wym.rpc.server.registry.ServiceObject;
import cn.wym.rpc.server.registry.ServiceRegister;
import lombok.Getter;
import lombok.Setter;

import java.lang.reflect.Method;

@Getter@Setter
public class RequestHandler {
    private RpcProtocol protocol;

    private ServiceRegister serviceRegister;

    public RequestHandler(RpcProtocol protocol, ServiceRegister serviceRegister) {
        super();
        this.protocol = protocol;
        this.serviceRegister = serviceRegister;
    }

    public byte[] handleRequest(byte[] data) throws Exception {
        // 1、解組消息
        Request req = this.protocol.unmarshallingRequest(data);

        // 2、查找服務對象
        ServiceObject so = this.serviceRegister.getServiceObject(req.getServiceName());

        Response rsp = null;

        if (so == null) {
            rsp = new Response(Status.NOT_FOUND);
        } else {
            // 3、反射調用對應的過程方法
            try {
                Method m = so.getInterf().getMethod(req.getMethod(), req.getParameterTypes());
                Object returnValue = m.invoke(so.getObj(), req.getParameters());
                rsp = new Response(Status.SUCCESS);
                rsp.setReturnValue(returnValue);
            } catch (Exception e) {
                rsp = new Response(Status.ERROR);
                rsp.setException(e);
            }
        }

        // 4、編組響應消息
        return this.protocol.marshallingResponse(rsp);
    }
}

五. 驗證

寫一個Provider,Consumer,還有一個DemoService的接口,分別啓動兩個provider實例,監聽端口8080,8079,然後運行consumer,查看服務調用情況在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章