基于BIO实现的RPC

基于BIO实现的RPC

Java BIO + JDK原生序列化 + JDK动态代理实现

代码实现

image-20210525211259596

image-20210528205440282

1、rpc-api

存放服务接口

1.1 服务接口

public interface BlogService {
	Blog getBlogById(Integer id);
}
public interface UserService {
	// 客户端通过这个接口调用服务端的实现类
	User getUserByUserId(Integer id);
	// 给这个服务增加一个功能
	Integer insertUserId(User user);
}

1.2 实体类

需要传递对应的对象,分别是Blog对象和User对象,都需要实现序列化接口,用的是JDK自带的序列化接口,原因它需要在调用过程中是从客户端传递给服务端。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Blog implements Serializable {
	private Integer id;
	private Integer userId;
	private String title;
}
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
	// 客户端和服务端共有的
	private Integer id;
	private String userName;
	private Boolean sex;
}

2、rpc-common

存放通用消息格式(Request、Response),枚举类、异常类

2.1 通用消息

  • Request对象

    作用:消费者向提供者发送的请求对象

    @Data
    @Builder
    public class RpcRequest implements Serializable {
    	/**
    	 * 待调用接口名称
    	 */
    	private String interfaceName;
    
    	/**
    	 * 待调用方法名称
    	 */
    	private String methodName;
    
    	/**
    	 * 调用方法的参数
    	 */
    	private Object[] parameters;
    
    	/**
    	 * 调用方法的参数类型
    	 */
    	private Class<?>[] paramTypes;
    }
    
  • Response对象

    作用:服务提供者执行完或出错后向消费者返回的结果对象

    @Data
    public class RpcResponse<T> implements Serializable {
    
    	/**
    	 * 响应状态码
    	 */
    	private Integer statusCode;
    
    	/**
    	 * 响应状态补充信息
    	 */
    	private String message;
    
    	/**
    	 * 响应数据
    	 */
    	private T data;
    
    	public static <T> RpcResponse<T> success(T data) {
    		RpcResponse<T> response = new RpcResponse<>();
    		response.setStatusCode(ResponseCode.SUCCESS.getCode());
    		response.setData(data);
    		return response;
    	}
    
    	public static <T> RpcResponse<T> fail(ResponseCode code) {
    		RpcResponse<T> response = new RpcResponse<>();
    		response.setStatusCode(code.getCode());
    		response.setMessage(code.getMessage());
    		return response;
    	}
    }
    

2.2 枚举类

  • ResponseCode

    作用:方法调用的响应状态码

    @AllArgsConstructor
    @Getter
    public enum  ResponseCode {
    
    	SUCCESS(200, "调用方法成功"),
    	FAIL(500,"调用方法失败"),
    	METHOD_NOT_FOUND(500,"未找到指定方法"),
    	CLASS_NOT_FOUND(500,"未找到指定类");
    
    	private final int code;
    	private final String message;
    
    }
    
  • RpcError

    作用:RPC调用过程中的错误枚举类

    @AllArgsConstructor
    @Getter
    public enum  RpcError {
    
    	SERVICE_INVOCATION_FAILURE("服务调用出现失败"),
    	SERVICE_NOT_FOUND("找不到对应的服务"),
    	SERVICE_NOT_IMPLEMENT_ANY_INTERFACE("注册的服务未实现接口");
    
    	private final String message;
    }
    

2.3 异常类

public class RpcException extends RuntimeException {

	public RpcException(RpcError error, String detail) {
		super(error.getMessage() + ": " + detail);
	}

	public RpcException(String message, Throwable cause) {
		super(message, cause);
	}

	public RpcException(RpcError error) {
		super(error.getMessage());
	}
}

3、rpc-core

RPC框架的核心实现类

3.1 服务注册类

作用:将服务暴露,即将接口的实现类注册到注册表中(先用Map存储)。

方法:注册服务、获取服务

  • 注册服务

    根据获取到的服务类,用Map作为映射关系,将服务接口和服务实现类进行存储。

    测试服务端一旦启动,就会将接口以及对应的实现类注册起来。

  • 获取服务

    服务端一旦获取到客户端的请求Request,解析出来对应的接口名,就会到注册表Map中获取对应的实现类,进而调用实现类的方法,获取执行结果

3.1.1 服务注册表通用接口
public interface ServiceRegistry {

	/**
	 * 将一个服务注册进注册表
	 * @param service 待注册的服务实体
	 * @param <T> 服务实体类
	 */
	<T> void register(T service);

