双向rpc

来源:互联网 发布:c语言中的continue 编辑:程序博客网 时间:2024/05/15 00:59

提起RPC(异步过程调用),大家马上想到的是java平台自带的RMI框架。那么什么是rpc呢,简单来说,就是服务端提供一系列接口,客户端可以透明的调用这些接口,就好像客户端直接调用本地的方法一样。

RMI虽然拥有贵族血统,奈何java自带的序列化方式速度较慢,占用内存也较多。在开源社区涌现了一批又一批高效的RPC框架,典型代表有WebService,Hession,Dubbo等等。

RPC本身采用C/S模式,注定了RPC天生是单向通信的。也就是说,客户端可以调用服务端的rpc接口,但反过来,服务端无法主动调用客户端的接口。在实际环境上,服务端需要主动通知客户端进行某些操作。因此实现双向的rpc框架是有必要的。

实现双向的rpc要求双方同时属于服务端和客户端。每一个节点都需要本身注册服务接口,同时作为客户端注册链接到另一个节点的链路。虽然有点麻烦,但不失为一个经得起考验的解决方案。

本文采用的RPC框架是baidu基于JProtobuff(通过注解实现Protobuff)的rpc实现,git地址为Jprotobuf-rpc-socket。

下面分别介绍该框架所用到的主要类(代码略长^_^)

1.定义rpc服务接口(RpcTransferService.java)。该接口只需要一个服务方法即可。

package com.kingston.rpc.transfer;import com.baidu.jprotobuf.pbrpc.ProtobufRPC;import com.kingston.rpc.event.RpcEvent;public interface RpcTransferService {@ProtobufRPC(serviceName = "RpcTransferService", onceTalkTimeout = 200)void transferRpc(RpcEvent evnet);}
2.定义rpc服务接口的具体实现(RpcTransferServiceImpl.java)。

package com.kingston.rpc.transfer;import com.baidu.jprotobuf.pbrpc.ProtobufRPCService;import com.kingston.rpc.MessageDispatcher;import com.kingston.rpc.event.RpcEvent;public class RpcTransferServiceImpl {@ProtobufRPCService(serviceName = "RpcTransferService")publicvoid transferRpc(RpcEvent event){MessageDispatcher.INSTANCE.onEventReceived(event);}} 
3.定义消息载体(RpcEvent.java)。

关于该类,需要注意的是在数据传输时,将所有的属性值序列化后存储在data字段。并且为了能够将属性的类型保持在可控的范围内,定义了SUPPORT_TYPES字段保存所有支持的数据类型。

