平时的工作用到了baidu-rpc搭建rpc服务,作为戈君大神的大作,在没有开源的时候,这个c 的rpc框架在厂内就已经好评颇多,无论是性能、文档、还是代码注释都很优秀,内部使用范围特别广,17年开源,开源版本叫做brpc,开源后不少大厂都有使用,目前已经进入apache孵化器,源码以及文档的地址如下:https://github.com/apache/incubator-brpc。
个人对底层的东西比较感兴趣,因此有结合文档和源码深入学习下这个rpc框架的想法,整个brpc代码量不小,很早就开始看了,实话说一开始看其实挺费劲的,不过越往后看越觉得这个框架的博大精深,所以准备把看过的部分总结成文章,今天整理了一部分先发第一篇,算是看源码的随笔吧,第一次写博客,如有错误,还望指正。
brpc作为一个完整的rpc框架,自然同时支持作为server和作为client,这篇文章聊的是作为server的使用方式以及启动server、开启相应服务的内部处理过程。bprc面向用户的接口还是挺友好的,调用很简单,这里贴一个官方http的demo:
搭建一个rpc server,概括起来就是新建一个server对象,设置好参数,往里面添加自己的service,然后启动,server可以包含多个service,而service又可以包含多个method,具体的某次请求就是针对某个method的,这个demo里的server有三个service。
具体怎么使用bprc这里不多说,官方文档有详细说明。因为是从业务使用开始接触这个框架的,所以我打算首先从和实际应用场景最接近的添加服务、启动服务器这块开始入手看源码。brpc里有个server类,作为服务器的话均是通过这个类作为入口,从rpc服务器的构造过程来说,主要是如下三个过程:
这里所说的service,指的是继承自proto文件描述的service的用户service,brpc是基于protobuf的,protobuf不包含rpc的实现,但却有rpc的相关定义,比如service,brpc就用到了这个,即便是用不到protobuf消息的http服务,服务接口也得定义在proto文件中,是为了确保所有的服务声明集中在proto文件中。
addservice函数有多个重载,比如上面官方demo调用的第三个是基于restful mapping字符串的,如下:
各个不同的addservice重载调用的都是addserviceinternal这个内部具体实现。总的来说addservice就是指定哪些服务去处理哪里来的请求。
参数有三个,protofuf service类型的服务,是否是内部服务(brpc提供了监控之类的内部服务)的标识,以及添加的service的选项。
google::protobuf::service是一个公共的基类,用户定义在proto文件里的service会被预处理成继承自这个基类的一个类,用户需要继承这个类去写真正的业务代码(实现method对应的虚函数),框架层面上统一按父类google::protobuf::service处理。addserviceinternal就是把service里的method拿出来映射到指定地址上,首先会调用initializeonce() 实现server的初始化(只会初始化一次,用pthread_once调用globalinitializeordieimpl,保证只执行一次),如下:
brpc是支持多协议的,而且是同一个端口,会有一个协议解析机制来保证相对高的效率,本身内置了各种协议,用户也可以很方便地自己新增协议支持,globalinitializeordieimpl函数里面,最重要的就是注册了包括http在内的各种各样的协议支持,简单来说就是指定了各协议的消息解析函数、server端用到的request处理函数、client端用到的response处理函数,比如request处理函数,以下图中http协议为例,processhttprequest就负责处理接收到的http request,这个函数里会处理request后交给用户自定义函数去处理。这里暂时不详细展开,后面的文章再详细说明。
brpc提供了丰富的参数设置,包括最大并发,是否开启ssl、关闭闲置连接时间等,具体的可以参考官方文档,这里不再赘述,主要是通过serveroption类型指定参数和直接调用set_xxx来直接设置。
调用start函数来启动服务器,和addservice一样,start也有多种调用方式的重载,最终都是调用的startinternal
在startinternal函数里,首先是一些准备工作,根据option进行了一些设置,包括ssl设置以及是否创建tls数据、提前启动好需要的bthread(brpc用到的m:n线程库的线程,bthread也是bprc性能优异的关键之一,后面的文章再具体介绍)等。然后就是在指定的ip和端口范围(在范围内不断尝试,成功了就停止继续尝试)上启动监听,一个server也只支持监听一个端口。主要代码如下:
acceptor顾名思义就是消息的接收器,buildacceptor也就是构建接收器,如果为null则调用buildacceptor
在buildacceptor里面,最重要的如下:
首先通过listprotocols拿到所有注册支持的协议,然后遍历这些协议,通过addhandler添加,handler是处理message的,注意到这里handler.process都是protocols[i]里面的process_request,也就是对应协议在服务端使用用来处理过接收到的请求的,对应的如果是客户端用的则是process_response。
startaccept核心内容如下:
brpc是采用epoll来处理事件的,用的是边缘触发,options.on_edge_triggered_events是epoll边缘触发事件到来后的处理函数,也就是onnewconnections作为事件的处理函数,顾名思义也就是新连接到来后使用的处理函数,这里不展开讲onnewconnections的具体实现,后面的文章再详细介绍。
关于socket类型,就是对fd等资源的的封装方便再多线程环境下使用,官方介绍是这样的:
和fd相关的数据均在socket中,是rpc最复杂的结构之一,这个结构的独特之处在于用64位的socketid指代socket对象以方便在多线程环境下使用fd。
socket::create函数是根据options新建socket并把id存入第二个参数中,内部最重要的操作就是用options.on_edge_triggered_event所指代的函数进行epoll add,在当前服务端start的场景下,也就是在监听fd上用onnewconnections注册epoll事件处理新过来的连接,至此启动完成,后续等待epoll事件进行相应处理。
负责第一步处理epoll事件的则是eventdispatcher,是分发epoll event的模块,负责把fd上边缘触发的事件分发给消费者(具体的业务处理函数),可以有多个,分别运行在不同的bthread上,具体的数量取决于参数,它所做的事情就是启动后不断去epoll_wait,获得epoll事件后交由相应函数处理,如果是epoll_in事件,调用socket::startinputevent,如果是epoll_out,调用socket::handleepollout,简化后的核心代码如下:
startaccept后,服务器基本就启动完成了。再回到最开始的实例里,最后会调用server.rununtilaskedtoquit()
registerquitsignalordie里主要是用signal函数注册了退出信号,一旦有退出信号s_signal_quit会为true,从而跳出循环并停止server。
brpc作为服务端整个启动过程基本就是这样,后面再写文章继续介绍一下brpc里一些关键的机制和类,以及在fd上收发请求的一些细节。