序:RPC就是使用socket告诉服务端我要调你的哪个类的哪个方法而后得到处理的结果。服务注册和路由就是借助第三方存储介质存储服务信息让服务消费者调用。然咱们本身动手从0开始写一个rpc功能以及实现服务注册,动态上下线,服务路由,负载均衡。html
RPC即远程过程调用,它的实现方式有不少,好比webservice等。框架调多了,烦了,没激情了,咱们就该问本身,这些框架的做用究竟是什么,来找回当初的激情。
通常来讲,咱们写的系统就是一个单机系统,一个web服务器一个数据库服务,可是当这单台服务器的处理能力受硬件成本的限制,是不能无限的提高处理性能的。这个时候咱们使用RPC将原来的本地调用转变为调用远端的服务器上的方法,给系统的处理能力和吞吐量带来了提高。
RPC的实现包括客户端和服务端,即服务的调用方和服务的提供方。服务调用方发送rpc请求到服务提供方,服务提供方根据调用方提供的参数执行请求方法,将执行的结果返回给调用方,一次rpc调用完成。java
原文和做者一块儿讨论:http://www.cnblogs.com/intsmaze/p/6058765.html
mysql
先让咱们利用socket简单的实现RPC,来看看他是什么鬼样子。nginx
服务端代码以下 web
服务端的提供服务的方法redis
package cn.intsmaze.tcp.two.service; public class SayHelloServiceImpl { public String sayHello(String helloArg) { if(helloArg.equals("intsmaze")) { return "intsmaze"; } else { return "bye bye"; } } }
服务端启动接收外部方法请求的端口类,它接收到来自客户端的请求数据后,利用反射知识,建立指定类的对象,并调用对应方法,而后把执行的结果返回给客户端便可。算法
package cn.intsmaze.tcp.two.service; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Method; import java.net.ServerSocket; import java.net.Socket; public class Provider { public static void main(String[] args) throws Exception { ServerSocket server=new ServerSocket(1234); while(true) { Socket socket=server.accept(); ObjectInputStream input=new ObjectInputStream(socket.getInputStream()); String classname=input.readUTF();//得到服务端要调用的类名 String methodName=input.readUTF();//得到服务端要调用的方法名称 Class<?>[] parameterTypes=(Class<?>[]) input.readObject();//得到服务端要调用方法的参数类型 Object[] arguments=(Object[]) input.readObject();//得到服务端要调用方法的每个参数的值 Class serviceclass=Class.forName(classname);//建立类 Object object = serviceclass.newInstance();//建立对象 Method method=serviceclass.getMethod(methodName, parameterTypes);//得到该类的对应的方法 Object result=method.invoke(object, arguments);//该对象调用指定方法 ObjectOutputStream output=new ObjectOutputStream(socket.getOutputStream()); output.writeObject(result); socket.close(); } } }
服务调用者代码sql
调用服务的方法,主要就是客户端启动一个socket,而后向提供服务的服务端发送数据,其中的数据就是告诉服务端去调用哪个类的哪个方法,已经调用该方法的参数是多少,而后结束服务端返回的数据便可。数据库
package cn.intsmaze.tcp.two.client; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Socket; public class consumer { @SuppressWarnings({ "unused", "rawtypes" }) public static void main(String[] arg) throws Exception { //咱们要想调用远程提供的服务,必须告诉远程咱们要调用你的哪个类,这里咱们能够在本地建立一个interface来获取类的名称,可是这样咱们必须 //保证该interface和远程的interface的所在包名一致。这种方式很差。因此咱们仍是经过硬编码的方式吧。
//虽然webservice就是这样的,我我的以为不是多好。 // String interfacename=SayHelloService.class.getName(); String classname="cn.intsmaze.tcp.two.service.SayHelloServiceImpl"; String method="sayHello"; Class[] argumentsType={String.class}; Object[] arguments={"intsmaze"}; Socket socket=new Socket("127.0.0.1",1234); ObjectOutputStream output=new ObjectOutputStream(socket.getOutputStream()); output.writeUTF(classname); output.writeUTF(method); output.writeObject(argumentsType); output.writeObject(arguments); ObjectInputStream input=new ObjectInputStream(socket.getInputStream()); Object result=input.readObject(); System.out.println(result); socket.close(); } }
固然实际中出于性能考虑,每每采用非阻塞式I/O,避免无限的等待,带来系统性能的消耗。json
上面的只是一个简单的过程,当系统之间的调用变的复杂以后,该方式有以下不足:服务调用者代码以硬编码的方式指明所调用服务的信息(类名,方法名),当服务提供方改动所提供的服务的代码后,服务调用者必须修改代码进行调整,否则会致使服务调用者没法成功进行远程方法调用致使系统异常,而且当服务提供者宕机下线了,服务调用者并不知道服务端是否存活,仍然会进行访问,致使异常。
一个系统中,服务提供者每每不是一个,而是多个,那么服务消费者如何从众多的服务者找到对应的服务进行RPC就是一个问题了,由于这个时候咱们不能在在服务调用者代码中硬编码指出调用哪个服务的地址等信息,由于咱们能够想象,没有一个统一的地方管理全部服务,那么咱们在错综复杂的系统之间没法理清有哪些服务,已经服务的调用关系,这简直就是灾难。
这个时候就要进行服务的注册,经过一个第三方的存储介质,当服务的提供者上线时,经过代码将所提供的服务的相关信息写入到存储介质中,写入的主要信息以key-value方式:服务的名称:(类名,方法名,参数类型,参数,IP地址,端口)。服务的调用者向远程调用服务时,会先到第三方存储介质中根据所要调用的服务名获得(类名,方法名,参数类型,参数,IP地址,端口)等参数,而后再向服务端发出调用请求。经过这种方式,代码就变得灵活多变,不会再由于一个局部的变得引起全局架构的变更。由于通常的改动是不会变得服务的名称的。这种方式其实就是soa架构,服务消费者经过服务名称,从众多服务中找到要调用的服务的相关信息,称为服务的路由。
下面经过一个静态MAP对象来模拟第三方存储的介质。
package cn.intsmaze.tcp.three; import net.sf.json.JSONObject; public class ClassWays { String classname;//类名 String method;//方法 Class[] argumentsType;//参数类型 String ip;//服务的ip地址 int port;//服务的端口 get,set...... }
第三方存储介质,这里固定了服务提供者的相关信息,理想的模拟是,当服务启动后,自动向该类的map集合添加信息。可是由于服务端和客户端启动时,是两个不一样的jvm进程,客户端时没法访问到服务端写到静态map集合的数据的。
package cn.intsmaze.tcp.three; import java.util.HashMap; import java.util.Map; import net.sf.json.JSONObject; public class ServiceRoute { public static Map<String,String> NAME=new HashMap<String, String>(); public ServiceRoute() { ClassWays classWays=new ClassWays(); Class[] argumentsType={String.class}; classWays.setArgumentsType(argumentsType); classWays.setClassname("cn.intsmaze.tcp.three.service.SayHelloServiceImpl"); classWays.setMethod("sayHello"); classWays.setIp("127.0.0.1"); classWays.setPort(1234); JSONObject js=JSONObject.fromObject(classWays); NAME.put("SayHello", js.toString()); } }
接下来看服务端代码的美丽面孔吧。
package cn.intsmaze.tcp.three.service;public class Provider { //服务启动的时候,组装相关信息,而后写入第三方存储机制,供服务的调用者去获取 public void reallyUse() { ClassWays classWays = new ClassWays(); Class[] argumentsType = { String.class }; classWays.setArgumentsType(argumentsType); classWays.setClassname("cn.intsmaze.tcp.three.service.SayHelloServiceImpl"); classWays.setMethod("sayHello"); classWays.setIp("127.0.0.1"); classWays.setPort(1234); JSONObject js=JSONObject.fromObject(classWays); //模拟第三方存储介质,实际中应该是redis,mysql,zookeeper等。 ServiceRoute.NAME.put("SayHello", js.toString()); } public static void main(String[] args) throws Exception { ServerSocket server = new ServerSocket(1234); //实际中,这个地方应该调用以下方法,可是由于简单的模拟服务的注册,将注册的信息硬编码在ServiceRoute类中,这个类的构造方法里面会自动注册服务的相关信息。 //server.reallyUse(); while (true) { Socket socket = server.accept(); ObjectInputStream input = new ObjectInputStream(socket.getInputStream()); String classname = input.readUTF(); String methodName = input.readUTF(); Class<?>[] parameterTypes = (Class<?>[]) input.readObject(); Object[] arguments = (Object[]) input.readObject(); Class serviceclass = Class.forName(classname); Object object = serviceclass.newInstance(); Method method = serviceclass.getMethod(methodName, parameterTypes); Object result = method.invoke(object, arguments); ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream()); output.writeObject(result); socket.close(); } } }
服务的调用者代码:
package cn.intsmaze.tcp.three.client;public class Consumer { public Object reallyUse(String provideName,Object[] arguments) throws Exception { //模拟从第三方存储介质拿去数据 ServiceRoute serviceRoute=new ServiceRoute(); String js=serviceRoute.NAME.get(provideName); JSONObject obj = new JSONObject().fromObject(js); ClassWays classWays = (ClassWays)JSONObject.toBean(obj,ClassWays.class); String classname=classWays.getClassname(); String method=classWays.getMethod(); Class[] argumentsType=classWays.getArgumentsType(); Socket socket=new Socket(classWays.getIp(),classWays.getPort()); ObjectOutputStream output=new ObjectOutputStream(socket.getOutputStream()); output.writeUTF(classname); output.writeUTF(method); output.writeObject(argumentsType); output.writeObject(arguments); ObjectInputStream input=new ObjectInputStream(socket.getInputStream()); Object result=input.readObject(); socket.close(); return result; } @SuppressWarnings({ "unused", "rawtypes" }) public static void main(String[] arg) throws Exception { Consumer consumer=new Consumer(); Object[] arguments={"intsmaze"}; Object result=consumer.reallyUse("SayHello",arguments); System.out.println(result); } }
回到开始的问题如今咱们保证了服务调用者对服务的调用的相关参数以动态的方式进行控制,经过封装,服务调用者只须要指定每一次调用时的参数的值便可。可是当服务提供者宕机下线了,服务调用者并不知道服务端是否存活,仍然会进行访问,致使异常。这个时候咱们该如何考虑解决了?
剩下的我就不写代码示例了,代码只是思想的表现形式,就像开发语言一直变化,可是思想是不变的。
服务下线咱们应该把该服务从第三方存储删除,在服务提供方写代码进行删除控制,也就是服务下线前访问第三方删除本身提供的服务。这样固然行不通的,由于服务宕机时,才不会说,我要宕机了,服务提供者你快去第三方存储介质删掉该服务信息。因此这个时候咱们就要在第三方存储介质上作手脚,好比服务提供方并非直接把服务信息写入第三方存储介质,而是与一个第三方系统进行交互,第三方系统把接收到来自服务提供者的服务信息写入第三方存储介质中,而后在服务提供者和第三方系统间创建一个心跳检测,当第三方系统检测到服务提供者宕机后,就会自动到第三方介质中删除对应服务信息。
这个时候咱们就能够选择zookeeper做为第三方存储介质,服务启动会到zookeeper上面建立一个临时目录,该目录存储该服务的相关信息,当服务端宕机了,zookeeper会自动删除该文件夹,这个时候就实现了服务的动态上下线了。
这个地方其实就是dubbo的一大特点功能:服务配置中心——动态注册和获取服务信息,来统一管理服务名称和其对于的服务器的信息。服务提供者在启动时,将其提供的服务名称,服务器地址注册到服务配置中心,服务消费者经过配置中心来得到须要调用服务的机器。当服务器宕机或下线,相应的机器须要动态地从服务配置中心移除,并通知相应的服务消费者。这个过程当中,服务消费者只在第一次调用服务时须要查询服务配置中心,而后将查询到的信息缓存到本地,后面的调用直接使用本地缓存的服务地址信息,而不须要从新发起请求到服务配置中心去获取相应的服务地址,直到服务的地址列表有变动(机器上线或者下线)。
zookeeper如何知道的?zookeeper其实就是会和客户端直接有一个心跳检测来判断的,zookeeper功能很简单的,能够本身去看对应的书籍便可。
随着业务的发展,服务调用者的规模发展到必定的阶段,对服务提供方也带来了巨大的压力,这个时候服务提供方就不在是一台机器了,而是一个服务集群了。
服务调用者面对服务提供者集群如何高效选择服务提供者集群中某一台机器?
一说到集群,咱们都会想到反向代理nginx,因此咱们就会采用nginx的配置文件中存储集群中的全部IP和端口信息。而后把第三方存储介质中存储的服务信息——key-value:服务的名称:(类名,方法名,参数类型,参数,IP地址,端口)IP地址改成集群的代理地址,而后服务消费者根据服务名称得到服务信息后组装请求把数据发送到nginx,再由nginx负责转发请求到对应的服务提供者集群中的一台。
这确实是能够知足的,可是若是吹毛求疵就会发现他所暴露的问题!
一:使用nginx进行负载均衡,一旦nginx宕机,那么依赖他的服务均将失效,这个时候服务的提供者并无宕机。
二:这是一个内部系统的调用,服务调用者集群数量远远小于外部系统的请求数量,那么咱们将全部的服务消费者到服务提供者的请求都通过nginx,带来没必要要的效率开销。
改进方案:将服务提供者集群的全部信息都存储到第三方系统(如zookeeper)中对应服务名称下,表现形式为——服务名:[{机器IP:(类名,方法名,参数类型,参数,IP地址,端口)}...]。这样服务消费者向第三方存储系统(如zookeeper)得到服务的全部信息(服务集群的地址列表),而后服务调用者就从这个列表中根据负载均衡算法选择一个进行访问。
这个时候咱们可能会思考,负载均衡算法咱们是参考nginx把IP地址的分配选择在第三方系统(如zookeeper)上进行实现仍是在服务调用者端进行实现?负载均衡算法部署在第三方系统(如zookeeper),服务消费者把服务名称发给第三方系统,第三方系统根据服务名而后根据负载均衡算法从该服务的地址信息列表中选择一个返回给服务消费者,服务消费者得到所调用服务的具体信息后,直接向服务的提供者发送请求。可是正如我所说,这只是一个内部系统,请求的数量每每没有多大的变化,并且实现起来要在服务消费者直接调用zookeeper系统前面编写一个中间件做为一个中间,难免过于麻烦。咱们彻底能够在服务的消费者处嵌入负载均衡算法,服务消费者获取服务的地址信息列表后,运算负载均衡算法从所得的地址信息列表中选择一个地址信息发送请求的数据。更进一步,服务消费者第一次执行负载均衡算法后就把选择的地址信息存储到本地缓存,之后再次访问就直接从本地拿去,再也不到第三方系统中获取了。
基于第三方系统实现服务的负载均衡的方案已经实现,那么咱们来解决下一个问题,服务的上线和下线如何告知服务的消费者,避免服务消费者访问异常?
前面咱们说了,服务提供者利用zookeeper系统的特性,能够实现服务的注册和删除,那么一样,咱们也可让服务的消费者监听zookeeper上对应的服务目录,当服务目录变更后,服务消费者则从新到zookeeper上获取新的服务地址信息,而后运算负载均衡算法选择一个新的服务进行请求。
若是有没有讲明白的能够留言,我进行更正。基本上一个RPC就是这样,剩下的一些基于RPC的框架无非就是实现了多些协议,以及一些多种语言环境的考虑和效率的提高。
以为不错点个推荐吧,看在我花了一天时间把本身的知识整理分析,谢谢喽。固然这仍是没有写好,等我下周有时间再添加图片进行完善,关于这个架构的设计欢迎你们讨论,共同成长。