	/**
	 * 根据服务名称获取服务实体
	 * @param serviceName 服务名称
	 * @return 服务实体
	 */
	Object getService(String serviceName);
}

3.1.2 默认服务注册表
public class DefaultServiceRegistry implements ServiceRegistry {

	private static final Logger logger = LoggerFactory.getLogger(DefaultServiceRegistry.class);

	private final Map<String, Object> serviceMap = new ConcurrentHashMap<>();
	private final Set<String> registeredService = ConcurrentHashMap.newKeySet();

	@Override
	public synchronized <T> void register(T service) {
		String serviceName = service.getClass().getCanonicalName();
		if(registeredService.contains(serviceName)) return;
		registeredService.add(serviceName);
        // 利用Class对象获取到服务接口
		Class<?>[] interfaces = service.getClass().getInterfaces();
		if(interfaces.length == 0) {
			throw new RpcException(RpcError.SERVICE_NOT_IMPLEMENT_ANY_INTERFACE);
		}
		for(Class<?> i : interfaces) {
			serviceMap.put(i.getCanonicalName(), service);
		}
		logger.info("向接口: {} 注册服务: {}", interfaces, serviceName);
	}

	@Override
	public synchronized Object getService(String serviceName) {
		Object service = serviceMap.get(serviceName);
		if(service == null) {
			throw new RpcException(RpcError.SERVICE_NOT_FOUND);
		}
		return service;
	}
}

3.2 服务端(提供者)

  • RpcServer

    作用:作为远程方法调用的提供者

    public class RpcServer {
    
    	private static final Logger logger = LoggerFactory.getLogger(RpcServer.class);
    
    	private static final int CORE_POOL_SIZE = 5;
    	private static final int MAXIMUM_POOL_SIZE = 50;
    	private static final int KEEP_ALIVE_TIME = 60;
    	private static final int BLOCKING_QUEUE_CAPACITY = 100;
    	private final ExecutorService threadPool;
    	private RequestHandler requestHandler = new RequestHandler();
    	private final ServiceRegistry serviceRegistry;
    
    	public RpcServer(ServiceRegistry serviceRegistry) {
    		this.serviceRegistry = serviceRegistry;
    		BlockingQueue<Runnable> workingQueue = new ArrayBlockingQueue<>(BLOCKING_QUEUE_CAPACITY);
    		ThreadFactory threadFactory = Executors.defaultThreadFactory();
    		threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, workingQueue, threadFactory);
    	}
    
    	public void start(int port) {
    		try (ServerSocket serverSocket = new ServerSocket(port)) {
    			logger.info("服务器启动……");
    			Socket socket;
    			// BIO的方式监听Socket
    			while((socket = serverSocket.accept()) != null) {
    				logger.info("消费者连接: {}:{}", socket.getInetAddress(), socket.getPort());
    				// 线程池创建线程处理消费者的请求
    				threadPool.execute(new RequestHandlerThread(socket, requestHandler, serviceRegistry));
    			}
    			threadPool.shutdown();
    		} catch (IOException e) {
    			logger.error("服务器启动时有错误发生:", e);
    		}
    	}
    
    }
    
  • RequestHandlerThread(处理线程)

    作用:

    1. 作为处理RpcRequest的工作线程,客户端每请求一次请求,服务端用就用一个线程进行处理,为了更好的管理线程,提高资源的利用率和降低资源消耗,所以自定义了线程池
    2. 从服务端代码分离出来,简化了服务端代码,单一职责原则
    3. 负责解析得到的Request,执行服务方法,返回给客户端
      • 从Request得到interfaceName
      • 根据interfaceName在ServiceRegistry中获取服务端的实现类
      • 将Request请求和获取到的实现类交给RequestHandler处理器处理,进一步解耦代码
      • 获取处理器返回的结果,封装成Response对象,写入Socket
    @AllArgsConstructor
    public class RequestHandlerThread implements Runnable {
    
    	private static final Logger logger = LoggerFactory.getLogger(RequestHandlerThread.class);
    
    	private Socket socket;
    	private RequestHandler requestHandler;
    	private ServiceRegistry serviceRegistry;
    
    	@Override
    	public void run() {
    		try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
    			 ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
    			RpcRequest rpcRequest = (RpcRequest) objectInputStream.readObject();
    			// 接口名称
    			String interfaceName = rpcRequest.getInterfaceName();
    			// 接口实现类
    			Object service = serviceRegistry.getService(interfaceName);
    			Object result = requestHandler.handle(rpcRequest, service);
    			objectOutputStream.writeObject(RpcResponse.success(result));
    			objectOutputStream.flush();
    		} catch (IOException | ClassNotFoundException e) {
    			logger.error("调用或发送时有错误发生:", e);
    		}
    	}
    
    }
    
  • RequestHandler(处理逻辑)

    作用:进行过程调用的处理器,进行真正的方法反射执行。

    RequestHandlerThread 只是一个线程,从ServiceRegistry 获取到提供服务的对象后,就会把RpcRequest 和服务对象直接交给RequestHandler 去处理,反射过程被放到了RequestHandler 里
    
    public class RequestHandler {
    
    	private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class);
    
    	public Object handle(RpcRequest rpcRequest, Object service) {
    		Object result = null;
    		try {
    			result = invokeTargetMethod(rpcRequest, service);
    			logger.info("服务:{} 成功调用方法:{}", rpcRequest.getInterfaceName(), rpcRequest.getMethodName());
    		} catch (IllegalAccessException | InvocationTargetException e) {
    			logger.error("调用或发送时有错误发生:", e);
    		} return result;
    	}
    
    	private Object invokeTargetMethod(RpcRequest rpcRequest, Object service) throws IllegalAccessException, InvocationTargetException {
    		Method method;
    		try {
    			method = service.getClass().getMethod(rpcRequest.getMethodName(), rpcRequest.getParamTypes());
    		} catch (NoSuchMethodException e) {
    			return RpcResponse.fail(ResponseCode.METHOD_NOT_FOUND);
    		}
    		return method.invoke(service, rpcRequest.getParameters());
    	}
    
    }
    

