第四篇 - 手写RPC框架

mac2022-06-30  178

Github源码下载地址:https://github.com/chenxingxing6/myrpc

一、前言

RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外的为这个交互作用编程,如果涉及的软件采用面向对象编程(java),那么远程过程调用亦可称作远程调用或远程方法调用。只要支持网络传输的协议就是RPC协议,RPC是一种框架。简单的说就是远程调用,以API的方式调用远程的服务器上的方法,像调本地方法一样!


RPC要解决的两个问题

1.解决分布式系统中,服务之间的调用问题。 2.远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。

完整RPC调用过程:

以左边的Client端为例,Application就是rpc的调用方,Client Stub就是我们上面说到的代理对象,其实内部是通过rpc方 式来进行远程调用的代理对象,至于Client Run-time Library,则是实现远程调用的工具包,比如jdk的Socket,最后通过底 层网络实现实现数据的传输。

这个过程中最重要的就是序列化和反序列化了,因为数据传输的数据包必须是二进制的,你直接丢一个Java对象过去,人家可不 认识,你必须把Java对象序列化为二进制格式,传给Server端,Server端接收到之后,再反序列化为Java对象。


二、项目结构

要实现一个RPC不算难,难的是实现一个高性能高可靠的RPC框架。

实现功能

1.支持多种协议传输socket,http,dubbo 2.支持多种注册方式file,redis,zookeepre 3.可以动态切换协议和注册方式,只需要更改配置文件 4.Netty实现 (更新2019.10.1) 5.注解方式发布服务@Rpc (更新2019.10.1)


服务端:提供API,启动的时候要注册服务 消费端:从注册中心获取服务,调用子服务 注册中心:保存服务配置 RPC协议:基于Tomcat的HttpProtocol,基于Netty的DubboProtocol,Socket 注册中心:本地文件,Redis,Zookeeper方式进行注册



三、核心代码

如果要补充协议,只需要实现IProtocolClient,IProtocolService接口

package com.mydubbo.rpc.protocol; import com.mydubbo.rpc.framework.Invocation; import com.mydubbo.rpc.framework.URL; /** * User: lanxinghua * Date: 2019/9/30 17:24 * Desc: */ public interface IProtocolClient { /** * 客户端发送请求 * @param url * @param invocation * @return */ public Object send(URL url, Invocation invocation); }
package com.mydubbo.rpc.protocol; import com.mydubbo.registry.AbstractRegistryDiscovery; import com.mydubbo.rpc.framework.URL; /** * User: lanxinghua * Date: 2019/9/30 17:24 * Desc: */ public interface IProtocolServer { /** * 启动服务 * @param url * @param charset * @param registryDiscovery */ public void start(URL url, String charset, AbstractRegistryDiscovery registryDiscovery); }

如果要实现多种注册中心,只需继承AbstractRegistryDiscovery

