朋友,你也不想不懂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场。当然我觉得我是看清了情感,所以才能更好地去追逐爱情,不过北京的相亲市场属实地狱,说是相亲其实也就扩列,我还是相对喜欢这种日久生情的感觉。但,哈哈,有时候就感觉男女沟通确实是门学问,大家都得学,还有思想独立拎得清的女生确实不多。以上就是我的一些简单吐槽,说是吃屎,但是我觉得这是我对期待生活和伴侣需要付出的代价吧,向阳而生吧,少年!

相关推荐
桦说编程13 分钟前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研16 分钟前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi39 分钟前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国2 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy2 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack2 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9653 小时前
pip install 已经不再安全
后端
寻月隐君3 小时前
硬核实战:从零到一,用 Rust 和 Axum 构建高性能聊天服务后端
后端·rust·github
Pitayafruit5 小时前
Spring AI 进阶之路03:集成RAG构建高效知识库
spring boot·后端·llm
我叫黑大帅5 小时前
【CustomTkinter】 python可以写前端?😆
后端·python