
1. 从“对话”到“通信”为什么协议设计是系统架构的基石在软件开发的江湖里我们每天都在和各种“协议”打交道只是很多时候我们不自知。你打开一个网页浏览器和服务器之间遵循着HTTP协议你发一条微信消息客户端和服务器之间有一套复杂的私有协议甚至你电脑里的CPU和内存也在通过总线协议进行着高速的数据交换。今天我们不聊那些宏大的、标准化的协议我们来聊聊在一个具体的、由我们自己掌控的系统内部如何设计一套高效、可靠、易于维护的通信协议。这不仅仅是“kimi-code”系列文章中的一个技术点更是任何一个想要构建健壮分布式系统或复杂单体应用的开发者都必须跨过的一道坎。很多人一听到“协议设计”就觉得是网络大神或者底层系统工程师的专属领域离业务开发很远。其实不然。想象一下你正在开发一个微服务架构的电商系统。订单服务需要调用库存服务来扣减库存支付服务需要通知物流服务准备发货。这些服务之间的每一次“对话”都需要一个明确的“约定”我该用什么格式跟你说我说“库存-1”你听得懂吗如果网络突然断了我的话只说了一半怎么办你处理完了该怎么告诉我结果这些“约定”就是通信协议。一个糟糕的协议设计会让系统变成“鸡同鸭讲”的混乱现场调试起来如同大海捞针而一个精良的协议设计则能让系统各个模块像训练有素的交响乐团协同流畅扩展自如。所以这篇文章的目的不是教你背诵某种特定协议比如gRPC的Protobuf格式或者HTTP/2的帧结构而是带你深入理解协议设计的核心思想与通用方法论。我们会从最朴素的字节流开始一步步构建起一个具备可扩展性、兼容性和高性能的协议并最终探讨如何基于这样的协议实现一个简洁而强大的RPC远程过程调用框架。无论你是正在为团队内部服务间通信而烦恼还是对网络编程的底层原理充满好奇相信接下来的内容都能给你带来实实在在的启发。2. 协议的本质定义数据交换的“宪法”在深入设计细节之前我们必须先统一思想协议到底是什么你可以把它理解为数据交换的“宪法”或“交通规则”。它不关心你车里坐的是谁业务数据但它严格规定了你是靠左行驶还是靠右行驶字节序、红灯停绿灯行状态码、以及发生事故后的处理流程错误处理。一个完整的通信协议通常需要定义以下几个核心要素传输载体Transport数据通过什么渠道传输是TCP Socket的可靠字节流还是UDP的数据报甚至是共享内存或管道这决定了协议的可靠性基础。消息边界Message Framing在字节流中如何区分一条消息的开始和结束这是协议设计首先要解决的问题否则接收方会面对一串无尽的字节不知道从哪里切开。消息格式Message Format一条完整的消息内部应该如何组织通常包括消息头Header和消息体Body。头里存放元数据体里存放业务数据。序列化与反序列化Serialization/Deserialization如何将内存中的对象结构体、类实例转换成可以在网络中传输的字节序列序列化以及如何将接收到的字节序列还原成内存对象反序列化错误处理与状态Error Handling Status通信过程中可能发生各种错误数据损坏、超时、服务不可用等。协议需要定义如何表达这些错误状态。兼容性与版本Compatibility Versioning系统需要升级协议可能改变。新版本的服务如何与旧版本的客户端通信协议需要具备向前/向后兼容的能力。接下来我们就围绕这些要素从零开始设计一个协议。我们将这个协议命名为“SimpleRPC Protocol”目标是足够简单以理解原理又具备实际可用的核心特性。2.1 第一步解决消息边界问题——定长与不定长TCP是流式协议它只保证字节的顺序和可靠送达但不保证“消息”的边界。发送方连续调用两次send(“Hello”)和send(“World”)接收方可能一次recv就收到了 “HelloWorld”。因此我们需要在应用层自己划分消息边界。常见方案有以下几种定长消息每条消息固定长度比如总是128字节。不足部分补零。这种方式简单粗暴但极不灵活浪费带宽基本只用于非常特定的场景。分隔符用一个特殊的字符序列作为消息的结束标志比如换行符\n很多文本协议如Redis CLI协议、或者自定义的\r\n\r\nHTTP头部结束。缺点是消息体本身不能包含分隔符否则会被错误切分通常需要对消息体进行转义如JSON字符串中的引号增加复杂度。长度前缀Length-Prefixed这是最主流、最灵活的方式。在消息体前面先发送一个代表消息体长度的字段。接收方先读取这个长度值N然后再读取后续N个字节这就是一条完整的消息。我们的选择长度前缀。因为它既高效又安全消息体可以包含任意字节。现在问题转化为这个“长度”字段本身有多长用几个字节表示如果用1个字节uint8_t最大只能表示255字节显然不够。如果用2个字节uint16_t最大65535字节约64KB对于大多数RPC请求/响应可能够用但传输文件时就捉襟见肘。如果用4个字节uint32_t最大约4GB这几乎能满足所有场景。如果用8个字节uint64_t有点杀鸡用牛刀。一个更工程化的做法是采用可变长度整数编码比如Protobuf使用的Varint。但对于我们的示例为了清晰起见我们选择使用4字节网络字节序大端序的无符号整数作为长度前缀。这意味着我们的消息结构变成了[4字节长度N] [N字节消息体]注意关于字节序。网络字节序Big-Endian是TCP/IP协议族规定的标准字节序即高位字节在前。在C/C中可以使用htonl()和ntohl()函数在主机序和网络序之间转换。在更高级的语言如Go、Java中直接操作字节流时也需要注意这一点。我们的协议规定所有多字节整型字段均采用大端序。2.2 第二步设计消息格式——头与体的分离有了长度前缀我们解决了“读多少”的问题。接下来要解决“读到的内容是什么”的问题。我们需要在消息体内部进一步划分结构。一个经典的设计是将消息体分为消息头Header和消息体Payload。注意这里的“消息体Payload”是狭义的指业务数据而外层整个结构我们称为“消息帧Frame”。为了避免混淆我们重新定义一下术语帧Frame一次通信传输的完整单元。[长度前缀] [帧数据]。帧数据Frame Data[帧头Header] [载荷Payload]。帧头Header存放控制信息是协议本身需要的信息。载荷Payload存放业务信息对协议透明。那么帧头应该包含哪些信息呢这取决于你的业务需求。一个典型的RPC协议头可能包含魔数Magic Number比如0x5A5A。用于快速校验接收到的数据是否是我们协议的数据包防止端口错乱或数据错位。接收方可以先读取并校验魔数如果不匹配则直接丢弃避免无谓的解析。版本号Version1字节。用于协议升级和兼容性判断。消息类型Message Type1字节。用于区分这是RPC请求、RPC响应、心跳包、还是控制命令如断开连接。序列号Sequence ID4字节。一个自增的ID用于将请求和响应关联起来。这是实现异步RPC的关键。状态码Status Code2字节。仅在响应消息中有意义表示调用结果成功、业务错误、系统错误等。头部扩展长度Header Extension Length2字节。为未来的头部扩展预留空间。载荷序列化类型Payload Serialization Type1字节。标识载荷是用JSON、Protobuf、MessagePack还是其他方式序列化的。载荷长度Payload Length4字节。注意这个长度和最外层的长度前缀是不同的。外层长度 帧头长度 载荷长度。这里记录载荷长度方便直接定位和提取载荷。根据以上设计我们可以定义帧头的二进制布局如下共16字节0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -------------------------------- | Magic (0x5A5A) |Version|MsgType| SeriType | -------------------------------- | Sequence ID | -------------------------------- | Status Code | Header Ext Len (HExLen) | -------------------------------- | Payload Length | --------------------------------Magic: 2字节固定为0x5A5A。Version: 高4位协议版本例如0x1。MsgType: 低4位消息类型。0x01请求0x02响应0x03心跳。SeriType: 1字节序列化类型。0x01JSON0x02Protobuf。Sequence ID: 4字节大端序。Status Code: 2字节大端序。0x0000成功其他为错误码。Header Ext Len: 2字节大端序。头部扩展区的长度。如果为0表示无扩展。Payload Length: 4字节大端序。因此一个完整的帧结构如下[4字节 总长度 (L)] [16字节 固定头] [HExLen字节 扩展头] [Payload Length字节 载荷]其中总长度 L 16 HExLen Payload Length。实操心得头部的可扩展性。将固定头部设计为定长并通过Header Ext Len字段预留扩展空间是一种非常实用的设计。未来如果需要增加如“超时时间”、“调用链追踪ID”等字段可以将其放入扩展头中而无需修改固定头的结构保证了协议的向前兼容性。旧版本的程序看到HExLen 0可以跳过这部分字节继续解析新版本的程序则可以读取并理解扩展信息。3. 序列化之争如何将对象变成字节流协议定义了传输的格式而序列化则决定了载荷Payload的具体内容如何组织。这是影响性能、兼容性和开发效率的关键选择。3.1 常见序列化方案对比JSON优点人类可读跨语言支持极好调试方便。是Web领域的绝对主流。缺点冗余度高字段名反复出现解析性能相对较差无二进制数据原生支持需Base64无严格的模式Schema约束容易因字段名拼写错误导致问题。适用场景对性能要求不高、需要频繁人工查看或与前端交互、快速原型开发的场景。XML优点人类可读支持复杂的层次结构和属性有严格的模式定义XSD。缺点冗余度比JSON还高解析性能更差使用复杂度高。适用场景传统企业级应用、配置文件如Android布局、需要强模式验证的场景。Protocol Buffers (Protobuf)优点二进制编码体积小序列化/反序列化速度极快有强类型的.proto模式文件能自动生成多语言代码向前向后兼容性设计得非常好。缺点二进制不可读需要预编译生成代码动态性较差。适用场景高性能内部服务通信、需要严格接口契约和长期兼容性的场景。gRPC的默认序列化方式。MessagePack优点二进制编码体积比JSON小序列化速度比JSON快号称“像JSON一样简单”无需模式定义动态性强。缺点二进制不可读虽然无模式很灵活但也失去了模式带来的安全性和优化潜力。适用场景需要在性能和易用性间取得平衡且不希望引入编译步骤的场景。Apache Thrift优点与Protobuf类似提供完整的RPC框架而不仅仅是序列化支持多种传输层和服务器模型。缺点生态略逊于Protobuf复杂度更高。适用场景需要一体化RPC解决方案且对Protobuf不感冒的团队。FlatBuffers / Cap‘n Proto优点零拷贝反序列化性能极致。无需先解析整个结构再到内存中构建对象可以直接在原始字节缓冲区上访问数据。缺点使用复杂度高生态相对小众。适用场景对性能有极端要求的场景如游戏、高频交易。3.2 为SimpleRPC协议选择序列化对于我们的SimpleRPC协议为了兼顾教学和实用性我们选择支持两种最典型的序列化方式JSON和Protobuf。通过帧头中的SeriType字段来区分。当SeriType 0x01载荷为UTF-8编码的JSON字符串。当SeriType 0x02载荷为Protobuf编码的二进制数据。为什么同时支持两种这体现了协议的灵活性。在开发调试阶段可以使用JSON方便用tcpdump、Wireshark抓包查看或者直接telnet手动发送请求。在上线生产环境时切换为Protobuf以获得最佳性能。客户端和服务端通过协商可以在连接建立时或每个请求独立指定来决定使用哪种格式。一个JSON载荷的例子调用UserService.GetUser{ service: UserService, method: GetUser, args: { user_id: 12345 } }对应的Protobuf模式定义.proto文件syntax proto3; package simple_rpc; message RpcRequest { string service 1; string method 2; bytes args 3; // 这里args是另一个Protobuf消息序列化后的字节需要二次解析。更优做法是使用Any类型或oneof。 } message GetUserArgs { int64 user_id 1; } message RpcResponse { int32 code 1; string msg 2; bytes result 3; } message User { int64 id 1; string name 2; string email 3; }在实际高性能设计中args和result字段通常会使用更高效的方式比如预注册参数/返回类型或者像gRPC那样为每个方法预生成强类型的请求/响应消息。这里为了展示通用性采用了嵌套序列化的方式。踩坑实录序列化与兼容性。我曾在一个项目初期为了快速上线使用了JSON。随着业务复杂接口字段越来越多经常出现客户端传了userName而服务端期望username的字段名大小写错误或者某个可选字段未传导致服务端解析异常。后来全面转向Protobuf通过.proto文件明确定义接口编译时就能发现类型不匹配并且利用Protobuf的字段编号和optional/repeated规则轻松实现了字段的增删新旧版本服务可以无缝协作维护成本大幅下降。强烈建议在严肃的生产环境中使用带模式的二进制序列化方案。4. 核心实现编解码器与协议解析器有了协议格式和序列化方案我们需要用代码来实现协议的“编码”和“解码”过程。这部分通常被称为编解码器Codec或协议解析器Protocol Parser。4.1 解码器Decoder的实现要点解码器的工作是从一个TCP Socket或其他字节流中正确地还原出一个个完整的消息帧对象。这是一个典型的状态机过程需要处理“粘包”和“拆包”问题。核心步骤读取长度前缀尝试从缓冲区Buffer中读取至少4个字节。如果不够则等待更多数据EAGAIN/EWOULDBLOCK。读到后解析出总长度L。检查数据完整性检查缓冲区中剩余的可读字节数是否大于等于L。如果不够说明一个完整的帧还没到达继续等待。读取帧数据从缓冲区中切分出L字节的数据这就是一个完整的帧数据。解析帧头从帧数据的前16字节解析出固定头部的各个字段。首先校验魔数不正确则丢弃数据并可能关闭连接说明数据错乱。根据版本号和消息类型决定后续处理逻辑。处理扩展头根据Header Ext Len跳过或解析扩展头。提取和反序列化载荷根据Payload Length提取出载荷字节数组再根据SeriType调用对应的反序列化器JSON或Protobuf将字节数组转换成内存中的请求/响应对象。这里有一个关键设计缓冲区Buffer的管理。绝对不能假设一次recv调用就能拿到一个完整帧。必须有一个应用层的缓冲区来累积数据。常见的实现是每个TCP连接关联一个动态增长的字节数组如std::vectorchar或ByteBuf。每次recv到数据就追加到缓冲区末尾然后尝试从缓冲区头部开始解码。伪代码示意以非阻塞IO为例class FrameDecoder: def __init__(self): self.buffer bytearray() self.state READ_LENGTH self.expected_length 0 def feed_data(self, data): self.buffer.extend(data) while True: if self.state READ_LENGTH: if len(self.buffer) 4: break self.expected_length struct.unpack(I, self.buffer[:4])[0] # 大端序解析 self.buffer self.buffer[4:] self.state READ_FRAME if self.state READ_FRAME: if len(self.buffer) self.expected_length: break frame_data self.buffer[:self.expected_length] self.buffer self.buffer[self.expected_length:] frame self._parse_frame(frame_data) self.state READ_LENGTH yield frame # 返回解析好的帧对象 else: break def _parse_frame(self, data): # 解析16字节固定头... magic, version, msg_type, seri_type struct.unpack(HBBH, data[:6]) # 示例实际需按定义解析 # ... 解析其他字段 payload_data data[header_len: header_len payload_len] # 根据 seri_type 反序列化 payload_data # ... return frame_obj4.2 编码器Encoder的实现要点编码器的工作正好相反将一个内存中的消息对象按照协议格式编码成字节流然后通过send发送。这个过程相对简单因为所有信息都是已知的。序列化载荷根据配置的序列化类型将请求/响应对象序列化成字节数组。构建帧头填充魔数、版本、消息类型、序列号、状态码、扩展头长度0、载荷长度等字段。计算总长度总长度 固定头长度 扩展头长度 载荷长度。组装字节流先写入4字节的总长度大端序再写入帧头字节最后写入载荷字节。发送将组装好的字节流通过send系统调用发送。注意send也可能只发送了部分数据需要循环发送直到全部完成。注意事项非阻塞IO下的发送。在非阻塞模式下send可能只发送了部分数据就返回EAGAIN。因此编码器通常也需要维护一个“发送缓冲区”将未发送完的数据暂存起来并在Socket可写时继续发送。这与解码器的接收缓冲区是对称的。一个健壮的网络库必须妥善处理这两种缓冲。5. 从协议到RPC构建一个简易的RPC框架协议是通信的基石而RPC远程过程调用则是建立在协议之上的一种高级抽象。它的目标是让开发者像调用本地函数一样调用远程服务隐藏网络通信的复杂性。现在我们已经有了SimpleRPC协议可以在此基础上构建一个简易的RPC框架。一个最简化的RPC框架核心包含以下组件客户端存根Stub由框架根据服务接口自动生成。它看起来像一个本地对象但内部会将方法调用、参数打包序列化成一个RPC请求消息通过协议发送给服务器并等待响应最后将响应结果反序列化后返回。服务端骨架Skeleton负责接收协议报文反序列化得到请求信息服务名、方法名、参数通过反射或注册表找到对应的本地服务实现类和方法进行调用然后将返回值序列化打包成响应消息发回。传输层Transport管理TCP连接、处理协议的编解码、网络IO。这部分就是我们前面实现的编解码器和网络事件循环。注册中心Registry可选用于服务发现。客户端从注册中心查找服务提供者的地址列表。在更简单的点对点RPC中可以直接配置地址。5.1 定义服务接口与实现首先我们需要一种方式来描述服务。这里我们使用一个简单的接口定义// 服务接口示例 (Java) public interface UserService { User getUser(long userId) throws RpcException; boolean updateUser(User user) throws RpcException; }框架的任务是生成UserService的“存根”实现。5.2 客户端存根的动态代理在Java中可以使用动态代理Dynamic Proxy或字节码增强如Byte Buddy、CGLib来生成存根。以下是一个基于JDK动态代理的极简示例public class RpcClientProxy implements InvocationHandler { private final String host; private final int port; private final Codec codec; // 编解码器封装了我们的SimpleRPC协议 private final Transport transport; // 传输层管理连接池和发送 public RpcClientProxy(String host, int port) { this.host host; this.port port; this.codec new SimpleRpcCodec(); this.transport new NettyTransport(); // 假设使用Netty } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 1. 构建RPC请求对象 RpcRequest request new RpcRequest(); request.setServiceName(method.getDeclaringClass().getSimpleName()); request.setMethodName(method.getName()); request.setArguments(serializeArgs(args)); // 序列化参数 request.setSequenceId(generateId()); // 2. 通过传输层发送请求并同步等待响应 RpcResponse response transport.sendSync(host, port, request, 5000); // 超时5秒 // 3. 处理响应 if (response.getStatusCode() ! 0) { throw new RpcException(response.getStatusCode(), response.getErrorMsg()); } // 4. 反序列化结果并返回 return deserializeResult(response.getResult(), method.getReturnType()); } public T T createProxy(ClassT serviceInterface) { return (T) Proxy.newProxyInstance( serviceInterface.getClassLoader(), new Class?[]{serviceInterface}, this ); } } // 使用方式 UserService userService rpcClient.createProxy(UserService.class); User user userService.getUser(12345); // 看起来是本地调用实际发生了网络通信5.3 服务端的反射调用服务端需要维护一个服务注册表将服务名和实际的服务实例关联起来。public class ServiceRegistry { private static final MapString, Object serviceMap new ConcurrentHashMap(); public static void register(String serviceName, Object serviceImpl) { serviceMap.put(serviceName, serviceImpl); } public static Object getService(String serviceName) { return serviceMap.get(serviceName); } } // 在协议解码器收到一个完整的RpcRequest后 public void processRequest(RpcRequest request, Channel channel) { // 1. 查找服务实例 Object service ServiceRegistry.getService(request.getServiceName()); if (service null) { sendError(channel, request.getSequenceId(), Service not found); return; } // 2. 通过反射找到方法 Method method findMethod(service.getClass(), request.getMethodName(), request.getArguments()); if (method null) { sendError(channel, request.getSequenceId(), Method not found); return; } // 3. 反序列化参数 Object[] args deserializeArgs(request.getArguments(), method.getParameterTypes()); // 4. 调用本地方法 try { Object result method.invoke(service, args); // 5. 构建成功响应 RpcResponse response buildSuccessResponse(request.getSequenceId(), result); channel.writeAndFlush(response); // 通过协议编码器发送 } catch (Exception e) { // 6. 构建异常响应 RpcResponse response buildErrorResponse(request.getSequenceId(), e); channel.writeAndFlush(response); } }5.4 异步化与Future/Promise上面的客户端示例是同步调用会阻塞线程直到收到响应。在高性能场景下我们需要支持异步调用。这可以通过Future/Promise模式实现。客户端发送请求后立即返回一个Future对象而不阻塞。这个Future内部持有一个Promise。当网络层在另一个线程如IO线程收到对应序列号Sequence ID的响应时会设置Promise的结果。用户可以在任何地方通过Future.get()同步等待结果或者通过Future.addListener()添加回调。核心变化客户端存根在invoke方法中不再同步等待而是将请求放入发送队列并立即创建一个DefaultFuture返回。传输层维护一个MapSequenceId, DefaultFuture发送请求时注册这个Future。当收到响应时根据响应中的SequenceId找到对应的DefaultFuture并设置其结果。用户线程调用future.get()时如果结果未就绪则等待如wait/notify或Condition。// 简化的异步调用 public class RpcClientProxy implements InvocationHandler { // ... 其他字段 private final MapLong, DefaultFuture futureMap new ConcurrentHashMap(); Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { RpcRequest request buildRequest(method, args); long sequenceId request.getSequenceId(); DefaultFuture future new DefaultFuture(sequenceId); futureMap.put(sequenceId, future); // 异步发送不等待 transport.sendAsync(host, port, request); // 返回Future用户决定何时获取结果 return future; } // 由网络线程回调 public void receiveResponse(RpcResponse response) { long sequenceId response.getSequenceId(); DefaultFuture future futureMap.remove(sequenceId); if (future ! null) { if (response.getStatusCode() 0) { future.setSuccess(deserializeResult(response.getResult(), ...)); } else { future.setFailure(new RpcException(...)); } } } }经验技巧序列号Sequence ID的管理与超时。Sequence ID必须在单个连接内唯一通常用一个原子长整型自增生成。务必记得在收到响应或请求超时后从futureMap中清理对应的Future防止内存泄漏。超时机制是生产级RPC的必备功能。可以在创建Future时启动一个定时任务超时后主动将Future标记为失败并清理避免用户线程永久阻塞。6. 高级话题与生产级考量我们设计的SimpleRPC协议和框架只是一个教学原型。要将其用于生产环境还需要考虑大量工程细节6.1 连接管理连接池为每个服务地址维护一个TCP连接池避免每次RPC调用都建立连接三次握手的开销。心跳与保活在帧类型中定义心跳包MsgType0x03。客户端定时发送心跳服务端回复。用于检测连接是否存活及时清理僵尸连接。长时间无数据交互时TCP Keep-Alive机制可能不够及时。多路复用一个连接上可以同时进行多个请求-响应交互通过Sequence ID来区分。这是提高吞吐量的关键。6.2 服务治理负载均衡当有多个服务提供者时客户端需要从列表中选择一个。策略包括随机、轮询、加权轮询、一致性哈希等。熔断与降级当某个服务节点失败率过高时客户端应暂时不再向其发送请求熔断并执行降级逻辑如返回缓存数据、默认值或友好错误。限流服务端需要保护自己不被突发流量打垮可在协议层面或应用层面实现限流如令牌桶、漏桶算法。6.3 性能优化IO模型使用非阻塞IONIO和多路复用器如Linux的epollJava的NIONetty框架来处理海量连接避免“一个线程一个连接”的阻塞模型。序列化优化选择高性能的序列化方案如Protobuf, Kryo, FST甚至针对特定业务结构定制序列化器。线程模型精心设计IO线程和工作线程的分工。通常IO线程只负责编解码和网络读写将耗时的业务逻辑抛给独立的工作线程池避免阻塞IO线程。6.4 可观测性链路追踪在协议扩展头中传递追踪IDTraceID、跨度IDSpanID便于在分布式系统中追踪一个请求的完整调用路径。指标监控收集并暴露RPC调用的QPS、延迟、成功率等指标用于监控和告警。7. 总结与个人体会通信协议的设计本质上是在确定性、效率、灵活性和复杂度之间寻找平衡。从最原始的字节流切割到包含丰富元数据的帧头再到支持多种序列化格式的载荷每一步设计决策都对应着不同的应用场景和权衡。自己动手实现一遍简单的协议和RPC框架即使功能简陋其价值也远大于单纯学习某个成熟框架如Dubbo, gRPC的API。这个过程会让你深刻理解网络编程的复杂性粘包拆包、缓冲区管理、同步异步、超时重试这些都不是凭空产生的概念而是为了解决实际问题。抽象的价值RPC框架将网络通信、序列化、服务发现等脏活累活封装起来让业务开发者可以专注于核心逻辑。理解底层才能更好地使用上层抽象。设计模式的应用在框架实现中你会自然地用到动态代理、工厂、责任链、观察者等多种设计模式。在实际工作中除非有极其特殊的性能或定制化需求否则我强烈建议直接使用成熟的RPC框架如 gRPC、Apache Thrift、Dubbo 等。它们经过了大规模生产环境的验证在性能、功能、稳定性、生态方面都远超个人实现的玩具框架。但是知其然并知其所以然能让你在遇到框架本身的问题时有能力进行排查、定制甚至贡献代码。最后关于协议设计我个人最深的体会是向前兼容性是协议设计的生命线。在最初设计头部时多花一点心思预留扩展字段就像我们设计的Header Ext Len未来就能避免很多“协议版本不兼容”导致的线上事故。一个优秀的协议应该能让新旧版本的服务在大多数情况下能够和平共处优雅降级。