朋友,你也不想不懂RPC的事情被同事发现吧?(附DEMO,快来玩!)

前言

距离上一次写正儿八经的技术文有个半年多了,抱歉抱歉ಥ_ಥ,确实有点怠惰了。工作强度吧,真的有点大,心力耗费有点严重,同时博主的相亲之路也不是很顺畅,北京的相亲市场简直就是神仙赶集(┬┬﹏┬┬),再加上工作和生活上事儿赶事儿,就慢慢躺了。不过心里确实还记着要写技术博客,也没耽搁,目前存稿和思路后面会出两篇,一篇RPC,一篇缓存相关。

本文是RPC的经典一本通文章,因为RPC是一个偏概念的东西,所以会搭配一个DEMO来讲解,同时相关的知识会尽可能的丰富一下,保持我一贯的文章水准。文章从RPC的定义和特征讲起,介绍核心流程和关键组件,接着会对一些名词做一下解释,同时回答一些问题。最后会用一个DEMO来介绍如何写一个RPC框架,当然也会拓展序列化、压缩、负载均衡和动态代理的知识,汇总网上的资料横向对比各种框架的性能和选型思路。本文的目的就是去帮助大家学习和认识平时最熟悉的基础框架-RPC组件的原理,同时拓展的知识也能帮助我们加深对其他中间件的理解。

定义和原理

什么是RPC?

RPC(Remote Procedure Call,远程过程调用 )是远程过程调用的缩写,是一种计算机通信协议 。它使得程序可以像调用本地函数一样,请求另一个进程或者计算机上的服务,而不需要了解底层网络技术。RPC可以简化分布式系统的开发,提高系统的可维护性和可扩展性。RPC协议的实现包括一个客户端和一个服务端,它们可以运行在不同的机器上。

核心目标: 简化分布式系统中跨进程/跨机器的交互,让你调用远程方法像调用本地方法一样简单。

核心特征:

  • 透明性:调用远程方法像本地方法
  • 通信封装:隐藏网络细节(序列化、传输、路由)
  • 协议自由:支持多种底层协议(TCP/HTTP/triple/dubbo/自定义)
  • 高效性:低延迟、高吞吐(二进制协议+长连接)

现代的RPC框架,部分还会提供服务治理能力 (服务注册与发现、负载均衡、容错机制等功能)、跨语言支持 (通过接口定义语言(IDL)生成多语言客户端代码,实现跨平台调用)、多样化通信模式(支持同步、异步调用,以及流式通信。例如,gRPC支持双向流式数据传输,适用于实时交互场景)

举个例子: 用交流来说,远程调用就相当于打电话沟通或者微信聊天,本地调用就是面对面说话。

核心原理

关键组件

  1. 客户端(服务消费端):调用远程方法的一端。
  2. 客户端Stub(存根) : 客户端Stub是远程服务在客户端的本地代理,负责将调用请求序列化为网络传输格式(如二进制流或JSON),并通过网络发送至服务端
  3. 网络传输 :将客户端的调用方法的信息传到服务端,等服务端执行完毕后把返回结果再传输回客户端。实现方式有最基础的Socket,基于Socket封装的Netty,还有Http以及各种自定义方式。
  4. 服务端Skeleton(骨架): 服务端Skeleton是服务端接收请求的代理,负责反序列化客户端请求,调用本地实际服务方法,并将结果序列化后返回给客户端
  5. 服务端(服务提供端): 提供远程方法的一端。

核心流程

名词拆解,答疑解惑

最基础的RPC框架-基石Socket

这是一个标准的RPC的原理网图,如果我们摘掉客户端 Stub和服务端 Skeleton,就成了用socket实现的一个普通通信代码,同时也是最简单的RPC框架。

