前言
SEASTAR的网络IO部分设计的比较灵活,用户可以选择使用内核协议栈,也可以选择基于Intel DPDK实现的用户态协议栈。
本文试图寻找一种方式,简单而明了地解析SEASTAR如何使用linux内核协议栈提供TCP协议网络接口。故本文只分析SEASTAR 的基于内核协议栈的TCP协议接口,用户太协议栈暂时放一放。
我们知道,在linux 系统下高性能网络编程是绕不开EPOLL机制的。SEASTAR 使用EPOLL机制实现了全异步的网络接口。
如何使用?
服务器端使用方式
使用SEASTAR 框架编写网络程序比较简单,在server 端有关网络部分代码样例:
auto listener = engine().listen(make_ipv4_address({_port}), ...);
keep_doing([listener] {
return listener->accept().then([] (connected_socket fd, socket_address addr) mutable {
auto conn = make_connection(std::move(fd), addr, ...);
do_until([conn] { return conn->_in.eof(); }, [conn] {
return conn->process().then([conn] {
return conn->_out.flush();
});
}).finally([conn] {
return conn->_out.close().finally([conn]{});
});
});
}).or_terminate();
假设已经按照要求,正确地初始化了SEASTAR框架。
上面代码功能是:
- 使用listen 接口创建一个listener;
- 开启一个filber, 该filber 一直处理accept时间,直到终止条件出现;
- 当accept返回时,创建一个新连接,并开启一个filber 处理该连接上的请求,直到该连接关闭。
- 网络接口会暴露input_stream<char> 和 output_stream<char> 对象,将连接上的数据抽象为2个方向的流。process函数读与写数据,直接操作stream对象即可。
客户端使用方式
客户端代码使用样例如下:
engine().connect(make_ipv4_address(server_addr)).then([this] (connected_socket fd) {
auto conn = make_connection(std::move(fd));
conn->process();
}).or_terminate();
假设已经按照要求,正确地初始化了SEASTAR框架。
上述代码实现功能是:
- 与远端服务器建立TCP链接,connect 函数是异步建立链接,不会阻塞线程;
- 当链接建立完成后,then 函数传入的lambda 会执行;
- connect_socket里包含了input_stream output_stream 对象;
- 业务逻辑代码可以通过stream对象读写数据;
大致一看,是不是很简单,你完全看不到EPOLL机制的影子。显然,框架将这些细节包装了,提供了良好抽象的接口。
如何实现的?
Linux 服务器端网络编程常用的几种模式:
- 模式一: 1 主线程 + N 工作线程模式 主线程创建server socket 并监听在对应FD上, 当收到新建链接时,将新建链接的FD传递给工作线程(例如通过队列);工作线程将该FD事件添加到epoll中,完成该线程上读写数据请求;
- 模式二:N 独立工作线程模式 在linux内核支持REUSEPORT机制后,同一个IP:PORT上可以绑定多个独立的server socket。那就不需要主线程了,每个工作线程都拥有自己的server socket, 从该socket 上收取新连接,处理链接上请求。这种模式大大提高了网络IO性能。
Seastar 实际上是实现了上述两种模式。
在模式一中,主线程+N 工作线程模式中,稍微有点区别是,主线程处理建立新链接,并将新链接分发给其他工作线程外,该线程也会处理属于自己的链接上的请求。也就是说这个主线程兼容了工作线程职责。
在模式一中,会涉及到 负载均衡问题、主线程与工作线程通讯问题。由于在SEASTART框架中线程之间实现了消息队列,主线程通过消息队列将新建链接分发给工作线程。
在模式二中,各个线程相互独立,都拥有自己的server socket。
在接口层面,与TCP协议功能相关的接口类主要有3个:
- network_stack: 提供listen方法,创建具体的server_socket实现;
- server_socket: 提供accept方法,创建具体的connected_socket具体实现;
- connected_socket:提供读写数据流对象;
我们看看服务器端监听端口的engine::listen方法如何实现的,代码如下:
server_socket reactor::listen(socket_address sa, listen_options opt) {
return server_socket(_network_stack->listen(sa, opt));
}
简单的说就是,调用代表网络协议栈的对象的listen方法,创建了一个server_socket类。代表网络协议栈的 _network_stack对象在框架启动时候,更加传入参数来确定具体创建那种类型协议栈,目前支持2种:linux内核协议栈(posix stack) 和 基于DPDK的用户态协议栈(native stack)。
// 注册代表内核协议栈的create函数; network_stack_registrator nsr_posix{"posix",
boost::program_options::options_description(),
[](boost::program_options::variables_map ops) {
return smp::main_thread() ? posix_network_stack::create(ops) : posix_ap_network_stack::create(ops);
},
true
};
// 注册代表用户态协议栈的create函数; network_stack_registrator nns_registrator{
"native", nns_options(), native_network_stack::create
};
在上述模式一中,主线程的server_socket 具体实现为 “`posix_server_socket_impl“`, 而工作线程的为“`posix_ap_server_socket_impl“`。在模式二中,每个线程的server_socket实现均为“`posix_reuseport_server_socket_impl“`。
在模式一中“`posix_server_socket_impl“` 的accept方法会将新建连接分发到不同的CPU CORE上。“`posix_ap_server_socket_impl“`的accept 方法在特定队列里去查看是否有新建连接,若有直接返回ready future, 如果没有就返回unready future, 该future 由“`posix_server_socket_impl“`的accept方法来set_value。 而模式二中,“`posix_reuseport_server_socket_impl“`的accept方法实现比较简单,没有链接分发和负载均衡的实现(负载均衡在内核层面已经做了)。
总结
SEASTAR 框架提供了异步编程接口,当然也包含异步的网络编程接口。抽象出network_stack、server_socket 和 connected_socket 接口类。对于不同协议栈,以及不同网络编程模式,只要实现各自的接口即可。在用户层面是统一的编程接口。良好抽象的代码,清晰明了,方便维护。
SEASTAR项目主页:
Seastar