package com.kingston.rpc.event;import java.util.HashMap;import java.util.HashSet;import java.util.Map;import java.util.Objects;import java.util.Set;import java.util.concurrent.atomic.AtomicLong;import com.baidu.bjf.remoting.protobuf.annotation.Protobuf;import com.kingston.rpc.ServerConfig;import com.kingston.rpc.utils.JsonUtils;public class RpcEvent {@Protobufprivate String id;@Protobufprivate int sourceServerId;@Protobufprivate int command;@Protobufprivate String data;@Protobufprivate long roleId;//private RpcEvent callback;private static AtomicLong ID_GENERATOR = new AtomicLong();private Map<String,Object> attrbutes = new HashMap<>();/** attrbutes支持的value类型  */private static final  Set<Class<?>> SUPPORT_TYPES = new HashSet<Class<?>>();static {SUPPORT_TYPES.add(Boolean.class);SUPPORT_TYPES.add(Byte.class);SUPPORT_TYPES.add(Character.class);SUPPORT_TYPES.add(Short.class);SUPPORT_TYPES.add(Integer.class);SUPPORT_TYPES.add(Float.class);SUPPORT_TYPES.add(Double.class);SUPPORT_TYPES.add(String.class);}public RpcEvent(){}public static RpcEvent build(int command,long roleId) {RpcEvent event = new RpcEvent();event.command  = command;event.roleId   = roleId;event.sourceServerId = ServerConfig.serverId;event.id = getNextSeq(event.sourceServerId);return event;}public static RpcEvent createResponse(String requestId) {RpcEvent event = new RpcEvent();event.id = requestId;return event;}public String getId() {return this.id;}public long getRoleId() {return this.roleId;}public int getSourceServerId(){return this.sourceServerId;}public static String getNextSeq(int serverId){return serverId + "_" + ID_GENERATOR.getAndIncrement();}public int getCommand(){return this.command;}/** * 将attribute序列为json格式 */public void serialize() {this.data = JsonUtils.object2String(this.attrbutes);}/** * 反序列化为attributes */public void unserialize() {this.attrbutes = JsonUtils.string2Object(this.data, Map.class);}public void setAttribute(String key, Object value) {Objects.requireNonNull(key);Objects.requireNonNull(value);if (!SUPPORT_TYPES.contains(value.getClass())) {throw new IllegalArgumentException("属性不支持"+value.getClass());}this.attrbutes.put(key, value);}public Object getAttibute(String key) {return this.attrbutes.get(key);}}

4.定义消息分发器(MessageDispatcher.java)

package com.kingston.rpc;import java.util.HashMap;import java.util.Map;import java.util.Set;import java.util.concurrent.BlockingQueue;import java.util.concurrent.LinkedBlockingQueue;import com.kingston.rpc.event.RpcEvent;import com.kingston.rpc.handler.LoginEventHandler;import com.kingston.rpc.handler.MessageHandler;import com.kingston.rpc.handler.RpcHandler;import com.kingston.rpc.utils.ClassFilter;import com.kingston.rpc.utils.ClassScanner;/** * 消息分发器 */public enum MessageDispatcher {INSTANCE;private int CORE = Runtime.getRuntime().availableProcessors()/2;/** 发送消息的线程池 */private IOHandler[] ioHandlers = new IOHandler[CORE]; /** 处理消息的线程池 */private EventHandler[] eventHandlers = new EventHandler[CORE]; private final Map<Integer,MessageHandler> RPC_EVENT_HANDLERS = new HashMap<>();private MessageDispatcher() {initHandlerPools();initHandlers();}/** * 初始化所有的handler处理器 */private void initHandlers() {ClassFilter filter = new ClassFilter() {@Overridepublic boolean accept(Class<?> clazz) {return MessageHandler.class.isAssignableFrom(clazz)&& !clazz.isInterface();}};Set<Class<?>> handlers = ClassScanner.getClasses("com.kingston",filter);for (Class c:handlers) {registerHandler(new LoginEventHandler());}}/** * 初始化读写任务线程池 */private void initHandlerPools() {for(int i=0;i<CORE;i++) {IOHandler handler = new IOHandler();ioHandlers[i] = handler;new Thread(handler).start();}for(int i=0;i<CORE;i++) {EventHandler handler = new EventHandler();eventHandlers[i] = handler;new Thread(handler).start();}}public void onEventReceived(RpcEvent event){int index = (int) (event.getRoleId()%CORE);eventHandlers[index].addEvent(event);}public void sendRequest(Session session, RpcEvent request) {if(session == null || request == null) {return ;}int key = (int) request.getRoleId();int index = key%CORE;ioHandlers[index].addTask(session,request);}/** * 注册handler * @param handler */public void registerHandler(MessageHandler handler){RpcHandler annotation = handler.getClass().getAnnotation(RpcHandler.class);int key = annotation.cmd();if(RPC_EVENT_HANDLERS.containsKey(key)){throw new IllegalStateException("handler key repeated");}RPC_EVENT_HANDLERS.put(key, handler);}private class IOHandler implements Runnable {/** 发送消息的生产者队列 */private BlockingQueue<Task> queue = new LinkedBlockingQueue<>();void addTask(Session client, RpcEvent event) {Task newTask = new Task();newTask.client = client;newTask.event = event;this.queue.add(newTask);}@Overridepublic void run() {while(true) {try{Task task = queue.take();if(task != null){handleTask(task);}}catch(Exception e) {e.printStackTrace();}}}private void handleTask(Task task) {RpcEvent event = task.event;Session client = task.client;MessageHandler handler = RPC_EVENT_HANDLERS.get(event.getCommand());client.write(event);}}private class Task {private Session client;private RpcEvent event;}private class EventHandler implements Runnable {/** 接受消息的消费者队列 */private BlockingQueue<RpcEvent> queue = new LinkedBlockingQueue<>();public void addEvent(RpcEvent event) {if(event != null) {queue.add(event);}}@Overridepublic void run() {while(true) {try{RpcEvent event = queue.take();if(event == null) {continue;}String key = event.getId();handlerReceive(event);}catch(Exception e) {e.printStackTrace();}}}private void handlerReceive(RpcEvent event) {MessageHandler handler = RPC_EVENT_HANDLERS.get(event.getCommand());handler.receiveEvent(event);}}}

该类需要特别注意的是发送和处理消息所采用的线程模型。以处理消息的线程池eventHandlers为例,在接受消息的时候,通过roleid字段,将事件映射到特定的线程队列。这种做法能够保证玩家的个人请求有序,省去同步的消耗。


5.定义消息类型与处理器的映射关系

消息的类型绑定在RpcEvent对象的command字段。对于每一种类型,都有它对应的处理器。我们可以通过注解来绑定两者之间的关系。

5.1定义处理器接口(MessageHandler.java)

package com.kingston.rpc.handler;import com.kingston.rpc.event.RpcEvent;public interface MessageHandler {/** * client发送前的数据处理 * @param event 发送的event对象 * @param attachment 附加数据 */void transmitEvent(RpcEvent event, Object attachment);/** * server端收到数据的处理 * @param event */void receiveEvent(RpcEvent event);}
5.2 定义处理器注解(RpcHandler.java)

package com.kingston.rpc.handler;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Documented@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface RpcHandler {int cmd() default 0;}
5.3 定义具体的处理器子类(LoginEventHandler.java),以登录逻辑作为示例。

package com.kingston.rpc.handler;import com.kingston.rpc.command.CommandKinds;import com.kingston.rpc.event.RpcEvent;@RpcHandler(cmd=CommandKinds.LOGIN)public class LoginEventHandler implements MessageHandler{@Overridepublic void transmitEvent(RpcEvent event, Object attachment) {}@Overridepublic void receiveEvent(RpcEvent event) {System.err.println("-----receive message----------");}}

5.4  定义command类型的常量池(CommandKinds.java)

package com.kingston.rpc.command;/** * command常量池 */public class CommandKinds {private CommandKinds() {} //禁止实例化/**登录*/public static final int LOGIN = 1;}


6 为了让程序在初始化的时候智能匹配类型与对应的处理器,可以在程序启动时使用类扫描器,扫描制定的注解。

定义类扫描工具(ClassScanner.java)。该工具类是在网上搜索的,自己加了个过滤器。
消息分发器MessageDispatcher的initHandlers() 在初始化的时候就会扫描并注册所有的handlers。

package com.kingston.rpc.utils;import java.io.File;import java.io.FileFilter;import java.io.IOException;import java.net.JarURLConnection;import java.net.URL;import java.net.URLDecoder;import java.util.Enumeration;import java.util.LinkedHashSet;import java.util.Set;import java.util.jar.JarEntry;import java.util.jar.JarFile;import com.kingston.rpc.handler.MessageHandler;/** * 类扫描器 */public class ClassScanner {/** * 默认过滤器(无实现) */private final static ClassFilter defaultFilter = new ClassFilter() {@Overridepublic boolean accept(Class<?> clazz) {return true;}};/** * 扫描目录下的所有class文件 * @param pack 包路径 * @return */public static Set<Class<?>> getClasses(String pack) {return getClasses(pack,defaultFilter);}/** * 扫描目录下的所有class文件 * @param pack 包路径 * @param filter 自定义类过滤器 * @return */public static Set<Class<?>> getClasses(String pack, ClassFilter filter) {  Set<Class<?>> result = new LinkedHashSet<Class<?>>();  // 是否循环迭代  boolean recursive = true;  // 获取包的名字 并进行替换  String packageName = pack;  String packageDirName = packageName.replace('.', '/');  // 定义一个枚举的集合 并进行循环来处理这个目录下的things  Enumeration<URL> dirs;  try {  dirs = Thread.currentThread().getContextClassLoader().getResources(  packageDirName);  // 循环迭代下去  while (dirs.hasMoreElements()) {  // 获取下一个元素  URL url = dirs.nextElement();  // 得到协议的名称  String protocol = url.getProtocol();  // 如果是以文件的形式保存在服务器上  if ("file".equals(protocol)) {  // 获取包的物理路径  String filePath = URLDecoder.decode(url.getFile(), "UTF-8");  // 以文件的方式扫描整个包下的文件 并添加到集合中  findAndAddClassesInPackageByFile(packageName, filePath,  recursive, result,filter);  } else if ("jar".equals(protocol)) {  // 如果是jar包文件  Set<Class<?>> jarClasses = findClassFromJar(url,packageName,packageDirName,recursive,filter);result.addAll(jarClasses);}  }  } catch (IOException e) {  e.printStackTrace();  }  return result;  }  private static Set<Class<?>> findClassFromJar(URL url,String packageName,String packageDirName,boolean recursive, ClassFilter filter) {Set<Class<?>> result = new LinkedHashSet<Class<?>>(); try {  // 获取jar  JarFile jar = ((JarURLConnection) url.openConnection()).getJarFile();  // 从此jar包 得到一个枚举类  Enumeration<JarEntry> entries = jar.entries();  // 同样的进行循环迭代  while (entries.hasMoreElements()) {  // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件  JarEntry entry = entries.nextElement();  String name = entry.getName();  // 如果是以/开头的  if (name.charAt(0) == '/') {  // 获取后面的字符串  name = name.substring(1);  }  // 如果前半部分和定义的包名相同  if (name.startsWith(packageDirName)) {  int idx = name.lastIndexOf('/');  // 如果以"/"结尾 是一个包  if (idx != -1) {  // 获取包名 把"/"替换成"."  packageName = name.substring(0, idx)  .replace('/', '.');  }  // 如果可以迭代下去 并且是一个包  if ((idx != -1) || recursive) {  // 如果是一个.class文件 而且不是目录  if (name.endsWith(".class")  && !entry.isDirectory()) {  // 去掉后面的".class" 获取真正的类名  String className = name.substring(packageName.length() + 1,name.length() - 6);  try {  // 添加到classes  Class c = Class.forName(packageName+'.'+className);if (filter.accept(c)) {result.add(c);  }} catch (ClassNotFoundException e) {  e.printStackTrace();  }  }  }  }  }  } catch (IOException e) {  e.printStackTrace();  }  return result;}private static void findAndAddClassesInPackageByFile(String packageName,  String packagePath, final boolean recursive, Set<Class<?>> classes,ClassFilter filter) {  // 获取此包的目录 建立一个File  File dir = new File(packagePath);  // 如果不存在或者 也不是目录就直接返回  if (!dir.exists() || !dir.isDirectory()) {  // log.warn("用户定义包名 " + packageName + " 下没有任何文件");  return;  }  // 如果存在 就获取包下的所有文件 包括目录  File[] dirfiles = dir.listFiles(new FileFilter() {  // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)  public boolean accept(File file) {  return (recursive && file.isDirectory())  || (file.getName().endsWith(".class"));  }  });  // 循环所有文件  for (File file : dirfiles) {  // 如果是目录 则继续扫描  if (file.isDirectory()) {  findAndAddClassesInPackageByFile(packageName + "."  + file.getName(), file.getAbsolutePath(), recursive,  classes,filter);  } else {  // 如果是java类文件 去掉后面的.class 只留下类名  String className = file.getName().substring(0,  file.getName().length() - 6);  try {  // 添加到集合中去  Class clazz = Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className);if (filter.accept(clazz)) {classes.add(clazz);}} catch (ClassNotFoundException e) {  // log.error("添加用户自定义视图类错误 找不到此类的.class文件");  e.printStackTrace();  }  }  }  } }
扫描过滤器的接口定义(ClassFilter.java)

package com.kingston.rpc.utils;/** * 类扫描的过滤器 */public interface ClassFilter {/** * 返回true表示添加到目标集合 * @param clazz * @return */boolean accept(Class<?> clazz);}

7. 定义服务节点的链接抽象(Session.java)。该类封装了rpc服务接口的发送与链接。
package com.kingston.rpc;import com.baidu.jprotobuf.pbrpc.client.ProtobufRpcProxy;import com.baidu.jprotobuf.pbrpc.transport.RpcClient;import com.kingston.rpc.event.RpcEvent;import com.kingston.rpc.transfer.RpcTransferService;public class Session {private RpcClient client;private RpcTransferService rpcTransfer;private ProtobufRpcProxy<RpcTransferService> rpcProxy;private String ip;private int port;private long createTime;public Session(String ip,int port) {this.ip = ip;this.port = port;this.createTime = System.currentTimeMillis();this.connect();}private void connect(){RpcClient rpcClient = new RpcClient();// 创建EchoService代理ProtobufRpcProxy<RpcTransferService> pbrpcProxy = new ProtobufRpcProxy<RpcTransferService>(rpcClient, RpcTransferService.class);pbrpcProxy.setHost(ip);pbrpcProxy.setPort(port);// 动态生成代理实例RpcTransferService echoService = pbrpcProxy.proxy();pbrpcProxy.setLookupStubOnStartup(false);this.client = rpcClient;this.rpcProxy = pbrpcProxy;this.rpcTransfer = echoService;}public void write(RpcEvent message){rpcTransfer.transferRpc(message);}}

8. 定义服务节点的路由表(Router.java)

package com.kingston.rpc;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ConcurrentMap;public class Router {/** serverid与session的映射*/private static ConcurrentMap<Integer,Session> CLIENTS = new ConcurrentHashMap<>();/** 路由节点映射表*/private static ConcurrentMap<Integer,Node> NODES = new ConcurrentHashMap<>();//测试节点信息static{NODES.put(22, new Node(22,"localhost",8122));NODES.put(23, new Node(23,"localhost",8123));}public static Session getClientSessionBy(int serverId){Session client = CLIENTS.get(serverId);if(client != null) {return client;}synchronized (Router.class) {client = CLIENTS.get(serverId);if(client != null) {return client;}Node node = NODES.get(serverId);client = new Session(node.ip, node.port);return client;}}private static class Node {int serverId;String ip;int port;public Node(int serverId, String ip, int port) {super();this.serverId = serverId;this.ip = ip;this.port = port;}}}

9. 定义服务配置类(ServerConfig.java)

package com.kingston.rpc;public class ServerConfig {/** 服务器id*/public static int serverId = 22;  /** rpc服务端口*/public static int serverPort = 8122;}

10 定义json的序列化工具(JsonUtils.java)。采用jackson工具库。

package com.kingston.rpc.utils;import java.io.StringWriter;import com.fasterxml.jackson.databind.JavaType;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.type.TypeFactory;public class JsonUtils {private static final ObjectMapper  mapper = new ObjectMapper();private static TypeFactory typeFactory = TypeFactory.defaultInstance();public static String object2String(Object object) {StringWriter writer = new StringWriter();try{mapper.writeValue(writer, object);}catch(Exception e) {e.printStackTrace();return null;}return writer.toString();}public static <T> T string2Object(String content, Class<T> clazz) {JavaType type = typeFactory.constructType(clazz);try{return (T)mapper.readValue(content, type);}catch(Exception e) {e.printStackTrace();return null;}}}

11. 定义rpc服务类(CrossRpcServer.java)

package com.kingston.rpc.service;import com.baidu.jprotobuf.pbrpc.transport.RpcServer;import com.kingston.rpc.ServerConfig;import com.kingston.rpc.transfer.RpcTransferServiceImpl;public class CrossRpcServer {private RpcServer server;public void start() {init();System.err.println("rpc server start at port " + getServerPort());}private void init() {try{server = new RpcServer();server.getRpcServerOptions().setKeepAliveTime(60*5);server.registerService(new RpcTransferServiceImpl());server.start(getServerPort());}catch(Exception e) {e.printStackTrace();}}public int getServerPort() {return ServerConfig.serverPort;}}


12.服务器启动入口(ServerMain.java)

package com.kingston.rpc.service;public class ServerMain {public static void main(String[] args) {new CrossRpcServer().start();}}


13.定义客户端入口(ClientMain.java)

package com.kingston.rpc.client;import com.kingston.rpc.MessageDispatcher;import com.kingston.rpc.Session;import com.kingston.rpc.command.CommandKinds;import com.kingston.rpc.event.RpcEvent;import com.kingston.rpc.service.CrossRpcServer;public class ClientMain {public static void main(String[] args) {//客户端也需要注册rpc服务new CrossRpcServer().start();Session client = new Session("localhost", 8122);RpcEvent request = RpcEvent.build(CommandKinds.LOGIN, 1);MessageDispatcher.INSTANCE.sendRequest(client, request);}}


测试方法:

将同样的代码部署在两个工程里,一个工程启动ServerMain,一个工程启动ClientMain。

需要注意的地方:

1.客户端本身也需要注册rpc服务端口;

2.ServerConfig配置里serverid与serverPort要避免冲突


一个简单的双向rpc就打造完成了。

但还有需要优化的地方。比如客户端在发送消息之后,可能需要服务端执行逻辑后能把执行结果回调给客户端。也就是说,客户端需要实现回调功能。限于篇幅,这里就不再贴代码了。

该工程使用了maven工具,依赖包的配置以及完整最新的代码,请移步到git

-->> rpc-cluster


1 0
原创粉丝点击