转自:http://blog.chinaunix.net/uid-22273897-id-393650.html
经过了近一个星期的反复纠结,今天要对于我的第一个服务器程序做一个小节。区区三百多行的程序,写了将近一个星期,整个程序不知重写了多少遍(应该在10遍左右)。作为第一个项目,它是失败的,下面是我的一些思考与总结。
关于需求
进行程序开发,对于需求的把握是至关重要的。可以说,我之前没有任何开发服务器程序的经验,因此首先在对于需求的把握上出现了问题。
本程序的功能:
在Linux环境下实现一个服务器程序,通过管道,从本地的客户端读取数据,然后进行解析、组包,之后发送POST给远程的服务器程序。最后,读取远程服务器发送回来的响应,并打印在屏幕上。
我的第一个程序就是仅仅考虑了上述的基本需求,然后编写了一个单线程同步程序实现。但是,这样的程序并不能够满足一个服务器程序的需求。
主要问题出现在如下方面:
1. 服务器程序代码要保证绝对的健壮,不能因为程序或数据的异常而退出。
2. 程序要保证尽量减少数据包的丢失。
3. 程序对于管道的监听应该采用阻塞的模式。
4. 对于管道传递过来的大量数据进行快速的处理。
5. 在保证整个服务器程序安全性的基础上,要尽量增大整个系统的吞吐量。
如果同时的去考虑上述的问题,那么原先的程序结构就要进行调整了。
管道通信
我的程序当中采用了FIFO进行本地程序之间的通信。对于FIFO的应用,我们需要清楚如下事实:
1. open()、read()都可以阻塞程序的运行。到服务器端调用open函数时,如何此时没有客户端连接则程序会发生阻塞,直到第一个客户端程序与服务器程序建立管道连接为止。在建立连接之后,如果管道当中没有数据可读,则read函数会发生阻塞直到管道当中有数据可读为止。如果无客户端连接,read不会发生阻塞,并且其返回值为0。
上面这一条要特别的注意,因为当read返回值为0的时候,我们就不能以read阻塞的方式来进行监听了,倘若程序当中不存在其它的阻塞方式,那么整个服务器程序就会陷入循环调用,他会严重的浪费处理器资源。具体情景如下:
server_fifo_fd = open(SERVER_FIFO_NAME,O_RDONLY);
while(1)
{
read_res = read(server_fifo_fd, &my_data, sizeof(my_data));
cout << read_res << endl;
if(read_res > 0)
{
...
}
}
一旦出现无客户端连接的状态,这个程序将无法进行进入“空转”状态,其表现为不断的打印0.这样,我们就会因为一个“什么也不做”的循环而白白浪费处理器资源。
那么,我们又应该如何解决上述问题呢?请看下面代码:
server_fifo_fd = open(SERVER_FIFO_NAME,O_RDONLY);
while(1)
{
read_res = read(server_fifo_fd, &my_data, sizeof(my_data));
cout << read_res << endl;
if(read_res > 0)
{
...
}
else
{
close(server_fifo_fd);
server_fifo_fd = open(SERVER_FIFO_NAME,O_RDONLY);
}
}
如何发现没有客户端连接在管道上,我们就关闭管道,然后再打开,通过open来再次让程序进入阻塞状态。
io_service
根据我现在的理解,io_service就是一个任务调度机。我们将任务交给io_service,他负责调度现有的系统资源,把这些任务消费掉。对于不同种类的任务,可以将它们分配给不同种类的调度机来分别执行,这样即便于管理,又有利于增加程序的吞吐量。
在我的程序当中,大体存在两个独立的任务:
1. 从管道读取数据。
2. 与远程服务器之间的通信。
这样,可以建立两个调度机来管理这两个相对独立任务。为了运行这两个调度机,我们首先需要将它们分别绑定在两个线程上。这里还不得不提到一个问题:boost库所提供的io_service在没有任务执行的时候会自动的退出。而boost库当中标准的线程绑定方式如下:
boost::thread *t_read = new boost::thread(boost::bind(
&boost::asio::io_service::run, io_service_read));
此时,相当于开始运行了io_service。也就是说,如果在它之前,没有对io_service进行初始的绑定,那么程序就会自行的退出。再有就是如果在运行的过程当中,io_service处理完了其本身的所有任务,而服务器程序又不会新建一个调度机,那么该程序也将死掉。为了解决上述问题,我们需要对于io_service绑定一个资源消耗低而且会永远执行下去的程序。
boost::asio为我们提供的定时器可以满足上述的需求,我们可以创建一个循环定时器作为io_service的初始化任务。代码如下:
class io_clock
{
private:
boost::asio::deadline_timer timer;
public:
io_clock(boost::asio::io_service &io):
timer(io, boost::posix_time::hours(24))
{
timer.async_wait(boost::bind(&io_clock::no_dead,this));
}
void no_dead()
{
timer.expires_at(timer.expires_at()+boost::posix_time::hours(24));
timer.async_wait(boost::bind(&io_clock::no_dead, this));
}
};
这段代码是一段经典的定时器异步程序。关于异步程序的问题,过一会再讨论。
下面继续讨论io_service。现在,我们已经知道什么是调度机了,并且计划在系统当中运用两个调度机,一个处理管道读取,另外一个运行远程通信。大致流程是,从管道读取数据,之后进行解析,将解析后的数据传入另外一个调度机当中实现数据包的组成、发送以及接收等操作。
io_service2 处理远程通信 |
io_service1 处理管道读取 |
解析后的数据 |
这里就涉及到io_service当中的post方法了。它可以实现将一个函数绑定到一个正在运行的io_service之上。这样,只要实现每当io_service1产生了一个数据就可以通过post的方式传递给io_service2来进行继续的执行。
请看下面这段代码:
while(1)
{
//读取数据
read_res = read(server_fifo_fd, &my_data, sizeof(my_data));
cout << read_res << endl;
if (read_res > 0)
{
//对于读取后的数据进行解析
for (int i = 0; i < my_data.number; i++)
{
std::string Furl;