java 复制代码
---------------------------客户端---------------------------
// 1. 创建Socket连接服务端(假设服务端运行在本机8080端口)
Socket socket = new Socket("localhost", 8080);
// 2. 获取输出流并包装为对象流(发送请求)
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
// 3. 获取输入流并包装为对象流(接收响应)
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream())
// 4. 发送方法名和参数(模拟调用add(1, 2))
oos.writeUTF("add");    // 写入方法名(UTF编码)
oos.writeInt(1);       // 写入第一个参数
oos.writeInt(2);        // 写入第二个参数
oos.flush();            // 确保数据发送

// 5. 读取服务端返回的结果
int result = ois.readInt();
System.out.println("调用结果: " + result);

---------------------------服务端---------------------------
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
String methodName = ois.readUTF();  // 读取方法名
int a = ois.readInt();              // 读取参数
int b = ois.readInt();
int result = a + b;                 // 实际计算
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeInt(result);               // 返回结果

那么这个最基础的框架有什么问题呢?

  1. 与服务端的强绑定
    • 服务端通过ois.readUTF()读取方法名,ois.readInt()读取参数,客户端必须严格按顺序写入对应类型数据
    • 若服务端期望更多参数(如String类型),客户端需追加oos.writeUTF("param")。
  2. 局限性:
    • 固定方法名: 客户端硬编码了方法名"add",实际RPC框架会通过接口动态代理实现。
    • 简单序列化: 使用Java原生对象流,跨语言支持差;实际框架会使用JSON/Protobuf等协议。
    • 无异常处理: 未处理服务端方法不存在或参数错误的情况(需自定义协议约定错误码)。
    • 资源浪费: Socket的频繁创建连接

为了解决这些问题,各种RPC框架出现了,比如Dubbo、Grpc等,他们的底层基础是Socket,因为要依赖Socket进行网络传输,所以是Socket的高度封装。在此之上通过动态代理、序列化、服务治理等功能,简化开发并提升系统能力,并且引入了一些高性能网络库(如Netty)、高效序列化协议(如Protobuf)和各种服务治理机制(如Zookeeper)

RPC和Http有什么关系?

先来对比一下两者的区别,但是这两者一般不能对比,因为RPC是一种设计理念,而Http是一种通信协议实现

场景 典型HTTP接口(如RESTful API) RPC框架(如gRPC)
设计目标 资源操作(GET/POST/PUT/DELETE) 方法调用(远程函数执行)
数据格式 文本(JSON/XML) 二进制(Protobuf/Thrift)
调用方式 显式构造URL和参数 通过接口代理直接调用方法
协议灵活性 需遵循HTTP语义 可自定义协议(如Dubbo的私有协议)
性能 较低(头部冗余,文本解析慢) 较高(二进制压缩,协议精简)

先来说一下Http和Socket的关系,有助于理解和RPC的关系

  • HTTP应用层协议 ,定义数据格式(请求头、请求体等)。HTTP基于TCP,HTTP协议依赖TCP建立连接(三次握手)和传输数据。
  • TCP传输层协议 ,提供可靠的数据传输。TCP基于Socket实现:操作系统通过Socket API暴露TCP能力,例如Java中的ServerSocket和Socket类。
  • Socket :操作系统提供的编程接口,用于操作TCP/UDP协议

Feign算不算RPC?

Feign这个框架,基本是HttpClient->RestTemplate->Feign(OpenFeign)这样的演化过程,实际上就是一个声明式、模板化的Http客户端 ,主要用于简化HTTP API的调用

FeignFeign通过动态代理和接口封装隐藏了HTTP调用细节,符合RPC"透明调用"的核心特征,从广义上可以认为是RPC的一种实现,狭义上不属于传统RPC框架:

  • 协议限制Feign强绑定HTTP协议,而传统RPC框架支持更灵活的协议。
  • 设计理念差异:Feign围绕RESTful资源操作设计,而RPC框架以方法调用为中心。
  • 性能与扩展性:Feign的性能和跨语言能力弱于专用RPC框架(如gRPC)。

为什么叫客户端Stub(存根)和服务端Skeleton(骨架)

