gRPC 源代码阅读笔记
潘忠显 / 2020-05-08
gRPC 基本概念已经独立一篇文章介绍,请移步点击《gRPC 基础概念详解》查看。
本文将会讨论 gRPC 核心概念和实现,期间会穿插介绍一些 C++ 的编程技巧。
ServerBuilder 是否可以多次调用 RegisterService
注册多个服务?
BuildAndStart 可以产生一个新的 Server 对象指针。
多个线程可以共用同一个 CompletionQueue 吗?可以,例子中就是。
greeter_server.cc 也不是同步的 server,比如在SayHello中增加一个:
std::this_thread::sleep_for(std::chrono::seconds(1));
一、目录说明
include/grpcpp/channel.h
include/grpcpp/impl/codegen/channel_interface.h
src/core/lib/surface/channel.h
src/cpp/client/channel_cc.cc
include/grpc/impl/codegen/grpc_types.h
C 代码 上边这行
/// Value is a \a census_context.
GRPC_CONTEXT_TRACING,
/// Value is a CallTracer object.
GRPC_CONTEXT_CALL_TRACER,
二、Channel
(为什么先说 Channel)
因为 greeter_client.cc 的main函数中,最先出现跟 gRPC 相关的就是利用 CreateChannel()
来创建一个Channel 的shared_ptr;另一方面,Channel 是 Client 和 Server最重要的***
include/grpcpp/channel.h
中定义了 Channel:
class Channel final : public ::grpc::ChannelInterface,
public ::grpc::internal::CallHook,
public std::enable_shared_from_this<Channel>,
private ::grpc::GrpcLibraryCodegen {...};
Channel 继承了 4 个类,每个类及其对应的作用为:
::grpc::ChannelInterface
Channel 的接口类,规定了哪些功能纯虚函数需要实现。::grpc::internal::CallHook
用于性能测试 (performing ops) 的 Hook,暂未确认具体功能std::enable_shared_from_this<Channel>
用于可以调用shared_from_this()
从内部创建指向自己的shared_ptr
,涉及 C++ 技巧 【技巧1】::grpc::GrpcLibraryCodegen
生成代码相关。暂未确认具体功能
涉及到其他 C++ 的技巧:
【技巧 2】利用私有构造函数 + 友元(类或函数),来限定只能在部分作用于中创建对象。
【技巧 3】利用 override
限定析构函数,以保证其父类的析构函数均已声明为虚函数。
【技巧 4】将类模板声明为友元,会对利用该模板实例化出来的所有类都有效。(?)
为更清晰的描述核心概念,这里以及后文中都略掉辅助类或函数的介绍。
类 Channel
利用了 grpc_channel
类型的成员变量,继承了类 ChannelInterface
并实现了虚函数或纯虚函数。
核心功能是在 struct grpc_channel
及相关函数实现的。
struct grpc_channel
是个结构,没有成员函数,都是通过 grpc_channel_
开头的一些函数来进行操作的,比如:
grpc_channel_check_connectivity_state()
grpc_channel_get_channel_stack()
grpc_channel_stack_last_element()
为什么 status.h 中 extern “C” 而没有找到对应的符号定义?
impl/codegen
port_platform.h 判断环境,并以此进行一些额外的定义。
atm_gcc_atomic.h 封装类 GCC 编译器的原子性接口 __atomic_*
接口,比如:
#define gpr_atm_acq_load(p) (__atomic_load_n((p), __ATOMIC_ACQUIRE))
比如这里的 __atomic_load_n
只有使用 GCC 才能通过,而这个函数的定义在 GCC 文档中有说明:
Built-in Function: type __atomic_load_n (type *ptr, int memorder)
This built-in function implements an atomic load operation. It returns the contents of
*ptr
.The valid memory order variants are
__ATOMIC_RELAXED
,__ATOMIC_SEQ_CST
,__ATOMIC_ACQUIRE
, and__ATOMIC_CONSUME
.
头文件中使用了 static inline 修饰这些封装的函数,这两个关键字一起用的作用:
https://stackoverflow.com/a/47821267/3801587
??? static受作用域限制,最大是文件作用域,不能被extern
static
修饰独立的函数时的作用
如果两个源文件中定义了两个相同的函数,且均为声明为 static
,在 链接阶段 会报错,因为有两个相同的符号冲突了。
报错类似于:
> g++ t.cc m.cc
/tmp/ccuQpoO7.o: In function `func()':
m.cc:(.text+0x0): multiple definition of `func()'
/tmp/cc5uQjXf.o:t.cc:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
我们先看 static 修饰一个源文件中函数的带来的影响:
// n.cc
int unstatic_func() { return 0; }
static int static_func() { return 0; }
编译后通过 objdump
指令来观察其中的符号:
> g++ -c n.cc
> objdump -t n.o | grep fun
000000000000000b l F .text 000000000000000b _ZL11static_funcv
0000000000000054 l F .text 0000000000000015 _GLOBAL__sub_I__Z13unstatic_funcv
0000000000000000 g F .text 000000000000000b _Z13unstatic_funcv
可以看到 static_func()
是一个本地符号,而 unstatic_func()
是个全局符号,符号均位于代码区 text (code) section。第二列字符的含义可以参考 objdump 命令的说明,其中提到:
“l” “g” “u” “!” The symbol is a local (l), global (g), unique global (u), neither global nor local (a space) or both global and local (!). A symbol can be neither local or global for a variety of reasons, e.g., because it is used for debugging, but it is probably an indication of a bug if it is ever both local and global. Unique global symbols are a GNU extension to the standard set of ELF symbol bindings. For such a symbol the dynamic linker will make sure that in the entire process there is just one symbol with this name and type in use.
如果 .o 文件中出现了两个相同的全局符号,就会造成以上重复定义的错误。《Symbol Processing》文档详细的介绍了链接器处理符号的过程,以及中间遇到的问题。
https://docs.oracle.com/cd/E26505_01/html/E26506/chapter2-90421.html#gluek
可重定位对象
弱符号
强符号
It is declared static
to avoid multiple definitions (at link time) in case the compiler did not inline it (e.g. when you use its address).
为什么有些使用宏定义,有些使用静态内联函数?
atm_gcc_sync.h
atm_windows.h
作用与 atm_gcc_atomic.h 相同,只是在不同环境中的封装。
atm.h
封装了 gpr_atm_no_barrier_clamped_add
函数,作用是
Adds delta to value, clamping the result to the range specified by min and max. Returns the new value.
sync_generic.h
同步相关的泛型,平台无关(平台相关内容被 atm.h 封装)。定义了 gpr_event
、gpr_refcount
、gpr_stats_counter
等结构。
sync.h -> 根据平台选择一个对应的 sync_*.h 文件,“Synchronization primitives for GPR”,GPR 的同步原语(类型):gpr_mu
、gpr_cv
、gpr_once
、gpr_event
、gpr_refcount
、gpr_stats_counter
status.h 定义了 gRPC 状态码的枚举值
grpc_types.h 定义或暴露了 gRPC 中低层使用的数据结构。分成几类:
- 就在该头文件中定义的,比如:
grpc_byte_buffer
、grpc_arg_pointer_vtable
、grpc_metadata
、grpc_event
等。 - 不透明的类型,比如
grpc_completion_queue
是定义在 completion_queue.cc 中的,外边可以通过 API 进行创建和操作,所以这里需要将类型暴露出来。类似的还有grpc_call
。 - 透明的类型,比如
grpc_channel
是定义在 “src/core/lib/surface/channel.h” 的头文件中的,外部是知道该结构的定义的。类似的还有:grpc_server
、grpc_socket_factory
。
gpr_types.h 定义了 GPR 使用的时钟和时间
byte_buffer.h 对 grpc_slice
进行操作的若干函数,没有结构体封装;前面提到了,grpc_byte_buffer
结构是定义在 grpc_types.h 头文件中的。
byte_buffer_reader.h 定义了 struct grpc_byte_buffer_reader
。
这两个文件有着紧密的联系,在 byte_buffer.h
中声明的 grpc_byte_buffer_reader_init
实现是在 byte_buffer_reader.cc
中。
拆成两个文件的作用是什么?使用者(gRPC Core 的开发者)需要自己去 #include
两个头文件?
/** Reader for byte buffers. Iterates over slices in the byte buffer */
struct grpc_byte_buffer_reader;
typedef struct grpc_byte_buffer_reader grpc_byte_buffer_reader;
自己 #include
的头文件中没有该结构体的定义。为什么不自己 #include
而是要使用者自己去 #include
?
阅读代码过程中,发现有符号没有定义,提 Pull Request 删除掉。
struct grpc_byte_buffer_reader {
struct grpc_byte_buffer* buffer_in;
struct grpc_byte_buffer* buffer_out;
/** Different current objects correspond to different types of byte buffers */
union grpc_byte_buffer_reader_current {
/** Index into a slice buffer's array of slices */
unsigned index;
} current;
};
为什么只有一个变量,还要使用 Union?为了更清晰?
使用过程:
int grpc_byte_buffer_reader_init(grpc_byte_buffer_reader* reader,
grpc_byte_buffer* buffer) {
reader->buffer_in = buffer;
switch (reader->buffer_in->type) {
case GRPC_BB_RAW:
reader->buffer_out = reader->buffer_in;
reader->current.index = 0;
break;
}
return 1;
}
compression_types.h 压缩的算法、等级、选项(算法+选项)
connectivity_state.h 连接的状态,在 《》 中有介绍。
propagation_bits.h propagation 相关的比特位开关,可以或操作, 默认是4个比特都打开。
log.h 定义了 gpr_log
、gpr_log_func
和 gpr_log_func_args
。
log.cc 中有一个 g_log_func
的静态变量,可以通过 log.h 中的 gpr_set_log_function
将日志函数设置进去。
另外,在 log.h 中定义了宏 GPR_ASSERT
,因为 GPR_ASSERT 的同时,会调用 gpr_log 打印日志。
fork.h
support 目录下是干嘛用的?
grpc 下边的头文件不全是 C 标准的头文件,比如 include/grpc/event_engine/slice_allocator.h 中有一个 SliceBuffer 的类,可能不合规范,因为注释写道:
stubbed out here, to be replaced with a real version in next PR.
src/core 是已经改成 C++ 写的了
gRPC 中 channelz 的含义
https://grpc.io/blog/a-short-introduction-to-channelz/
https://github.com/grpc/proposal/blob/master/A14-channelz.md
https://github.com/grpc/grpc/issues/15340
grpc_context
struct grpc_call_context_element {
void* value = nullptr;
void (*destroy)(void*) = nullptr;
};
/* Contexts for various subsystems (security, tracing, ...). */
grpc_call_context_element context[GRPC_CONTEXT_COUNT] = {};
以 census 为例,看 gRPC filter 的实现
grpc::CensusContext
是利用 opencensus 的 Span 和 TagMap 封装的是上下文。这个类的使用方式跟 Span 的创建方式很类似。
- 没有参数的构造函数,会对应产生一个空 Span
- 如果带父 Span 的参数,会产生一个对应的子 Span
grpc::OpenCensusCallTracer
又对 grpc::CensusContext
做了封装(中间还通过一个内部的类)
grpc::OpenCensusCallTracer::OpenCensusCallAttemptTracer
grpc_core::CallTracer
grpc::OpenCensusCallTracer
grpc.h 中有 声明(弱符号)
struct census_context;
但是这个符号是在哪里定义的呢?在整个项目中没有被定义。
所有 census_text 的操作,都是通过指针来进行的:
census_context* grpc_census_call_get_context(grpc_call* call);
void grpc_census_call_set_context(grpc_call* call, census_context* context);
且上述函数的都是按照 C 代码来进行编译的,这产生的结果是在编译的结果中的符号就是grpc_census_call_get_context
> objdump -t greeter_server| grep census
000000000046462a g F .text 0000000000000064 grpc_census_call_get_context
00000000004645aa g F .text 0000000000000080 grpc_census_call_set_context
src/cpp/ext/filters/census/grpc_plugin.h
比如编译下边的单对文件
// t.h
#ifdef __cplusplus
extern "C" {
#endif
struct T;
T* func(T* t);
#ifdef __cplusplus
}
#endif
// t.cc
#include "t.h"
T* func(T* t) { return t; };
int main() {
T* t = nullptr;
func(t);
return 0;
}
结构 T
没有定义也能够成功并正常运行。
const struct census_context* ServerContextBase::census_context() const {
return call_.call == nullptr ? nullptr
: grpc_census_call_get_context(call_.call);
}
::opencensus::trace::Span GetSpanFromServerContext(
grpc::ServerContext* context) {
if (context == nullptr) return opencensus::trace::Span::BlankSpan();
return reinterpret_cast<const grpc::CensusContext*>(context->census_context())
->Span();
}
grpc_trace
struct FilterRecord {
grpc_channel_stack_type stack_type;
int priority;
std::function<bool(const grpc_channel_args&)> include_filter;
grpc_channel_filter filter;
};
通过 docker 执行所有单元测试
https://github.com/panzhongxian/grpc/tree/master/tools/run_tests#unit-tests-run_testspy
slice.h grcp_slice 的定义和 API。
#ifndef GPRAPI
#define GPRAPI
#endif
std::vector<FilterRecord>* channel_filters;
FilterType::InitCallElement
internal::FilterRecord filter_record = {
stack_type,
priority,
include_filter,
{FilterType::StartTransportStreamOpBatch, FilterType::StartTransportOp,
FilterType::call_data_size, FilterType::InitCallElement,
FilterType::SetPollsetOrPollsetSet, FilterType::DestroyCallElement,
FilterType::channel_data_size, FilterType::InitChannelElement,
FilterType::DestroyChannelElement, FilterType::GetChannelInfo, name}};
internal::channel_filters->push_back(filter_record);
}
static grpc_error_handle InitCallElement(grpc_call_element* elem,
const grpc_call_element_args* args) {
// Construct the object in the already-allocated memory.
CallDataType* call_data = new (elem->call_data) CallDataType();
std::cout << "InitCallElement()" << std::endl;
return call_data->Init(elem, args);
}
/// Value is a \a census_context.
GRPC_CONTEXT_TRACING,
/// Value is a CallTracer object.
GRPC_CONTEXT_CALL_TRACER,
protoc 是怎么来的 protoc 的插件是怎么来的
什么是 memory barrier?
空定义 GPRAPI
的作用是什么?
以用户不会直接用到的 Slice 为例,说明 gRPC 各个目录之间的关系。
include/grpc/impl/codegen/slice.h
声明并定义了结构grpc_slice
相关类型,不会被 C++ 文件#include
include/grpc/slice.h
声明并定义了grpc_slice_new()
等GPRAPI
函数,会被 C++ 文件#include
include/grpcpp/impl/codegen/slice.h
定义了Slice
类,是对结构grpc_slice
及其操作函数的相关封装include/grpcpp/impl/codegen/gpr_slice.h
兼容历史的版本,目前没有用也不应该被使用
README.md 文档中也有说明,最后 slice.h
文件放在 include/grpcpp/impl/codegen/
目录下而不是放在 include/grpcpp
的原因是:
protoc
生成的文件,可以只依赖部分文件,而不是整个 gRPC 项目,可以减少时间- 用户正常使用 gRPC 库不会直接使用到这些文件
实际上,gRPC 库也基本上没有直接使用 Slice,而是通过 ByteBuffer
类又进行了一次封装,文件位置在:include/grpcpp/impl/codegen/byte_buffer.h
用户**会直接用到的 Status **与 Slice 的文件组织并没有太多的不同。其实,直接 #include <grpcpp/grpcpp.h>
之后,两种结构都可以直接使用:
#include <grpcpp/grpcpp.h>
using grpc::Slice;
using grpc::Status;
int main() {
Status status;
Slice slice;
return 0;
}
什么情况下会使用到 grpc/support
目录下边的文件?
GPRAPI
修饰函数的作用是什么?
不再管 include/grpc++
目录下的文件了.
Channel = Connection + Registered Method
grpc_core::channelz::ChannelNode
grpc::Channel
是了封装,
1. State
Channel 是有状态的,在 connectivity_state.h 中有定义 grpc_connectivity_state
,[TODO]gRPC 的 enum 命名风格。
省去前缀 GRPC_CHANNEL_
,5 种类型分别是:IDLE、CONNECTING、READY、TRANSIENT_FAILURE、SHUTDOWN,(这五种关系其实是描述的连接的状态?)
有点奇怪,为什么没有成员变量来维护这个状态呢?
每次获取状态,都会通过 grpc_channel_check_connectivity_state
来检查一下链接状态,在再Channel 或者 ChannelInterface 中则没有对应的成员变量来维护这个状态。
获取状态的逻辑
GetState
grpc_channel_check_connectivity_state
grpc_core::ClientChannel::GetFromChannel grpc_channel* -> ClientChannel*
grpc_channel_get_channel_stack grpc_channel* -> grpc_channel_stack*
grpc_channel_stack_last_element grpc_channel_stack* -> grpc_channel_element*
grpc_core::ClientChannel::CheckConnectivityState
从成员变量 state_tracker_ 中获取状态,如果空闲则尝试连接
grpc_core::ClientChannel::TryToConnectLocked // 虽然TryToConnect,但返回的状态仍未之前的状态
lb_policy_ resolver_
grpc_stream_unref
ClientChannel 的几个
work_serializer_
lb_policy_
resolver_
owning_stack_
ConnectivityStateTracker 维护连接状态和 watchers_ (map)
只有ClientChannel 没有 ServerChannel
不是一个channel 而是一个channel的stack?
grpc_channel* const c_channel_; // owned
virtual grpc_connectivity_state GetState(bool try_to_connect) = 0;
virtual internal::Call CreateCall(const internal::RpcMethod& method,
::grpc::ClientContext* context,
::grpc::CompletionQueue* cq) = 0;
virtual void PerformOpsOnCall(internal::CallOpSetInterface* ops,
internal::Call* call) = 0;
virtual void* RegisterMethod(const char* method) = 0;
virtual void NotifyOnStateChangeImpl(grpc_connectivity_state last_observed,
gpr_timespec deadline,
::grpc::CompletionQueue* cq,
void* tag) = 0;
virtual bool WaitForStateChangeImpl(grpc_connectivity_state last_observed,
gpr_timespec deadline) = 0;
virtual ::grpc::CompletionQueue* CallbackCQ() { return nullptr; }
grpc_channel_create_registered_call
grpc_timespec_to_millis_round_up
grpc_channel_create_call_internal
grpc_call_create
codegen
/// Codegen interface for \a grpc::Channel.
namespace internal
Greeter::NewStub(channel)
include/grpcpp/impl/codegen/channel_interface.h
include/grpcpp/channel.h
class Channel final : public ::grpc::ChannelInterface,
public ::grpc::internal::CallHook,
public std::enable_shared_from_this<Channel>,
private ::grpc::GrpcLibraryCodegen {
template <class R>
friend class ::grpc::ClientReader;
意味着 ClientReader 类模板产生的类 都是Channel 的友类?
template <class R>
class ClientReader;
实现三个接口。
intercepted_channel.h
An InterceptedChannel is available to client Interceptors.
class ClientReaderWriterInterface : public internal::ClientStreamingInterface,
public internal::WriterInterface<W>,
public internal::ReaderInterface<R> {
CreateCallInternal
和 CallbackCQ
class CallHook
: This is an interface that Channel and Server implement to allow them to hook performing ops.
“performing ops”
Classes that require gRPC to be initialized should inherit from this class.
class GrpcLibraryCodegen
channel_factory
构造函数是 private
+ 包含友元类/友元函数 =》 只有在友类中才可以创建对象。
ChannelArguments
以下内容为待整理内容…
terminology
IDL
EOS (end-of-stream)
Payload message
Synchronous
Asynchronous
Mandatory
GPR - Google Portable Runtime for C
gRFC - gRPC 的 RFC,即 https://github.com/grpc/proposal 中的提案
grpc::ServerBuilder::HostString
Experimental, to be deprecated.
- 一个线程对应一个 Completion Queue
- 先关闭 Server,再关闭CQ
- 关闭 CQ 之后,需要清空 CQ
Best performance is typically obtained by using one thread per polling completion queue.
Caller is required to shutdown the server prior to shutting down the returned completion queue
SyncServerOption 用于设置同步 server 的不同的属性
grpc_transport 是个什么概念:
由 grpc_transport_vtable 定义操作
grpc_transport <= grpc_create_cronet_transport(engine, target, new_args, reserved);
grpc_channel <= grpc_channel_create_internal(target, new_args, GRPC_CLIENT_DIRECT_CHANNEL, ct, nullptr);
引擎
000
typedef struct grpc_transport_vtable {
/* implementation of grpc_transport_init_stream */
int (*init_stream)(grpc_transport* self, grpc_stream* stream,
grpc_stream_refcount* refcount, const void* server_data,
grpc_core::Arena* arena);
grpc::InsecureChannelCredentials() –> ChannelCredentials
grpc_transport
grpc_transport_vtable 只有两个普通变量,一个是stream的size,另外一个是名称,其他的全是 需要的函数指针。
GrpcLibraryCodegen
ChannelCredentials
InsecureChannelCredential -> std::shared_ptr
gpr_once_init 为了初始化什么?
std::atomic
template <typename Callable, typename... Args>
void call_once(absl::once_flag& flag, Callable&& fn, Args&&... args) {
std::atomic<uint32_t>* once = base_internal::ControlWord(&flag);
uint32_t s = once->load(std::memory_order_acquire);
if (ABSL_PREDICT_FALSE(s != base_internal::kOnceDone)) {
base_internal::CallOnceImpl(
once, base_internal::SCHEDULE_COOPERATIVE_AND_KERNEL,
std::forward<Callable>(fn), std::forward<Args>(args)...);
}
}
channel_factory
call_once
gpr_mu_init 全局变量控制 gpr_mu_init 只调用一次,创建一个全局的锁
grpc_channel_credentials
ArenaPromise
不同的鉴权方式参考:grpc_security.h
std::shared_ptr<Channel> channel = grpc::CreateChannelInternal(
"", grpc_channel_create(target.c_str(), creds, &channel_args),
std::move(interceptor_creators));
grpc_channel* grpc_channel_create 干了什么
void gpr_mu_init(gpr_mu* mu) {
static_assert(sizeof(gpr_mu) == sizeof(absl::Mutex),
"gpr_mu and Mutex must be the same size");
new (mu) absl::Mutex;
}
grpc_shutdown()
有两套同步的代码,主要是根据 下边三个标签来判断:
#if defined(GPR_POSIX_SYNC) && !defined(GPR_ABSEIL_SYNC) && !defined(GPR_CUSTOM_SYNC)
好多地方会调用 grpc_init()
,但是实际上内部分成两部分:
- 通过
gpr_once_init()
执行一次do_basic_init
,其中包括初始化锁和关闭信号变量(cv, condition variable)的创建构造、日志、注册插件管理器、全局cq、时钟等 - 通过上一步创建出来的锁,来执行另外的一些初始化操作,包括:进程处理、统计初始化、channelz注册处、应用回调、IO管理器、定时器、插件、调用跟踪等,启动 IO 管理器
有 gpr_once_init()
,如果已经初始化过之后,就不会在进入了。
直接判断 ++g_initializations == 1 会不会有益处的风险。如果 grpc_shutdown 之后,会 –g_initializations。
通过 gpr_once_init 来
使用 absl::MutexLock 在作用域内都加锁,出了作用域释放:
The class uses RAII to acquire the mutex and automatically releases it when the class goes out of scope.
GRPC Core API & C++ API
GRPC Core通过底层API实现素有gRPC的核心功能的library,以便被其他库封装,API在 include/grpc.h 文件中,文档在这。GRPC C++ 文档入口在这,安全相关的在grpc_security.h
提议L6-core-allow-cpp.md中有描述。**最初的GRPC Core是用C89写的,后来改用C99,再后来又改用C++。**从代码中,我们也能看到一些蛛丝马迹:
- grpc.h虽然是C++的头文件(比如有
extern "C"
),但函数命名不符合谷歌 C++ 代码规范 - Core API在grpc_types.h中定义的结构体,都是struct
- Core API的实现是面向过程的
- 找一个一直有更新的文件,比如channel_create.cc,Blame看一下修改
以grpc::CreateChannel为例,说明Core和C++库的关系
很多客户端示例代码中有这样一个创建Channel的代码:
grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());
我们以此为例,来说明一下Core和C++的关系,顺便看一下文件组织的规则。
-
grpc::CreateChannel函数定义在
include/grpcpp/create_channel.h
文件中,static inline。 -
调用::grpc_impl::CreateChannelImpl返回产生的Channel指针,该函数声明于
include/grpcpp/create_channel_impl.h
中,实现在src/cpp/client/create_channel.cc
中。- 调用::grpc_impl::CreateCustomChannelImpl,该函数的声明和实现位置同上
- 如果传入的creds无效(为空),则创建一个lame_client_channel,就是一个不能用的Channel
- 如果creds非空,则调用其成员函数
CreateChannelImpl
- 调用::grpc_impl::CreateCustomChannelImpl,该函数的声明和实现位置同上
再以最初传入的grpc::InsecureChannelCredentials
为例,看看接下来的调用过程。
-
grpc::InsecureChannelCredentials()
返回一个ChannelCredentials
的共享指针,声明于credentials_impl.h
中,实现的实际定义是在src/cpp/client/insecure_credentials.cc
中,new了一个InsecureChannelCredentioalsImpl
对象,返回其共享指针。 -
InsecureChannelCredentialsImpl 类定义在相同cc文件的 unnamed/anonymous namespace 中。该类(当然地)继承自
ChannelCredentials
类,需要实现后者的所有纯虚函数,包括CreateChannelImpl
。 -
调用
InsecureChannelCredentialsImpl::CreateChannelImpl()
- 调用
CreateChannelWithInterceptors()
- 调用
::grpc::CreateChannelInternal
+grpc_insecure_channel_create()
- 调用
- 调用
这里的grpc_insecure_channel_create
就是gRPC core中的C API。再看回insecure_credentials.cc 文件的include:
#include <grpc/grpc.h>
这就是gRPC C++ 和 Core的关联。grpc.h中也有 extern “C” 以便 C 中调用 C++ 函数。
基类
ServerContextBase
派生类
ServerContext
CallbackServerContext
class CallbackServerContext : public ServerContextBase {
public:
/// Public constructors are for direct use only by mocking tests. In practice,
/// these objects will be owned by the library.
CallbackServerContext() {}
// ...
using ServerContextBase::context_allocator;
using ServerContextBase::set_context_allocator;
// ...
// CallbackServerContext only
using ServerContextBase::DefaultReactor;
private:
// Sync/CQ-based Async ServerContext only
using ServerContextBase::AsyncNotifyWhenDone;
/// Prevent copying.
CallbackServerContext(const CallbackServerContext&) = delete;
CallbackServerContext& operator=(const CallbackServerContext&) = delete;
};
grpc_slice
A grpc_slice s, if initialized, represents the byte range s.bytes[0..s.length-1].
两种情况:
- 有引用计数的 使用
grpc_slice_new()
创建 - 无引用计数的 inlined 使用
grp_slice_unref()
创建
TODO: 如何实现的grpc_slice
grpc library
Operations:
- init
- shutdown
- register_plugin
grpc_call
A Call represents an RPC. When created, it is in a configuration state allowing properties to be set until it is invoked. After invoke, the Call can have messages written to it and read from it.
创建:
- grpc_channel_create_call
- grpc_channel_create_registered_call
TODO: grpc_call不用显式销毁吗?
其他操作:
- ref / unref
- cancel / cancel_with_status
grpc_channel
A gRPC channel provides a connection to a gRPC server on a specified host and port. It is used when creating a client stub. Clients can specify channel arguments to modify gRPC’s default behaviour, such as switching message compression on or off. A channel has state, including connected and idle.
一个 Channel 对应一个连接。真实情况是这样吗?
channel 有状态:
- connected
- idle
创建与销毁:
- insecure_channel_create
- channel_destroyed
连接相关:
- check_connectivity_state
- ping
- watch_connectivity_state
- support_connective_watcher
TODO: What is a channel watcher? How to add a channel watcher?
Call 相关:
- create_call
- register_call
- create_registered_call
lame channel (one on which all operations fail)
grpc_call_details
一个 RPC 的详细信息,包括method、host、deadline、flags
grpc_call_error
grpc_completion_queue
其他
cencus-context
gRPC Channel
疑问:什么时候会用到server side channel?
疑问:stack对于channel是一个什么概念
疑问:filter 初始化完之后消失掉了,以后如何使用?
疑问:什么时候建立连接?
grpc_insecure_channel_create
- grpc_once_init
- arg = ClientChannelFactory::CreateChannelArg(g_factory)
- 从arg删除传入参数args中的key -> new_args
- CreateChannel(target, new_args) 同文件匿名命名空间里
- 销毁new_args
channel_create.cc中的grpc_core::CreateChannel
- 处理target字段,并清理args中可能存在的
GRPC_ARG_SERVER_URI
字段 - 调用
grpc_channel_create
channel.cc中的grpc_channel_create
- grpc初始化,函数内部保证只会有一次生效
- 构建channel参数
- 创建
grpc_channel_stack_builder
- 通过builder设置入channel的参数,并销毁掉参数(释放指针内容?)
- 设置builder的其他一些信息
- 创建stack
- 使用builder创建channel
- 以上两步如果又失败,会调用
grpc_shutdown
,返回空指针;否则返回创建的channel指针
builder起到了传递参数的作用,也有其他的(什么)功能。
grpc_channel_create
grpc_channel_create_with_builder
grpc_channel_stack_builder_finish
grpc_channel_stack_builder_finish
的过程:
- 创建filters,先计数然后算大小分配
- 根据创建的 filters 和 fitler数量计算并分配channel_stack的大小(加上预留前缀字节)
- 初始化channel_stack
- 依次初始化每个filter
- 销毁builder
- 释放filters指针内容
init_channel_elem
grpc_error* error = elems[i].filter->init_channel_elem(&elems[i], &args);
一个Channel有一个Stack,有多个Filter。
创建Channel的时候,会对每个Filter进行初始化,会用到Stack的指针。
grpc_arg arg = grpc_core::ClientChannelFactory::CreateChannelArg(g_factory);
连接就是在工厂类的CreateSubChannel建立起来的。
class Chttp2InsecureClientChannelFactory : public ClientChannelFactory
Filter
Interpretor
Credentials
Stats
gRPC中有使用到内部的统计
if (channel_stack_type == GRPC_SERVER_CHANNEL) {
GRPC_STATS_INC_SERVER_CHANNELS_CREATED();
}
使用 grpc_stats_per_cpu_storage 数组来存储。
grpc_stats_data[CPU_index]
每个CPU对应的grpc_stats_data中又分为计数器和直方图,根据对计数器分类和直方图桶个数的枚举,预先创建好数组。
通过使用宏对该变量进行操作,计数或统计分布。
在哪里汇总和展示?
mutator
https://grpc.github.io/grpc/core/pages.html
疑问与解释
名词解释
实现技巧
C++
类可以delete this。
如果能在一定会调用一次的成员函数中:
delete this
,那么只需要new
这个对象,而不用使用智能指针管理或者显式的delete
new SelfClass
,那么能链式的一直创建下去
只是为了说明的空 define
#define GPRAPI
#define GRPCAPI GPRAPI
#define CENSUSAPI GRPCAPI
同时定义两个头文件的作用
为了屏蔽大部分用户不会用到的细节,grpcpp大部分头文件都是这样组织的:
#ifndef GRPCPP_ALARM_H
#define GRPCPP_ALARM_H
#include <grpcpp/alarm_impl.h>
namespace grpc {
typedef ::grpc_impl::Alarm Alarm;
}
#endif // GRPCPP_ALARM_H
实际上实现Alarm的详细声明仍然在文件中,只是使用另外的命名空间,存放在单独的一个文件中。
std::enable_shared_from_this
std::enable_shared_from_this
能让其一个对象(假设其名为 t ,且已被一个 std::shared_ptr 对象 pt 管理)安全地生成其他额外的 std::shared_ptr 实例(假设名为 pt1, pt2, … ) ,它们与 pt 共享对象 t 的所有权。
若一个类 T 继承 std::enable_shared_from_this<T>
,则会为该类 T 提供成员函数: shared_from_this 。 当 T 类型对象 t 被一个为名为 pt 的 std::shared_ptr<T>
类对象管理时,调用 T::shared_from_this 成员函数,将会返回一个新的 std::shared_ptr<T>
对象,它与 pt 共享 t 的所有权。
Internal、Impl 后缀的作用和关系
Extern “C”
#ifndef GRPC_GRPC_H
#define GRPC_GRPC_H
#include ...
#ifdef __cplusplus
extern "C" {
#endif
模板类的实例化
private:
template <class InputMessage, class OutputMessage>
friend class ::grpc::internal::BlockingUnaryCallImpl;
CreateCall
是Channel的private函数,因此文档中也没有说明。
代码构造
include/create_channel.h | include/create_channel_impl.h cpp/client/create_channel.cc |
include/create_channel_impl.h cpp/client/create_channel.cc |
---|---|---|
grpc::CreateChannel -call-> |
grpc_impl::CreateChannelImpl -call-> |
grpc_impl::CreateCustomChannelImpl |
grpc_impl::CreateChannelImpl
grpc_impl::CreateCustomChannelImpl
grpc_impl::ChannelCredentials::CreateChannelImpl = 0 or grpc::CreateChannelInternal (lame)
以InsecureChannelCredentials
为例,看看是如何实现CreateChannelImpl
的:
grpc::InsecureChannelCredentials()
返回一个ChannelCredentials的共享指针。实现的声明是在credentials_impl.h
文件中,实现的实际定义是在src/cpp/client/insecure_credentials.cc
中,new了一个InsecureChannelCredentioalsImpl
对象,返回其共享指针。
InsecureChannelCredentialsImpl 类定义在相同cc文件的 unnamed/anonymous namespace 中。该类(当然地)继承自ChannelCredentials
类,需要实现后者的所有纯虚函数,包括CreateChannelImpl
。
InsecureChannelCredentialsImpl::CreateChannelImpl()
–>CreateChannelWithInterceptors()
–> ::grpc::CreateChannelInternal
+ grpc_insecure_channel_create()
这里的grpc_insecure_channel_create
就是gRPC core中的C API。
Init函数
应该内部好正只能初始化一次,而不是通过外部判断。
初始化过程中应该加锁、
工厂类
ClientChannelFactory
3/5/0 rule
- a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three.
- a user-defined destructor, copy-constructor, or copy-assignment operator prevents implicit definition of the move constructor and the move assignment operator, any class for which move semantics are desirable, has to declare all five special member functions
- Other classes should not have custom destructors, copy/move constructors or copy/move assignment operators.
TODO:
InterceptedChannel
https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md
Appendix
Words
concrete
agnostic
a.k.a. also known as.
grainted
eager
infrastructure
retrieve
invoke
advent
coarse-grained
pitfalls of distributed objects
the fallacies of ignoring the network
viable
facilitate
impede
traversal
facet
revision
lameduck
consensus 一致性
安全传输层协议(TLS)用于在两个通信应用程序之间提供保密性和数据完整性。
该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。
传输层安全性协议(英语:Transport Layer Security,缩写作TLS),及其前身安全套接层(Secure Sockets Layer,缩写作SSL)是一种安全协议,目的是为互联网通信提供安全及数据完整性保障。网景公司(Netscape)在1994年推出首版网页浏览器,网景导航者时,推出HTTPS协议,以SSL进行加密,这是SSL的起源。IETF将SSL进行标准化,1999年公布第一版TLS标准文件。随后又公布RFC 5246 (2008年8月)与RFC 6176(2011年3月)。在浏览器、邮箱、即时通信、VoIP、网络传真等应用程序中,广泛支持这个协议。主要的网站,如Google、Facebook等也以这个协议来创建安全连线,发送数据。目前已成为互联网上保密通信的工业标准。
gRPC
三种方式 同步、基于 CQ 的异步、Callback方式
一个宏定义判断,该宏定义默认是定义上去,所以都走的 #ifdef GRPC_CALLBACK_API_NONEXPERIMENTAL
的分支
GRPC_CALLBACK_API_NONEXPERIMENTAL
生成的代码和 gRPC 库的联系
grpc
├── experimental
├── internal
├── protobuf
│ ├── io
│ └── util
└── testing
grpc_event_engine
└── experimental
override
说明符确保该函数为虚函数并覆盖某个基类中的虚函数
explicit
的构造函数不能用于隐式转换和复制初始化
non-static data member initializers
成员变量初始化的几种方式
- 构造函数成员函数初始化列表
S() : n(7) {}
Greeter 中几个类:
- StubInterface
- Stub
技巧
限制类模板形参必须为某种类
template <class BaseClass>
class WithAsyncMethod_SayHello : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
~WithAsyncMethod_SayHello() override {
BaseClassMustBeDerivedFromService(this);
}
}
ChannelCredentials
所有使用 gRPC 的类都需要继承这个类,构造函数中会初始化一次 gRPC 的 g_glip。
/// Classes that require gRPC to be initialized should inherit from this class.
GrpcLibraryCodegen
多线程的情况会如何处理呢?会不会被初始化多次?
g_glip 指向的内容是
两个全局变量
grpc::CoreCodegenInterface* grpc::g_core_codegen_interface;
grpc::GrpcLibraryInterface* grpc::g_glip;
class ChannelCredentials : private grpc::GrpcLibraryCodegen
继承 ChannelCredentials 需要实现纯虚函数 CreateChannelImpl
调用 CreateChannelWithInterceptors 创建的 Channel
将 ChannelArguments 类转成 grpc_channel_args
调用 CreateChannelInternal 创建,但是其中使用 grpc_insecure_channel_create(target.c_str(), &channel_args, nullptr) 创建了一个grpc_channel
异步的进行:
- resolve target
- connect to it (trying alternatives as presented)
- perform handshakes
new_args
grpc_channel_args 和 grpc_arg 的关系是什么?
创建之后,args会被主动的销毁
因为 interceptor_creators 用的是形参,所以直接使用了move不会对外边造成影响。
grpc_channel* grpc_insecure_channel_create
还会调用 grpc_core::CreateChannel 是个内部函数(gcore 之内的匿名命名空间,位于同一个文件中)
grpc_channel_arg_string_create
最底层调用: grpc_channel_create,位于 src/core/lib/surface/channel.cc 中,其逻辑是:
- 创建grpc channel 栈 构建器
- grpc_channel_args_get_client_channel_creation_mutator 创建参数
- grpc_channel_stack_ 开头的一堆参数设置,是设置到builder中?
- 对于Client的channel来说,还需要创建CreateChannelzNode
- 最终调用
grpc_channel_create_with_builder
来创建channel
grpc_channel_create_with_builder 中逻辑:
- 设置不同的args
- 通过 grpc_channel_stack_builder_finish 创建 channel 对象
- 设置属性
- 设置压缩选项
channel的 registration_table 是干嘛用的?
gpr_atm_no_barrier_store 是干嘛用的。空间分配限制。
builder中维护着一堆filter
grpc_channel
grpc_channel_stack_builder_finish
filter 的概念
- 计算出stack的大小
- 使用 gpr_zalloc 分配空间
- 调用 grpc_channel_stack_init 去初始化 channel_stack
grpc_channel_element
call 由两部分组成:grpc_call_stack + grpc_call_element(s) + 对应不同filter的 call_data
grpc ref
channel_stack 有说明
#include "src/cpp/client/create_channel_internal.h"
Channel 类是对 grpc_channel 的一个封装,创建不是在Channel中进行的。而是在CreateChannel 中调用 CreateChannelImpl 进行,本质上是调用 grpc_insecure_channel_create。
调用的 grpc_core::CreateChannel 只是内部的一个函数,外部也不能用。
实际上是在include/grpc/grpc.h 中定义了。
src/core/ext/transport/chttp2/client/insecure/channel_create.cc
grpc_channel_args_copy_and_add_and_remove
grpc_channel_create
grpc_channel_stack_builder_create
CreateChannelzNode
grpc_channel_create_with_builder
先忽略channelz相关的内容
channel_stack channel_stack_builder
channelz_node
创建一个 grpc_call 需要的内容,都要在 grpc_call_create_args,包括 XXXX
创建 call 以及 所需要的参数
关联一个 call 和一个CompletionQueue
grpc_call_internal_ref
从 call中获得参数,比如arena、call_stack、context键、是否为 client类型的call
对 call 进行参数设置,比如context
grpc_call 的结构是在 call.cc 中定义的,所以无法直接创建,只能通过 grpc_call_create() 来创建,其成员也无法通过指针直接访问,只能通过对外提供的函数来操作。
缩写含义
mdelem Metadata Element
pollent Polling Entity