新协议

qianmoQqianmoQ· 更新于 2026-10-05· 阅读 13 分钟· 0 次阅读

登录后可跨设备保存划线和私人笔记登录

新协议

了解如何向 bRPC 中添加新协议。

服务端的多协议支持

brpc server 在同一个端口上支持所有协议,这在大多数情况下让部署和运维更加便捷。由于不同协议的数据格式差异很大,在同一端口上无歧义地支持所有协议是很困难的。考虑到解耦与可扩展性,为所有协议构建一个复用器同样困难。因此我们的做法是把所有协议分成三类,逐一尝试匹配:

  • 一类协议:协议数据的开头标有特殊字符,例如协议 baidu_std](https://github.com/apache/brpc/blob/master/docs/cn/baidu_std.md) 和 hulu_pbrpc 的数据分别以 ‘PRPC’ 和 ‘HULU’ 开头。解析器只需检查前四个字符即可判断协议是否匹配。这一类协议会被最先检查,并且所有这些协议可以共享同一个 TCP 连接。
  • 二类协议:一些没有特殊标记字符的复杂协议,只有在解析了部分输入数据之后才能被识别。目前只有 HTTP 被归入这一类。
  • 三类协议:特殊字符位于协议数据的中间,例如 nshead 协议的魔数位于第 25 至 28 个字符。处理这种情况很复杂,因为在读取前 28 个字节之前,我们无法确定该协议是否为 nshead。如果在 http 之前尝试它,那么长度小于 28 字节的 http 消息可能无法被解析,因为解析器会将其视为不完整的 nshead 消息。

考虑到大多数连接中只会使用一种协议,我们会记录上一次选择的结果,当后续数据到达时优先尝试该协议。这使长连接的协议匹配开销几乎降为零。虽然短连接每次都要执行协议匹配过程,但短连接的瓶颈并不在这里,这种方式仍然足够快。如果未来 brpc 中新增了大量协议,我们可能会考虑采用一些启发式方法来匹配协议。

客户端的多协议支持

与服务端必须根据连接上的数据动态判定协议不同,客户端作为发起方,天然知道自己使用的协议格式。只要协议数据是通过连接池或短连接发送的,即独占使用该连接,那么协议可以具有任何复杂(或糟糕)的格式。由于客户端在发送数据时会记录协议,当响应回来时,它会直接使用记录的协议来解析数据,没有任何匹配开销。某些协议(如 memcache、redis)没有魔数,在服务端很难区分,但在客户端则没有问题。

支持新协议

brpc 的设计允许随时添加新协议,只需按以下步骤操作:

以 nshead 开头的协议已获得统一支持,只需阅读 此] 即可。

添加 ProtocolType

在 options.proto] 的 ProtocolType 中添加新的协议类型。如果您需要添加新协议,请联系我们,由我们为您添加,以确保不会与其他人的协议产生冲突。

目前我们在 ProtocolType 中支持的类型(截至 2018 年年中):

enum ProtocolType {
    PROTOCOL_UNKNOWN = 0;
    PROTOCOL_BAIDU_STD = 1;
    PROTOCOL_STREAMING_RPC = 2;
    PROTOCOL_HULU_PBRPC = 3;
    PROTOCOL_SOFA_PBRPC = 4;
    PROTOCOL_RTMP = 5;
    PROTOCOL_HTTP = 6;
    PROTOCOL_PUBLIC_PBRPC = 7;
    PROTOCOL_NOVA_PBRPC = 8;
    PROTOCOL_NSHEAD_CLIENT = 9;        // implemented in brpc-ub
    PROTOCOL_NSHEAD = 10;
    PROTOCOL_HADOOP_RPC = 11;
    PROTOCOL_HADOOP_SERVER_RPC = 12;
    PROTOCOL_MONGO = 13;               // server side only
    PROTOCOL_UBRPC_COMPACK = 14;
    PROTOCOL_DIDX_CLIENT = 15;         // Client side only
    PROTOCOL_REDIS = 16;               // Client side only
    PROTOCOL_MEMCACHE = 17;            // Client side only
    PROTOCOL_ITP = 18;
    PROTOCOL_NSHEAD_MCPACK = 19;
    PROTOCOL_DISP_IDL = 20;            // Client side only
    PROTOCOL_ERSDA_CLIENT = 21;        // Client side only
    PROTOCOL_UBRPC_MCPACK2 = 22;       // Client side only
    // Reserve special protocol for cds-agent, which depends on FIFO right now
    PROTOCOL_CDS_AGENT = 23;           // Client side only
    PROTOCOL_ESP = 24;                 // Client side only
    PROTOCOL_THRIFT = 25;              // Server side only
}

实现回调

所有回调都在 struct Protocol 中定义,该结构定义于 protocol.h] 中。在这些回调里,parse 是必须实现的回调。此外,服务端必须实现 process_request,客户端必须实现 serialize_request、pack_request 和 process_response。

实现协议的回调比较困难。这些代码不像普通用户编写的代码那样拥有良好的提示和保护机制。你需要研究其他协议中对类似代码的处理方式,实现自己的协议,然后将其发送给我们进行代码评审。