客户端Stub和服务端Skeleton的概念最早源于1984年Andrew Birrell和Bruce Nelson的论文《Implementing Remote Procedure Calls》。

该论文提出将RPC抽象为五个组件(User、User-stub、RPCRuntime、Server-stub、Server),其中Stub和Skeleton的设计目标是屏蔽网络通信复杂性,是实现透明调用的基石。

简单来说,Stub 帮客户端假装自己在本地调用,Skeleton 帮服务端偷偷执行真实方法,深藏功与名~


如何写一个RPC框架?

偷个小懒,从Github上扒了一个RPC教学类star排名前几的项目,github.com/Snailclimb/...,接下来会根据该项目的代码进行讲解。项目启动很简单,下载一个3.5.10或者最新的zk,启动后,先启动example-server,再启动example-client即可。

大致讲一下代码结构,对应上前面讲的关键组件

  • example-client: 客户端(服务消费端)
  • example-server: 服务端Skeleton(骨架)
  • hello-service-api: API拓展包,就是咱们组件管理里面给项目定义的各种export包,对外提供的资源(接口)
  • rpc-framework-common、rpc-framework-simple: 客户端Stub(存根)、服务端Skeleton(骨架)、网络传输

代码分析

资源定义

需要注解+bean注册类

一般来说至少需要两种类,一种是服务端在类上的服务方定义@RpcService ,一种是客户端在类变量上定义的资源定义@RpcReference。这里多了一个扫描器@RpcScan,类似于@ComponentScan,有用处,但是不要也可以,可以直接在@RpcService中加上@Component之类的的bean资源注解,交给Spring,事后再捞出来已注册的bean特殊处理即可。

  • CustomScannerRegistrar这个类用于将RpcService注解定义的资源塞入Spring的Bean容器中。
  • SpringBeanPostProcessor implements BeanPostProcessor,这个类是核心注册类,用于定义当前项目的资源和变量的注入两件事情。
    1. 在postProcessBeforeInitialization中遍历已注册的bean,找到类上有注解RpcService的对象,定义服务端的资源,并调用服务治理(比如Zookeeper)进行了资源注册。
    2. 在postProcessAfterInitialization中遍历已注册的Bean,找到变量上有注解RpcReference的对象,创建客户端Stub(存根)反射注入该变量中。

使用BeanPostProcessor这个进行Bean生命周期拓展可以,但是更推荐使用LifeCycle这个Bean生命周期全部走完再进行操作的类,避免BeanPostProcessor后有什么额外的操作影响当前拓展。

服务治理

需要大致两种接口和一个本地缓存操作类

  • ServiceRegistry---服务注册接口,方法是registerService,单纯一个资源注册即可,入参是资源名rpcServiceName和节点网络地址internetAddress
  • ServiceDiscovery---服务发现接口,方法是lookupService,需要通过指定的参数获取对应的资源,才能知道调用具体哪一个节点
  • ServiceProvider ---本地缓存操作类,方法提供void publishService(RpcServiceConfig rpcServiceConfig)和Object getService(String rpcServiceName)。这个类中会定义一个本地缓存,Map<String(rpcServiceName), Object(服务端对象)> serviceMap,在调用publishService的时候会调用ServiceRegistry.registerService,并往serviceMap新增一个资源KV。getService类就是服务端在处理请求时,根据rpcServiceName返回真正对象,从而反射处理请求,这是服务端Skeleton(骨架)的一部分。

网络传输

核心模块,需要定义出入参和信息传递三个类,定义两个接口,一个负责发消息,一个负责接收消息

  • RpcRequest-包含请求方的信息(链路ID、版本号、分组等各种校验和日志信息)、入参的类型和值、接口名和方法名(用于代理对象反射调用方法)
  • RpcResponse-包含方法调用结果、结果码、信息
  • RpcMessage-用于网络传输,将RpcRequest和RpcResponse包住,增加消息类型(比如心跳检测、请求RpcRequest、返回结果RpcResponse)、编码方式、压缩方式、加密解密