3.3 客户端(消费者)

  • RpcClientProxy

    作用:RPC客户端代理类,动态代理封装Request对象

    消费者调用服务接口方法时,实际上调用的接口代理类的invoke方法,把需要调用的接口名称、方法、参数通过代理类封装成RpcRequest对象后传递给RpcClient发送出去。

    public class RpcClientProxy implements InvocationHandler {
    
    	private static final Logger logger = LoggerFactory.getLogger(RpcClientProxy.class);
    	private String host;
    	private int port;
    
    	public RpcClientProxy(String host, int port) {
    		this.host = host;
    		this.port = port;
    	}
    
    	@SuppressWarnings("unchecked")
    	public <T> T getProxy(Class<T> clazz) {
    		return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]{clazz}, this);
    	}
    
    	// jdk 动态代理, 每一次代理对象调用方法,会经过此方法增强 (反射获取request对象,socket发送至客户端)
    	@Override
    	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    		logger.info("调用方法: {}#{}", method.getDeclaringClass().getName(), method.getName());
    		RpcRequest rpcRequest = RpcRequest.builder()
    				.interfaceName(method.getDeclaringClass().getName())
    				.methodName(method.getName())
    				.parameters(args)
    				.paramTypes(method.getParameterTypes())
    				.build();
    		RpcClient rpcClient = new RpcClient();
    		return rpcClient.sendRequest(rpcRequest, host, port);
    	}
    }
    
  • RpcClient

    作用:远程方法调用的消费者

    负责将RpcReuqest发送出去,同时接收服务端处理完结果返回的RpcResponse对象。

    使用Java的序列化方式,通过Socket传输。创建一个Socket,获取ObjectOutputStream对象,然后把需要发送的对象传过去即可。接收时获取ObjectInputStream对象,readObject()方法就可以获得一个返回的对象。
    
    public class RpcClient {
    
    	private static final Logger logger = LoggerFactory.getLogger(RpcClient.class);
    
    	public Object sendRequest(RpcRequest rpcRequest, String host, int port) {
    		try (Socket socket = new Socket(host, port)) {
    			ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
    			ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
    			objectOutputStream.writeObject(rpcRequest);
    			objectOutputStream.flush();
    			RpcResponse rpcResponse = (RpcResponse) objectInputStream.readObject();
    			if(rpcResponse == null) {
    				logger.error("服务调用失败,service:{}", rpcRequest.getInterfaceName());
    				throw new RpcException(RpcError.SERVICE_INVOCATION_FAILURE, " service:" + rpcRequest.getInterfaceName());
    			}
    			if(rpcResponse.getStatusCode() == null || rpcResponse.getStatusCode() != ResponseCode.SUCCESS.getCode()) {
    				logger.error("调用服务失败, service: {}, response:{}", rpcRequest.getInterfaceName(), rpcResponse);
    				throw new RpcException(RpcError.SERVICE_INVOCATION_FAILURE, " service:" + rpcRequest.getInterfaceName());
    			}
    			return rpcResponse.getData();
    		} catch (IOException | ClassNotFoundException e) {
    			logger.error("调用时有错误发生:", e);
    			throw new RpcException("服务调用失败: ", e);
    		}
    	}
    }
    

