手写rpc
RPC 是“远程过程调用(Remote Procedure Call)”的缩写形式,比较通俗的解释是:像本地方法调用一样调用远程的服务。虽然 RPC 的定义非常简单,但是相对完整的、通用的 RPC 框架涉及很多方面的内容,例如注册发现、服务治理、负载均衡、集群容错、RPC 协议等,如下图所示:
主要实现RPC 框架的基石——远程调用,简易版 RPC 框架一次远程调用的核心流程是这样的:
Client 首先会调用本地的代理,也就是图中的 Proxy。
Client 端 Proxy 会按照协议(Protocol),将调用中传入的数据序列化成字节流。
之后 Client 会通过网络,将字节数据发送到 Server 端。
Server 端接收到字节数据之后,会按照协议进行反序列化,得到相应的请求信息。
Server 端 Proxy 会根据序列化后的请求信息,调用相应的业务逻辑。
Server 端业务逻辑的返回值,也会按照上述逻辑返回给 Client 端。
这个远程调用的过程,就是我们简易版本 RPC 框架的核心实现,只有理解了这个流程,才能进行后续的开发。
代码结构:
client: 客户端:动态代理完成对request消息格式的封装,
server服务端: 包含netty, 线程池,序列化jsonSerializer/objectSerializer, 消息格式 encoder/decoder
core: 自定义协议,包含:消息头部和协议
codec:提供自定义协议对于端编码器和解码器,serializer: 序列化反序列化类
netty:基于 Netty 提供了底层网络通信的功能,其中会使用到 codec 包中定义编码器和解码器,以及 serialization 包中的序列化器和反序列化器。分为服务端通信和客户端通信。
registry:注册中心,基于 ZooKeeper 和 Curator 实现了简易版本的注册中心功能。
loadbalance:负载均衡,有 随机,一致性哈希,轮询等。
spi:自定义spi实现,类似dubbo spi。
自定义协议
当前已经有很多成熟的协议了,例如 HTTP、HTTPS 等,那为什么我们还要自定义 RPC 协议呢?
从功能角度考虑,HTTP 协议在 1.X 时代,只支持半双工传输模式,虽然支持长连接,但是不支持服务端主动推送数据。从效率角度来看,在一次简单的远程调用中,只需要传递方法名和加个简单的参数,此时,HTTP 请求中大部分数据都被 HTTP Header 占据,真正的有效负载非常少,效率就比较低。
当然,HTTP 协议也有自己的优势,例如,天然穿透防火墙,大量的框架和开源软件支持 HTTP 接口,而且配合 REST 规范使用也是很便捷的,所以有很多 RPC 框架直接使用 HTTP 协议,尤其是在 HTTP 2.0 之后,如 gRPC、Spring Cloud 等。
这里我们自定义一个RPC 协议,如下所示:
±----------------------------------------------------------------------------------+
| 消息类型 2byte | 序列化类型 2byte | 数据长度4byte | 消息体 |
±----------------------------------------------------------------------------------+
在自定义RPC 的消息头中,包含了整个 RPC 消息的一些控制信息,例如,消息类型、序列化类型、消息体的长度。当然,你也可以自己扩充 Demo RPC 协议,实现更加复杂的功能。
编解码
Netty 每个 Channel 绑定一个 ChannelPipeline,并依赖 ChannelPipeline 中添加的 ChannelHandler 处理接收到(或要发送)的数据,其中就包括字节到消息(以及消息到字节)的转换。Netty 中提供了 ByteToMessageDecoder、 MessageToByteEncoder、MessageToMessageEncoder、MessageToMessageDecoder 等抽象类来实现 Message 与 ByteBuf 之间的转换以及 Message 之间的转换,如下图所示:
Netty 提供的 Decoder 和 Encoder 实现
在 Netty 的源码中,我们可以看到对很多已有协议的序列化和反序列化都是基于上述抽象类实现的,例如,HttpServerCodec 中通过依赖 HttpServerRequestDecoder 和 HttpServerResponseEncoder 来实现 HTTP 请求的解码和 HTTP 响应的编码。如下图所示,HttpServerRequestDecoder 继承自 ByteToMessageDecoder,实现了 ByteBuf 到 HTTP 请求之间的转换;HttpServerResponseEncoder 继承自 MessageToMessageEncoder,实现 HTTP 响应到其他消息的转换(其中包括转换成 ByteBuf 的能力)。
在代码中,自定义请求暂时没有 HTTP 协议那么复杂,只要简单继承 ByteToMessageDecoder 和 MessageToMessageEncoder 即可。
按照自定义的消息格式解码数据RpcDecode,它实现了Message 到 ByteBuf 的转换:
/**
* 按照自定义的消息格式解码数据
*/
@Slf4j
@AllArgsConstructor
public class RpcDecode extends ByteToMessageDecoder {
/*
+----------------------------------------------------------------------------+
|消息类型 2byte | 序列化类型 2byte | 数据长度4byte |
+-----------------------------------------------------------------------------+
*/
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
log.info("=============begin RpcEncode============");
// 写入消息类型
if (msg instanceof RpcRequest) {
out.writeShort(MessageType.REQUEST.getCode());
} else if (msg instanceof RpcResponse) {
out.writeShort(MessageType.RESPONSE.getCode());
}
// 写入序列化方式
out.writeShort(serializer.getType());
// 得到序列化数组
byte[] serialize = serializer.serialize(msg);
// 写入长度
out.writeInt(serialize.length);
// 写入序列化字节数组
out.writeBytes(serialize);
}
}
接下来看 RpcEncode,它实现了 Demo RPC Message 到 ByteBuf 的转换:
/**
* 依次按照自定义的消息格式写入,传入的数据为request或者response
* 需要持有一个serialize器,负责将传入的对象序列化成字节数组
*/
@Slf4j
@AllArgsConstructor
public class RpcEncode extends MessageToByteEncoder {
private Serializer serializer;
/*
+----------------------------------------------------------------------------+
|消息类型 2byte | 序列化类型 2byte | 数据长度4byte |
+-----------------------------------------------------------------------------+
*/
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
log.info("=============begin RpcEncode============");
// 写入消息类型
if (msg instanceof RpcRequest) {
out.writeShort(MessageType.REQUEST.getCode());
} else if (msg instanceof RpcResponse) {
out.writeShort(MessageType.RESPONSE.getCode());
}
// 写入序列化方式
out.writeShort(serializer.getType());
// 得到序列化数组
byte[] serialize = serializer.serialize(msg);
// 写入长度
out.writeInt(serialize.length);
// 写入序列化字节数组
out.writeBytes(serialize);
}
}
网络通信相关
netty中启动一个Bootstrap,大致有8步,如下图:
服务端和客户端通过netty进行网络传输,netty通过channelInitializer装配handler流水线,如图,分别实现了NettyClientInitializer和NettyServerInitializer。
在 自定义RPC 框架的 Server 端接收到请求时,首先会通过上面介绍的 RpcDecode 反序列化得到请求消息,之后我们会通过一个自定义的 ChannelHandler(NettyRpcServerHandler)将请求提交给业务线程池进行处理。
在 Demo RPC 框架的 Client 端接收到响应消息的时候,也是先通过RpcDecode 反序列化得到响应消息,之后通过一个自定义的 ChannelHandler(NettyRpcClientHandler)将响应返回给上层业务。
NettyRpcServerHandler 和 NettyRpcClientHandler 都继承自 SimpleChannelInboundHandler,如下图所示:
下面我们就来看一下这两个自定义的 ChannelHandler 实现:
/**
* 因为是服务器端,我们知道接受到请求格式是RPCRequest
* Object类型也行,强制转型就行
*/
@AllArgsConstructor
public class NettyRpcServerHandler extends SimpleChannelInboundHandler<RpcRequest> {
private ServiceProvider serviceProvider;
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequest msg) throws Exception {
RpcResponse response = getResponse(msg);
ctx.writeAndFlush(response);
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
RpcResponse getResponse(RpcRequest request) {
// 得到服务名
String interfaceName = request.getInterfaceName();
// 得到服务端相应服务实现类
Object service = serviceProvider.getService(interfaceName);
// 反射调用方法
Method method = null;
try {
method = service.getClass().getMethod(request.getMethodName(), request.getParamsTypes());
Object invoke = method.invoke(service, request.getParams());
return RpcResponse.success(invoke);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
System.out.println("方法执行错误");
return RpcResponse.fail();
}
}
}
/**
* SimpleChannelInboundHandler继承自ChannelInboundHandlerAdapter
*
* ChannelInboundHandlerAdapter是ChannelHandler的适配器之一,其对应的还有ChannelOutboundHandlerAdapter,其中ChannelInboundHandler负责处理处理进站数据和所有状态更改事件,而ChannelOutboundHandler负责处理出站数据,允许拦截各种操作。
*/
public class NettyRpcClientHandler extends SimpleChannelInboundHandler<RpcResponse> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcResponse msg) throws Exception {
// 接收到response, 给channel设计别名,让sendRequest里读取response
AttributeKey<RpcResponse> key = AttributeKey.valueOf("RpcResponse");
ctx.channel().attr(key).set(msg);
ctx.channel().close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
public class NettyClientInitializer extends ChannelInitializer<SocketChannel> {
/**
* v4: 自定义编解码器
*/
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 使用自定义的编解码器
pipeline.addLast(new RpcDecode());
// 编码需要传入序列化器,这里是json,还支持ObjectSerializer,也可以自己实现其他的
pipeline.addLast(new RpcEncode(new JsonSerializer()));
pipeline.addLast(new NettyRpcClientHandler());
}
}
/**
* 初始化,主要负责序列化的编码解码, 需要解决netty的粘包问题
*/
@AllArgsConstructor
public class NettyServerInitializer extends ChannelInitializer<SocketChannel> {
private ServiceProvider serviceProvider;
/**
* v4: 自定义的编解码器
* 消息格式: |消息类型 2byte | 序列化类型 2byte | 数据长度4byte|
* 加上消息长度:防止粘包, 再根据长度读取data
* encoder/decoder 在netty的pipline中
*/
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 使用自定义的编解码器
pipeline.addLast(new RpcDecode());
// 编码需要传入序列化器,这里是json,还支持ObjectSerializer,也可以自己实现其他的
pipeline.addLast(new RpcEncode(new JsonSerializer()));
pipeline.addLast(new NettyRpcServerHandler(serviceProvider));
}
}
@AllArgsConstructor
public class NettyRpcServer implements RpcServer {
private ServiceProvider serviceProvider;
@Override
public void start(int port) {
// netty 服务线程组boss负责建立连接, work负责具体的请求
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workGroup = new NioEventLoopGroup();
System.out.println("Netty服务端启动了...");
try {
// 启动netty服务器 (ServerBootstrap引导类)
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 初始化
serverBootstrap.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class) //指定传输类型Channel
.childHandler(new NettyServerInitializer(serviceProvider)); //添加业务Handler
// 同步阻塞 绑定服务器及端口
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
// 监听 (使用sync()去监听服务端的Channel,直到channel被关闭)
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放资源
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
@Override
public void stop() {
}
}
//****** netty客户端。************
public class NettyRpcClient implements RpcClient {
private static final Bootstrap bootstrap;
private static final EventLoopGroup eventLoopGroup;
private String host;
private int port;
private ServiceRegister serviceRegister;
public NettyRpcClient() {
this.serviceRegister = new ZkServiceRegister();
}
public NettyRpcClient(String host, int port) {
this.host = host;
this.port = port;
}
// netty客户端初始化,重复使用
static {
//NioEventLoopGroup创建连接及处理出/入站数据
eventLoopGroup = new NioEventLoopGroup();
//Bootstrap引导类
bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class) //指定传输类型为NioSocketChannel类型
.handler(new NettyClientInitializer()); //添加业务Handler
}
/**
* 这里需要操作一下,因为netty的传输都是异步的,你发送request,会立刻返回一个值, 而不是想要的相应的response
*/
@Override
public RpcResponse sendRequest(RpcRequest request) {
InetSocketAddress address = serviceRegister.serviceDiscovery(request.getInterfaceName());
host = address.getHostName();
port = address.getPort();
try {
//------连接远程指定的host、port节点
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
Channel channel = channelFuture.channel();
//发送数据
channel.writeAndFlush(request);
//------监听服务器的Channel, 直到channel关闭
channel.closeFuture().sync();
// 阻塞的获得结果,通过给channel设计别名,获取特定名字下的channel中的内容(这个在hanlder中设置)
// AttributeKey是,线程隔离的,不会由线程安全问题。 实际上不应通过阻塞,可通过回调函数
AttributeKey<RpcResponse> key = AttributeKey.valueOf("RpcResponse");
RpcResponse response = channel.attr(key).get();
System.out.println("NettyRpcClient.response=" + response);
return response;
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}
注册中心
注册中心(如zookeeper)的地址是固定的(为了高可用一般是集群,我们看做黑盒即可), 服务端上线时,在注册中心注册自己的服务与对应的地址,而客户端调用服务时,去注册中心根据服务名找到对应的服务端地址。
zookeeper我们可以近似看作一个树形目录文件系统,是一个分布式协调应用,其它注册中心有EureKa, Nacos等。
注:需本地安装zookeeper
1、下载解压Zookeeper (https://zookeeper.apache.org/releases.html)
2、zookeeper启动
2.1 zoo.cfg 修改dataDir为一个存在目录
2.2 windows启动命令: bin/zkServer.cmd
3、java项目中引入Curator客户端
代码:
/**
* 服务注册实现类
* @author dengkun
* @date 2023/11/5
*/
@Slf4j
public class ZkServiceRegister implements ServiceRegister {
// curator 提供的zookeeper客户端
private CuratorFramework client;
// zookeeper根路径节点
private static final String ROOT_PATH = "MyRPC";
//初始化负载均衡,这里用的是随机,一般通过构造函数传人
//private LoadBalance loadBalance = new RandomLoadBalance();
private LoadBalance loadBalance = new ConsistentHashLoadBalance();
/**
* 这里负责zookeeper客户端的初始化,并与zookeeper服务端建立连接
*/
public ZkServiceRegister(){
// 指数时间重试
RetryPolicy policy = new ExponentialBackoffRetry(1000, 3);
// zookeeper的地址固定,不管是服务提供者还是,消费者都要与之建立连接
// sessionTimeoutMs 与 zoo.cfg中的tickTime 有关系,
// zk还会根据minSessionTimeout与maxSessionTimeout两个参数重新调整最后的超时值。默认分别为tickTime 的2倍和20倍
// 使用心跳监听状态
this.client = CuratorFrameworkFactory.builder().connectString("127.0.0.1:2181")
.sessionTimeoutMs(40000).retryPolicy(policy).namespace(ROOT_PATH).build();
this.client.start();
System.out.println("---------------------zookeeper 连接成功------------------------");
}
@Override
public void register(String serviceName, InetSocketAddress serverAddress){
try {
System.out.println("register......, serviceName =" + serviceName);
// serviceName创建成永久节点,服务提供者下线时,不删服务名,只删地址
String parentNode = "/" + serviceName;
if(client.checkExists().forPath(parentNode) == null){
client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath("/" + serviceName);
}
// 路径地址,一个/代表一个节点
String path = parentNode +"/"+ getServiceAddress(serverAddress);
// 临时节点,服务器下线就删除节点
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
} catch (Exception e) {
System.out.println("此服务已存在");
}
}
/**
* 根据服务名返回地址
*/
@Override
public InetSocketAddress serviceDiscovery(String serviceName) {
try {
// List<String> strings = client.getChildren().forPath("/" + serviceName);
// log.warn("serviceDiscovery services:" + strings.toString());
// // 这里默认用的第一个,后面加负载均衡
// //String string = strings.get(0);
//
// // 负载均衡
// String string = loadBalance.balance(strings);
// return parseAddress(string);
List<InetSocketAddress> serviceAddressList = lookup(serviceName);
return loadBalance.selectServiceAddress(serviceName, serviceAddressList);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 注册中心的核心目的是什么?拉取合适的服务列表
* @param serviceName 服务名称
* @return 服务列表
*/
@Override
public List<InetSocketAddress> lookup(String serviceName) throws Exception {
// 1、找到服务对于的节点
String serviceNode = "/" + serviceName;
// 2、从zk中获取他的子节点,192.168.12.123:2151
List<String> children = client.getChildren().usingWatcher(new UpAndDownWatcher()).forPath(serviceNode);
// 设置监听 一次性的
// 获取所有可用的服务列表
List<InetSocketAddress> inetSocketAddresses = children.stream().map(ipString -> {
String[] ipAndPort = ipString.split(":");
String ip = ipAndPort[0];
int port = Integer.parseInt(ipAndPort[1]);
return new InetSocketAddress(ip, port);
}).collect(Collectors.toList());
if(inetSocketAddresses.isEmpty()){
throw new Exception("未发现任何可用的服务主机.");
}
return inetSocketAddresses;
}
// 地址 -> XXX.XXX.XXX.XXX:port 字符串
private String getServiceAddress(InetSocketAddress serverAddress) {
return serverAddress.getHostName() +
":" +
serverAddress.getPort();
}
// 字符串解析为地址
private InetSocketAddress parseAddress(String address) {
String[] result = address.split(":");
return new InetSocketAddress(result[0], Integer.parseInt(result[1]));
}
}
服务端
public class TestServer {
/**
* netty, 线程池,序列化jsonSerializer/objectSerializer, 消息格式 encoder/decoder
*
*/
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
BlogService blogService = new BlogServiceImpl();
// 这里重用了服务暴露类,顺便在注册中心注册,实际上应分开,每个类做各自独立的事
ServiceProvider serviceProvider = new ServiceProvider("127.0.0.1", 8890);
serviceProvider.provideServiceInterface(userService);
serviceProvider.provideServiceInterface(blogService);
RpcServer rpcServer = new NettyRpcServer(serviceProvider);
rpcServer.start(8890);
}
}
客户端
public class TestClient {
/**
* NettyRpcClient使用自定义的序列化方式 encoder/decoder
*/
public static void main(String[] args) {
// 构建一个使用java Socket/ netty/....传输的客户端
//RpcClient rpcClient = new NettyRpcClient("127.0.0.1", 8899); //缺点!!!
RpcClient rpcClient = new NettyRpcClient(); //zk获取
// 把这个客户端传入代理客户端
RpcClientProxy rpcClientProxy = new RpcClientProxy(rpcClient);
// 代理客户端根据不同的服务,获得一个代理类, 并且这个代理类的方法以或者增强(封装数据,发送请求)
UserService userService = rpcClientProxy.getProxy(UserService.class); //多个请求公用一个连接
// 调用方法
User userByUserId = userService.getUserByUserId(11);
System.out.println("从服务端得到的user为:" + userByUserId);
User user = User.builder().userName("张三").id(101).sex(true).build();
Integer integer = userService.insertUserId(user);
System.out.println("向服务端插入数据:"+integer);
BlogService blogService = rpcClientProxy.getProxy(BlogService.class);
Blog blogById = blogService.getBlogById(10001);
System.out.println("从服务端得到的blog为:" + blogById);
}
}