收发消息这个需要定义发送端和服务端,这时候技术选型就是真正的传输了

传输方式 性能 复杂度 适用场景 代表框架
原生Socket 需要底层控制的场景 Java RMI(究极老)
Netty(主流) 高并发生产环境 Dubbo、gRPC
HTTP/1.x 兼容性优先的跨平台调用 Spring Cloud Feign
HTTP/2 现代高性能RPC gRPC(默认)
自定义协议 内部高性能服务调用,定制化处理 Dubbo协议

这里简单讲下代码里的Netty模块吧,毕竟是主流

客户端:

  • ChannelProvider -用于存储和获取Channel对象,Channel是Netty框架中的一个核心接口,代表了一个网络连接,可以用于读写操作。内部用了一个ConcurrentHashMap来复用和管理这些Channel对象。
  • NettyRpcClient -主要用于向服务端发送请求,会调用ChannelProvider创建和获取Channel。因为内部使用CompletableFuture做了异步,所以用UnprocessedRequests管理异步请求映射,UnprocessedRequests的生产在NettyRpcClient类,消费在NettyRpcClientHandler监听类。
  • NettyRpcClientHandler -处理服务端返回的信息,比如请求的响应结果和心跳信息。

服务端:

  • NettyRpcServer -完整初始化一个Netty的服务端,用于监听请求
  • NettyRpcServerHandler -监听并校验消息,调用RpcRequestHandler类反射执行指定方法,并响应结果信息
  • RpcRequestHandler-这个类要做的很简单,从RpcMessage解析出rpcServiceName,从本地缓存类ServiceProvider中获取到真正的类对象,invokeTargetMethod反射调用方法即可

流程图

拓展

额外插播一个我觉得很有意思的网站,云原生计算基金会(cncf)的技术全景图landscape.cncf.io/,里面都是一些很知名的分布式项目,涵盖容器、CI/CD、监控、服务治理、网络、分布式数据库、流处理计算、消息队列等。大家如果遇到什么问题,想要拓展思路或者单纯为了技术选型,都可以来瞅瞅,激发灵感。当然最重要的是看看目前最潮流的技术是啥样的,发展到什么程度了。

通信协议

通信协议是 RPC 通信双方约定的 数据传输规则,定义了如何组织、传输和解析数据包。其职责如下:

  • 规范通信的消息格式(如头部、正文、校验码等)。
  • 定义通信流程(如建立连接、心跳检测、超时重试等)。
  • 处理网络传输细节(如 TCP/HTTP 层的行为、多路复用等)。

协议和序列化的区别,用寄快递举例来讲,协议定义收发快递的规范(哪里发快递,怎么发以及收快递,快递的单子信息)、走陆运还是空运海运(通信流程)。序列化就是单纯决定用盒子装还是用袋子装这个东西。

一次客户端发起的 RPC 调用的流程是,协议规范传输行为 → 序列化处理数据内容 → 协议封装并发送数据。

Dubbo2/3 gRPC Thrift
传输协议 TCP 长连接(Dubbo 协议),HTTP/2(Triple 协议,Dubbo 3.x 引入) 基于 HTTP/2 TCP、HTTP 等
支持序列化方式 Hessian 2、Protobuf、JSON 等 Protobuf、JSON 等 TBinary(Thrift自研)、JSON、CompactProtocol 等
默认序列化方式 Hessian 2(Dubbo 协议);Protobuf(Triple 协议) Protobuf TBinary
跨语言支持能力 支持 Java 为主,对其他语言有一定支持,Triple 协议增强了跨语言能力 支持多语言,如 Java、C++、Python、Go 等 支持多语言,如 Java、C++、Python、PHP 等
社区活跃度 Apache Dubbo 社区活跃度较高,有大量企业应用和贡献 社区活跃,有广泛的用户和贡献者 社区有一定活跃度,在特定领域应用较多
核心优势 高性能,长连接减少开销;可扩展性,协议头部预留扩展字段;可靠性,支持心跳检测、超时重试 基于 HTTP/2 和 Protobuf 的高性能;多语言跨平台二进制兼容能力 高效二进制协议,节省带宽和提高性能;多语言支持
服务治理能力 Dubbo 框架具备完善的服务治理功能,如服务注册与发现、负载均衡、容错机制等 通过相关组件和工具可实现服务治理,如服务发现、负载均衡等 可通过扩展实现服务治理功能
流式通信 Triple 协议支持流式调用,Dubbo 协议默认不支持 支持客户端流、服务器流和双向流 支持流式传输