package com.mydubbo.registry; import com.mydubbo.rpc.framework.URL; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; /** * User: lanxinghua * Date: 2019/9/30 16:09 * Desc: 服务注册,发现接口 */ public abstract class AbstractRegistryDiscovery { protected static Map<String/*接口名*/, Map<URL, Class>> REGISTER = new HashMap<String, Map<URL, Class>>(); /** * 服务注册 * @param url * @param interfaceName * @param implClass */ public void register(URL url, String interfaceName, Class implClass){ Map<URL, Class> map = new HashMap<URL, Class>(); map.put(url, implClass); REGISTER.put(interfaceName, map); save(); } /** * 服务发现 * @param url * @param interfaceName * @return */ public Class discovery(final URL url, String interfaceName){ REGISTER = get(); return Optional.ofNullable(REGISTER.get(interfaceName)).map(r -> r.get(url)).orElseThrow(() -> new RuntimeException("service not found!")); } /** * 负载均衡,获取可用服务地址 * @param interfaceName * @return */ public URL randomServer(String interfaceName){ REGISTER = get(); Set<URL> urls = Optional.ofNullable(REGISTER.get(interfaceName)).map(r -> r.keySet()).orElseThrow(() -> new RuntimeException("service not found!")); if (urls == null || urls.isEmpty()){ throw new RuntimeException("service not found!"); } // 这里就返回第一个 return urls.iterator().next(); } /** * 保存到注册中心 */ public abstract void save(); /** * 从注册中心获取 */ public abstract Map<String, Map<URL, Class>> get(); }

四、内置tomca

<Server port="8005" shutdown="SHUTDOWN"> <Service name="Catalina"> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/> <Engine name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Context path="" doBase="WORKDIR" reloadable="true"/> </Host> </Engine> </Service> </Server>
package com.mydubbo.rpc.protocol.http; import com.mydubbo.registry.AbstractRegistryDiscovery; import com.mydubbo.rpc.framework.URL; import com.mydubbo.rpc.protocol.IProtocolServer; import org.apache.catalina.*; import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardContext; import org.apache.catalina.core.StandardEngine; import org.apache.catalina.core.StandardHost; import org.apache.catalina.startup.Tomcat; /** * User: lanxinghua * Date: 2019/9/30 11:59 * Desc: */ public class HttpServer implements IProtocolServer { private static final String TOMCAT = "Tomcat"; @Override public void start(URL url, String charset, AbstractRegistryDiscovery registryDiscovery){ // 实例一个Tomcat Tomcat tomcat = new Tomcat(); // 构建Server Server server = tomcat.getServer(); // 获取Service Service service = server.findService(TOMCAT); // 构建Connector Connector connector = new Connector(); connector.setPort(url.getPort()); connector.setURIEncoding(charset); // 构建引擎 Engine engine = new StandardEngine(); engine.setDefaultHost(url.getHostName()); // 构建Host Host host = new StandardHost(); host.setName(url.getHostName()); // 构建Context String contextPath = ""; Context context = new StandardContext(); context.setPath(contextPath); context.addLifecycleListener(new Tomcat.FixContextListener()); // 按照server.xml进行配置 host.addChild(context); engine.addChild(host); service.setContainer(engine); service.addConnector(connector); // tomcat是一个servlet,设置路径与映射 String servletName = "dispatcher"; tomcat.addServlet(contextPath, servletName, new DispatcherServlet(url, registryDiscovery)); context.addServletMappingDecoded("/client/*", servletName); // 启动服务,接受请求 try { System.out.println("Tomcat..服务启动成功....."); tomcat.start(); tomcat.getServer().await(); }catch (Exception e){ e.printStackTrace(); } } }

五、注册中心

1.本地文件 2.zk 3.redis



六、测试

## 协议配置信息 ## 支持socket,http,dubbo config.protocol.name=socket config.protocol.host=localhost config.protocol.port=8080 config.protocol.charset=UTF-8 ## 服务注册 支持localfile,redis,zookeeper service.registry.type=redis #service.registry.type=localfile #service.registry.type=zookeeper
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath:spring.properties"/> <!-- 协议配置信息 --> <bean id="protocolConfig" class="com.mydubbo.config.ProtocolConfig"> <constructor-arg name="name" value="${config.protocol.name}"/> <constructor-arg name="host" value="${config.protocol.host}"/> <constructor-arg name="port" value="${config.protocol.port}"/> <constructor-arg name="charset" value="${config.protocol.charset}"/> </bean> <!-- 服务注册方式 --> <bean id="registryDiscoveryFactory" class="com.mydubbo.registry.RegistryDiscoveryFactory"> <constructor-arg name="registryType" value="${service.registry.type}"/> </bean> <bean id="rpcClient" class="com.mydubbo.rpc.RpcClient"> <constructor-arg name="config" ref="protocolConfig"/> <constructor-arg name="registryDiscoveryFactory" ref="registryDiscoveryFactory"/> </bean> <bean id="rpcServer" class="com.mydubbo.rpc.RpcServer"> <constructor-arg name="config" ref="protocolConfig"/> <constructor-arg name="registryDiscoveryFactory" ref="registryDiscoveryFactory"/> </bean> </beans>

package com.demo.consumer; import com.demo.provider.api.IHelloService; import com.mydubbo.rpc.RpcClient; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * User: lanxinghua * Date: 2019/9/30 11:56 * Desc: 服务启动 */ public class ConsumerStart { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml"); RpcClient rpcClient = (RpcClient) context.getBean("rpcClient"); IHelloService helloService = rpcClient.getProxy(IHelloService.class); System.out.println(helloService.sayHello("lanxinghua")); System.out.println(helloService.getUser()); System.out.println(helloService.saveUser(helloService.getUser())); } }

package com.demo.provider; import com.demo.provider.api.IHelloService; import com.demo.provider.impl.HelloService; import com.mydubbo.rpc.RpcServer; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * User: lanxinghua * Date: 2019/9/30 11:55 * Desc: 服务提供 */ public class ProviderStart { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml"); RpcServer rpcServer = (RpcServer) context.getBean("rpcServer"); // 服务注册 rpcServer.register(IHelloService.class.getName(), HelloService.class); // 启动服务 rpcServer.start(); } }

注解方式发布服务

package com.demo.provider.impl; import com.demo.provider.api.ILogService; import com.mydubbo.config.Rpc; /** * @Author: cxx * @Date: 2019/10/1 20:03 * 通过注解方式发布dubbo服务 */ @Rpc public class LogService implements ILogService { @Override public void log(String msg) { System.out.println("【日志】" + msg); } }

总结

希望路过的童鞋们,自己手写RPC后,可以更加清楚知道服务远程间是怎么调用的,后期还要许多东西需要完善,比如:负载均衡,熔断机制,Mock数据等等…


最新回复(0)