4、test-server

4.1 服务实现类

BlogServiceImplUserServiceImpl

public class BlogServiceImpl implements BlogService {

	private static final Logger logger = LoggerFactory.getLogger(BlogServiceImpl.class);

	@Override
	public Blog getBlogById(Integer id) {
		Blog blog = Blog.builder().id(id).title("我的博客").userId(22).build();
		logger.info("客户端查询了{}博客", id);
		return blog;
	}
}
public class UserServiceImpl implements UserService {

	private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
	@Override
	public User getUserByUserId(Integer id) {
		logger.info("接收到用户id:{}", id);
		// 模拟从数据库中获取用户的行为
		Random random = new Random();
		User user = User.builder()
				.userName("张三")
				.id(id)
				.sex(random.nextBoolean())
				.build();
		// 返回查询对象
		return user;
	}

	@Override
	public Integer insertUserId(User user) {
		logger.info("插入用户:{}", user);
		return 1;
	}
}

4.2 测试服务端

注册服务、启动监听端口等待客户端连接

public class TestServer {

	public static void main(String[] args) {
		UserService userService = new UserServiceImpl();
		BlogService blogService = new BlogServiceImpl();
		ServiceRegistry serviceRegistry = new DefaultServiceRegistry();
		serviceRegistry.register(userService);
		serviceRegistry.register(blogService);
		RpcServer rpcServer = new RpcServer(serviceRegistry);
		rpcServer.start(9000);
	}

}

5、test-client

5.1测试客户端

客户端:我们需要通过动态代理、生成代理类对象,并且调用,代理类对象会自动帮我们向服务端发送请求的

public class TestClient {

	public static void main(String[] args) {
		RpcClientProxy proxy = new RpcClientProxy("127.0.0.1", 9000);
		// 不同的service需要进行不同的封装,客户端只知道service接口,需要一层动态代理根据反射封装不同的Service
		UserService userService = proxy.getProxy(UserService.class);
		// 服务方法1
		User userByUserId = userService.getUserByUserId(10);
		System.out.println(userByUserId);
		// 服务方法2
		User user = User.builder().userName("张三").id(100).sex(true).build();
		Integer integer = userService.insertUserId(user);
		System.out.println("向服务端插入数据:" + integer);
		BlogService blogService = proxy.getProxy(BlogService.class);
		Blog blog = blogService.getBlogById(1);
		System.out.println("获取博客:" + blog);
	}

}

6、测试结果

服务端:

image-20210528224905860

客户端:

image-20210528224943854

7、版本一特点

优点:

  • 通用消息格式(Request、Response)
  • 客户端的动态代理完成对Request消息格式的封装
  • 支持服务端暴露多个服务接口,服务端程序抽象化,规范化
  • 添加线程池版的服务端的实现

缺点:

  • 传统的BIO与线程池网络传输性能低

待解决:

  • 使用高性能网络框架Netty实现网络通信,以及客户端代码重构

  • 自定义消息格式,支持多种序列方式(Java原生、Json)

  • 服务器注册与发现的实现,zookeeper作为注册中心

  • 负载均衡的策略的实现

8、补充BIO知识

BIO(Blocing I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。

8.1 传统BIO

BIO通信(一请求一应答)模型图如下

image-20210528225411271

采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在 while(true) 循环中服务端会调用 accept() 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示。

如果要让 BIO 通信模型 能够同时处理多个客户端请求,就必须使用多线程(主要原因是 socket.accept()socket.read()socket.write() 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通信模型 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过 线程池机制 改善,线程池还可以让线程的创建和回收成本相对较低。使用FixedThreadPool 可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M),

8.2 伪异步IO

为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

伪异步IO模型图

image-20210528230638061

采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层任然是同步阻塞的BIO模型,因此无法从根本上解决问题。

9、项目地址

https://github.com/TheWhc/MyRpc

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值