序列化

通常我们需要提供一个Serializer接口用以扩展序列化方式,序列化byte[] serialize(Object obj)和反序列化 T deserialize(byte[] bytes, Class clazz);。以下是常见的序列化框架,简单搜罗网上数据做了个横向对比表格,一般Java环境推荐个人使用Kryo和JSON来适配不同环境。

框架 设计定位 序列化速度 数据体积 跨语言支持 兼容性 社区活跃度 易用性 典型适用场景
Kryo 纯Java高性能序列化 极快 最小 否(仅Java) 低(字段增减需处理) 需管理线程安全/类注册 实时通信、游戏、内存缓存
Protostuff 基于Protobuf的无预编译Java优化版 有限(主Java) 中(末尾追加兼容) 无需预编译,但需处理无参构造 微服务通信、持久化存储
Protobuf 跨语言结构化数据协议 较快 高(版本兼容) 修改Schema需重新生成代码,需预编译.proto文件 多语言RPC、API协议、长期数据存储
Hessian/Hessian2 跨语言动态兼容协议 中等/较快(接近Protobuf) 大/中等 高(自动兼容) 开箱即用,无复杂配置 遗留系统集成、简单跨语言调用
JSON(Jackson/fastjson/gson) 可读性优先的文本序列化(其他为二进制,均不可读!) 较慢 最大(文本) 高(无结构约束) 直接操作POJO,开发便捷 前后端交互、调试日志
Java原生 内置序列化 极慢 极大 否(仅Java) 低(完全依赖类结构) 简单但效率低下 临时测试、简单场景
MessagePack 高效二进制跨语言序列化 小(紧凑) 中(字段顺序敏感) 中等活跃(持续更新) 类似JSON,需处理二进制 需POJO注解或预定义类 移动端、IoT设备、跨语言微服务

压缩

序列化方式合适的话会有一定的压缩率,但是我们仍然需要提供扩展接口Compress,压缩byte[] compress(byte[] bytes)和解压缩byte[] decompress(byte[] bytes)。比较常见的是GZIP,毕竟JDK内置确实方便好用,最好和序列化的一起打配合。

