A TCP echo server with Boost.Asio(2)

在前一篇文章中,我们介绍了如何构建一个简单的TCP echo server。

本文继续谈一下如何构建一个可用程度更高一些(更复杂)的server。

Linux平台下的问题

Asio 给出的标准实例,是单个io_context可以多线程run,使用该io_context进行分发回调。

这个模型在window 上的iocp 实现,简直完美,因为接口都是系统api,各个线程等待完成事件都是不需要锁来等待的。锁只需要保护队列即可。

Linux 平台,使用epoll模拟,导致一个contex在多个线程run会有一把大锁直接锁调用。其实多线程run就是不同线程切换run,性能会有损失。但是,另一种做法是用一个io_context进行accept拿到连接,再建立一个io_context池,把连接的处理抛到池中执行。跟默认epoll模型一致,也就可以了。

一个这样的io_context池的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
typedef std::shared_ptr<boost::asio::io_context> io_context_ptr;
typedef boost::asio::executor_work_guard<
boost::asio::io_context::executor_type> io_context_work;

// A pool of io_context objects.
class io_context_pool : private boost::noncopyable
{
public:
// Construct the io_context pool.
explicit io_context_pool(): next_io_context_(0)
{
const auto pool_size = std::thread::hardware_concurrency();

// Give all the io_contexts work to do so that their run() functions will not
// exit until they are explicitly stopped.
for (std::size_t i = 0; i < pool_size; ++i)
{
io_context_ptr io_context(new boost::asio::io_context);
io_contexts_.push_back(io_context);
work_.push_back(boost::asio::make_work_guard(*io_context));
}
}

// Run all io_context objects in the pool.
void run()
{
// Create a pool of threads to run all of the io_contexts.
std::vector<std::shared_ptr<std::thread>> threads;
for(const auto& io_context : io_contexts_)
{
std::shared_ptr<std::thread> thread(new std::thread(
std::bind(&boost::asio::io_context::run, io_context)));
threads.push_back(thread);
}

// Wait for all threads in the pool to exit.
for(const auto& thread : threads)
thread->join();
}

// Stop all io_context objects in the pool.
void stop()
{
// Explicitly stop all io_contexts.
for(const auto& io_context : io_contexts_)
io_context->stop();
}

// Get an io_context to use.
boost::asio::io_context& get_io_context()
{
// Use a round-robin scheme to choose the next io_context to use.
boost::asio::io_context& io_context = *io_contexts_[next_io_context_];
++next_io_context_;
if (next_io_context_ == io_contexts_.size())
next_io_context_ = 0;
return io_context;
}

private:
// The pool of io_contexts.
std::vector<io_context_ptr> io_contexts_;

// The work that keeps the io_contexts running.
std::vector<io_context_work> work_;

// The next io_context to use for a connection.
std::size_t next_io_context_;
};

在这个io_context池的帮助下,我们可以在拿到连接后将连接抛入池中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void do_accept()
{
acceptor_.async_accept(socket_,
[this](boost::system::error_code ec)
{
if (!ec)
{
std::make_shared<session>(std::move(socket_))->start();
}
do_accept();
});

// hand the socket to a io_context in pool
// each io_context object run on their own thread
socket_ = tcp::socket(context_pool_.get_io_context());
}

这种做法有一个小的提升——因为每一个io_context对象都只在一个线程中run,所以socket的读写不再需要加锁了。

平滑重启

与客户端程序不同的是,服务端程序要尽可能的长时间运行,故障时能够自动恢复,并且更新时不能影响服务正在处理的请求。这就产生了平滑重启的功能需求。实际上,网络上比较流行的 HTTP 服务器 Nginx 就支持平滑重启。

平滑重启的原理实际上比较简单,即,当接收到平滑重启(或退出)的信号后,旧的服务关闭监听套接字,处理完所有请求后便退出。而在旧的服务关闭监听套接字后,启动一个新的服务监听套接字,处理新的请求。

Asio提供了signal_set来帮助我们处理应用程序的信号通信。

附录:实现源码