新协议
新协议
了解如何向 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)]
评论
登录后参与评论
KnowForge