框架 设计定位 压缩速度 压缩率 解压速度 内存占用 兼容性 易用性 适用场景 社区活跃度
GZIP 高压缩比,长期存储 中等 跨平台(RFC 1952) JDK原生支持,简单API HTTP压缩、日志归档 稳定维护(JDK内置)
LZ4 极致速度优先,低延迟场景 极快 极快 需第三方库 依赖lz4-java库,API简洁 实时通信、内存缓存、游戏数据流 高活跃度(GitHub开源)
Snappy 速度与压缩率平衡,Google生态集成 中低 跨语言(C++/Java) 简单API(如Snappy.compress() 大数据传输(Hadoop、Kafka) 高活跃度(Google维护)
Bzip2 高压缩比,牺牲速度 极慢 最高 需第三方库 依赖commons-compress 离线数据归档、静态资源压缩 低活跃度(维护较少)
LZO 快速解压,适合读取密集型场景 快(解压) 极快 需第三方库 复杂配置(需指定算法版本) 日志分析、HDFS存储 停滞(社区支持有限)
LZMA/LZMA2 极高压缩比,适合长期归档 极慢 最高 中等 高(百MB级) 需第三方库 配置复杂(需设置字典大小等参数) 软件分发、历史数据归档 中等(7z生态维护)
QuickLZ 极速压缩,实时性优先 极快 中低 极快 低(KB级) 社区支持差,自己写 文档少,自己写 实时通信、嵌入式设备、数据库写入 低活跃度(更新停滞)

负载均衡

分布式部署的话肯定需要一个合理的负载均衡算法,这里也需要提供一个扩展接口。JSF里面还蛮全的,可以参考源码中的com.jd.jsf.gd.lb.Loadbalance,去找各个实现类,这里留一个JSF官方文档的表格,我觉得总结的还蛮好的。

名称 配置 版本 优点 缺点
随机(默认) random 1.0.0 1. 按权重的进行随机,可以方便的调整 2. 调用量越大分布越均匀 当调用次数比较少时,Random 产生的随机数可能会比较集中,此时多数请求会落到同一台服务器上。 这个缺点并不是很严重,多数情况下可以忽略。RandomLoadBalance 是一个简单,高效的负载均衡实现,因此它被作为缺省实现。
轮询 roundrobin 1.0.0 1. 轮循,按公约后的权重设置轮循比率 如果某个Provider较慢,可能会积压请求
最少并发优先 leastactive 1.0.0 1. 在调用前后增加计数器,并发最小的Provider认为是最快的Provider并发大的Provider认为是慢的Provider。 2. 并发数相同再比较权重。 3. 这样快的Provider收到更多请求。 快速抛异常的Provider有可能被误认为是响应快的Provider。 目前此问题已解决,Provider抛异常会降低选中概率,最低到10%,等Provider恢复并调用成功后,选中概率回恢复到100%。
一致性hash consistenthash 1.2.0 1. 服务端节点没变化的情况下,同样的请求(根据第一个参数值进行hash)会指向同一台机器 2. 基于虚拟节点,如果一台挂了也没事,会平摊到其它提供者 可能调用分布不均匀。
本机IP调用优先 localpref 1.5.1 1. 本机IP上的Provider优先调用 2. 匹配到多个使用随机调用本机IP,未匹配也随机调用远程机器 计算相同IP稍微增加点耗时。
最快响应 shortestresponse 1.7.2 根据平均响应时间 * 当前请求数,选取两者乘积最小的节点 cpu偏高
平滑加权轮询 swrr 1.7.7 平滑加权轮询算法,provider接受到的流量更加均匀 计算会消耗些cpu
自定义负载 Extensible里的value 1.7.2 自定义负载均衡算法,具体例子如下面

动态代理

随着JDK的版本更新JDK动态代理的效率越来越高了,Spring默认代理接口也是用的这个。在查询资料的时候发现之前没怎么了解的Byte Buddy,神灯冲浪的时候发现之前有人了解过,ASM字节码操作类库(打开java语言世界通往字节码世界的大门)

框架 实现原理 依赖 代理限制 性能 易用性 内存开销 适用场景
JDK动态代理 基于接口,通过反射生成代理类 JDK原生支持 仅支持接口 -生成速度:快(4 ms) -调用速度:反射较慢(约136 ms/千万次调用) 简单(仅需实现InvocationHandler接口) 低(代理类由JVM缓存) 接口代理(如Spring AOP默认策略)
CGLIB 基于继承,通过字节码增强生成目标类的子类 需引入cglib 可代理普通类(无法代理final类和方法) -生成速度:较慢(108 ms) -调用速度:较快(约215 ms/千万次) 中等(需配置EnhancerMethodInterceptor 较高(生成子类字节码) 类代理(如Spring无接口时的AOP)
Javassist 基于源码或字节码操作,通过字符串拼接生成类和方法 需引入javassist 可代理普通类(需处理构造函数,支持动态生成新类) -生成速度:中(39 ms) -调用速度:慢(1690 ms/千万次) 高(通过字符串拼接生成代码,无需了解字节码) 中等(支持动态生成和缓存) 快速原型开发、动态生成类(如ORM工具)
ASM 直接操作JVM字节码指令,基于访问者模式生成类 需引入asm 可代理任何类(包括final类,但无法覆盖final方法) -生成速度:极快(4 ms) -调用速度:最快(172 ms/千万次) 低(需熟悉JVM指令和类结构) 低(直接生成紧凑字节码) 高性能场景(如序列化框架Kryo)
Byte Buddy 基于ASM的高层抽象,提供类型安全的DSL生成字节码 需引入byte-buddy依赖 可代理任何类(支持复杂字节码操作) -生成速度:快(接近ASM) -调用速度:接近ASM(179 ms/千万次) 中等(需学习DSL语法,但比ASM友好) 低(优化内存管理) 复杂字节码操作(如Mockito、Hibernate)

写在最后

这篇写在今年的二月底吧,当时是计划去写定制化开发的RPC的,结果想了想还是应该从源头开始,顺道我也相对系统的建立起RPC的知识体系。本文相对来说已经非常详细了,当时本来打算是四部曲,原理、实战、公司框架分析和主流RPC对比。结果写原理也就是本篇写了一半,公司给推了一篇大佬写的公司框架分析,好家伙写的真好,瞬间就觉得没必要重复写了,实战篇在后面马上出,主流RPC对比不一定会写的,但是我比较想看看GRPC,如果有机会的话会拜读一下。emmm,这半年的总结,躺了这么久也该回归了,虽然还要和生活搏斗,但是老本行不能忘了。最近在做一个还算有点意思的东西,写了一段有点意思的代码,应该下个月就会分享出来。工作上有意思的东西还不算特别多,之前写了太多东西,其实新的环境也有点雷同,公司的东西封装的太狠,说实话我也没有细细品味,仅仅是在用着,不过文档都写得蛮不错的。对了,实战篇的时候,我参杂着公司的RPC文档来聊些私货吧,有些设计思路我觉得可以参考,毕竟中间件大部分都是差不多的思路。

聊聊生活吧,又是深夜,沟槽的世界,到家10点,感觉还没干啥就0点多了,能不能给单身狗留一点追逐爱情的时间(大雾)。最近也是感受到了父母急切的心情,博主快28了,谁能想到还是一个纯情大男孩,淦,品味着男女对立的资讯,抱着吃屎的决心还要冲锋,这就是生活,一个巨大的SM场。当然我觉得我是看清了情感,所以才能更好地去追逐爱情,不过北京的相亲市场属实地狱,说是相亲其实也就扩列,我还是相对喜欢这种日久生情的感觉。但,哈哈,有时候就感觉男女沟通确实是门学问,大家都得学,还有思想独立拎得清的女生确实不多。以上就是我的一些简单吐槽,说是吃屎,但是我觉得这是我对期待生活和伴侣需要付出的代价吧,向阳而生吧,少年!

相关推荐
卑微小文6 分钟前
企业级IP代理安全防护:数据泄露风险的5个关键防御点
前端·后端·算法
lovebugs10 分钟前
如何保证Redis与MySQL双写一致性?分布式场景下的终极解决方案
后端·面试
斑鸠喳喳22 分钟前
模块系统 JPMS
java·后端
kunge201324 分钟前
【手写数字识别】之数据处理
后端
SimonKing25 分钟前
Redis7系列:百万数据级Redis Search 吊打 ElasticSearch
后端
uhakadotcom28 分钟前
Python应用中的CI/CD最佳实践:提高效率与质量
后端·面试·github
AI小智1 小时前
MCP:昙花一现还是未来标准?LangChain 创始人激辩实录
后端
bobz9651 小时前
strongswan IKEv1 proposal 使用
后端
Sans_2 小时前
初识Docker-Compose(包含示例)
后端·docker·容器