parse

typedef ParseResult (*Parse)(butil::IOBuf* source, Socket *socket, bool read_eof, const void *arg);

此函数用于从 source 中切分消息。客户端与服务端必须使用同一个解析函数。返回的消息会被传递给 process_request(服务端)或 process_response(客户端)。

参数:source 是来自远端的二进制内容,socket 是对应的连接,read_eof 仅在连接被远端关闭时为 true,arg 是指向服务端的指针(在服务端模式下)或 NULL(在客户端模式下)。

ParseResult 可能是一个错误,也可能是一条切分出的消息,其可能的取值包括:

  • PARSE_ERROR_TRY_OTHERS:当前协议不匹配,框架会尝试下一个协议。source 中的数据不能被消费。
  • PARSE_ERROR_NOT_ENOUGH_DATA:输入数据尚未违反当前协议,但同时也无法判定为一条完整消息。当连接上有新数据到达时,新数据会被追加到 source 中并再次调用解析函数。如果能够确定数据符合当前协议,source 中的内容也可以被转移到协议的内部状态中。例如,如果 source 中不包含一条完整的 http 消息,它会被 http 解析器消费,以避免重复解析。
  • PARSE_ERROR_TOO_BIG_DATA:消息尺寸过大,将关闭连接以保护服务器。
  • PARSE_ERROR_NO_RESOURCE:内部错误,例如资源分配失败。连接将被关闭。
  • PARSE_ERROR_ABSOLUTELY_WRONG:本应是某种协议(magic number 已匹配),但格式与预期不符。连接将被关闭。

serialize_request

typedef bool (*SerializeRequest)(butil::IOBuf* request_buf,
                                 Controller* cntl,
                                 const google::protobuf::Message* request);

此函数用于将请求序列化到 request_buf 中,该序列化必须由客户端实现。它在 RPC 调用之前发生,且只会被调用一次。某些协议(例如 http)所需的必要信息包含在 cntl 中。成功返回 true,否则返回 false。

pack_request

typedef int (*PackRequest)(butil::IOBuf* msg,
                           uint64_t correlation_id,
                           const google::protobuf::MethodDescriptor* method,
                           Controller* controller,
                           const butil::IOBuf& request_buf,
                           const Authenticator* auth);

此函数用于将 request_buf 打包进 msg,每次向服务器发送消息(包括重试)之前都会调用它。当 auth 不为 NULL 时,还需要打包认证信息。成功返回 0,否则返回 -1。

process_request

typedef void (*ProcessRequest)(InputMessageBase* msg_base);

此函数用于在服务器端解析请求消息,服务器必须实现该函数。它与 parse() 可能在不同的线程中运行,且可能有多个 process_request 同时运行。处理完成后,必须调用 msg_base->Destroy()。为避免忘记调用 Destroy,建议使用 DestroyingPtr<>。

process_response

typedef void (*ProcessResponse)(InputMessageBase* msg);

此函数用于在客户端解析消息响应,必须由客户端自行实现。它与 parse() 可能在不同的线程中运行。多个 process_request 可能同时运行。处理完成后,必须调用 msg_base->Destroy()。为避免忘记调用 Destroy,建议使用 DestroyingPtr<>。

verify

typedef bool (*Verify)(const InputMessageBase* msg);

此函数用于对连接进行身份验证,在收到第一条消息时被调用。需要身份验证的服务端必须实现该函数,否则该函数指针可以为 NULL。成功返回 true,失败返回 false。

parse_server_address

typedef bool (*ParseServerAddress)(butil::EndPoint* out, const char* server_addr_and_port);

此函数将 Channel.Init 的参数 server_addr_and_port 转换为 butil::EndPoint,该转换是可选的。某些协议对服务器地址的表达和理解可能有所不同。

get_method_name

typedef const std::string& (*GetMethodName)(const google::protobuf::MethodDescriptor* method,
                                            const Controller*);

此函数用于自定义方法名称,是可选的。

supported_connection_type

用于标记支持的连接方式。如果支持所有连接方式,此值应设置为 CONNECTION_TYPE_ALL。如果支持连接池和短连接,此值应设置为 CONNECTION_TYPE_POOLED_AND_SHORT。

name

协议的名称,会出现在各种配置与展示中,应尽可能简短,且必须是字符串常量。

注册到全局

应调用 RegisterProtocol 将](https://github.com/brpc/brpc/blob/master/src/brpc/global.cpp)实现的协议注册到 brpc 中,例如:

Protocol http_protocol = { ParseHttpMessage,
                           SerializeHttpRequest, PackHttpRequest,
                           ProcessHttpRequest, ProcessHttpResponse,
                           VerifyHttpRequest, ParseHttpServerAddress,
                           GetHttpMethodName,
                           CONNECTION_TYPE_POOLED_AND_SHORT,
                           "http" };
if (RegisterProtocol(PROTOCOL_HTTP, http_protocol) != 0) {
    exit(1);
}

最后修改于 2023 年 1 月 10 日:移除 incubator(devlive-community/knowforge#122) (7647361c1)]

评论

登录后参与评论

正在加载评论…