來(lái)自:http://blog.csdn.net/NewNebuladream/archive/2009/08/12/4437888.aspx

  C++掃盲系列--第一個(gè)服務(wù)器程序 收藏

關(guān)于需求

         進(jìn)行程序開發(fā),對(duì)于需求的把握是至關(guān)重要的。可以說,我之前沒有任何開發(fā)服務(wù)器程序的經(jīng)驗(yàn),因此首先在對(duì)于需求的把握上出現(xiàn)了問題。

本程序的功能:

Linux環(huán)境下實(shí)現(xiàn)一個(gè)服務(wù)器程序,通過管道,從本地的客戶端讀取數(shù)據(jù),然后進(jìn)行解析、組包,之后發(fā)送POST給遠(yuǎn)程的服務(wù)器程序。最后,讀取遠(yuǎn)程服務(wù)器發(fā)送回來(lái)的響應(yīng),并打印在屏幕上。

我的第一個(gè)程序就是僅僅考慮了上述的基本需求,然后編寫了一個(gè)單線程同步程序?qū)崿F(xiàn)。但是,這樣的程序并不能夠滿足一個(gè)服務(wù)器程序的需求。

主要問題出現(xiàn)在如下方面:

1.       服務(wù)器程序代碼要保證絕對(duì)的健壯,不能因?yàn)槌绦蚧驍?shù)據(jù)的異常而退出。

2.       程序要保證盡量減少數(shù)據(jù)包的丟失。

3.       程序?qū)τ诠艿赖谋O(jiān)聽?wèi)?yīng)該采用阻塞的模式。

4.       對(duì)于管道傳遞過來(lái)的大量數(shù)據(jù)進(jìn)行快速的處理。

5.       在保證整個(gè)服務(wù)器程序安全性的基礎(chǔ)上,要盡量增大整個(gè)系統(tǒng)的吞吐量。

如果同時(shí)的去考慮上述的問題,那么原先的程序結(jié)構(gòu)就要進(jìn)行調(diào)整了。

管道通信

         我的程序當(dāng)中采用了FIFO進(jìn)行本地程序之間的通信。對(duì)于FIFO的應(yīng)用,我們需要清楚如下事實(shí):

1.       open()read()都可以阻塞程序的運(yùn)行。到服務(wù)器端調(diào)用open函數(shù)時(shí),如何此時(shí)沒有客戶端連接則程序會(huì)發(fā)生阻塞,直到第一個(gè)客戶端程序與服務(wù)器程序建立管道連接為止。在建立連接之后,如果管道當(dāng)中沒有數(shù)據(jù)可讀,則read函數(shù)會(huì)發(fā)生阻塞直到管道當(dāng)中有數(shù)據(jù)可讀為止。如果無(wú)客戶端連接,read不會(huì)發(fā)生阻塞,并且其返回值為0

上面這一條要特別的注意,因?yàn)楫?dāng)read返回值為0的時(shí)候,我們就不能以read阻塞的方式來(lái)進(jìn)行監(jiān)聽了,倘若程序當(dāng)中不存在其它的阻塞方式,那么整個(gè)服務(wù)器程序就會(huì)陷入循環(huán)調(diào)用,他會(huì)嚴(yán)重的浪費(fèi)處理器資源。具體情景如下:

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)

{

          ...

     }

}

一旦出現(xiàn)無(wú)客戶端連接的狀態(tài),這個(gè)程序?qū)o(wú)法進(jìn)行進(jìn)入“空轉(zhuǎn)”狀態(tài),其表現(xiàn)為不斷的打印0.這樣,我們就會(huì)因?yàn)橐粋€(gè)“什么也不做”的循環(huán)而白白浪費(fèi)處理器資源。

 

那么,我們又應(yīng)該如何解決上述問題呢?請(qǐng)看下面代碼:

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);

     }

}

 

如何發(fā)現(xiàn)沒有客戶端連接在管道上,我們就關(guān)閉管道,然后再打開,通過open來(lái)再次讓程序進(jìn)入阻塞狀態(tài)。

io_service

根據(jù)我現(xiàn)在的理解,io_service就是一個(gè)任務(wù)調(diào)度機(jī)。我們將任務(wù)交給io_service,他負(fù)責(zé)調(diào)度現(xiàn)有的系統(tǒng)資源,把這些任務(wù)消費(fèi)掉。對(duì)于不同種類的任務(wù),可以將它們分配給不同種類的調(diào)度機(jī)來(lái)分別執(zhí)行,這樣即便于管理,又有利于增加程序的吞吐量。

         在我的程序當(dāng)中,大體存在兩個(gè)獨(dú)立的任務(wù):

1.       從管道讀取數(shù)據(jù)。

2.       與遠(yuǎn)程服務(wù)器之間的通信。

這樣,可以建立兩個(gè)調(diào)度機(jī)來(lái)管理這兩個(gè)相對(duì)獨(dú)立任務(wù)。為了運(yùn)行這兩個(gè)調(diào)度機(jī),我們首先需要將它們分別綁定在兩個(gè)線程上。這里還不得不提到一個(gè)問題:boost庫(kù)所提供的io_service在沒有任務(wù)執(zhí)行的時(shí)候會(huì)自動(dòng)的退出。而boost庫(kù)當(dāng)中標(biāo)準(zhǔn)的線程綁定方式如下:

     boost::thread *t_read = new boost::thread(boost::bind(

              &boost::asio::io_service::run, io_service_read));

此時(shí),相當(dāng)于開始運(yùn)行了io_service。也就是說,如果在它之前,沒有對(duì)io_service進(jìn)行初始的綁定,那么程序就會(huì)自行的退出。再有就是如果在運(yùn)行的過程當(dāng)中,io_service處理完了其本身的所有任務(wù),而服務(wù)器程序又不會(huì)新建一個(gè)調(diào)度機(jī),那么該程序也將死掉。為了解決上述問題,我們需要對(duì)于io_service綁定一個(gè)資源消耗低而且會(huì)永遠(yuǎn)執(zhí)行下去的程序。

boost::asio為我們提供的定時(shí)器可以滿足上述的需求,我們可以創(chuàng)建一個(gè)循環(huán)定時(shí)器作為io_service的初始化任務(wù)。代碼如下:

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));

     }

};

 

    這段代碼是一段經(jīng)典的定時(shí)器異步程序。關(guān)于異步程序的問題,過一會(huì)再討論。

    下面繼續(xù)討論io_service。現(xiàn)在,我們已經(jīng)知道什么是調(diào)度機(jī)了,并且計(jì)劃在系統(tǒng)當(dāng)中運(yùn)用兩個(gè)調(diào)度機(jī),一個(gè)處理管道讀取,另外一個(gè)運(yùn)行遠(yuǎn)程通信。大致流程是,從管道讀取數(shù)據(jù),之后進(jìn)行解析,將解析后的數(shù)據(jù)傳入另外一個(gè)調(diào)度機(jī)當(dāng)中實(shí)現(xiàn)數(shù)據(jù)包的組成、發(fā)送以及接收等操作。

    現(xiàn)在一個(gè)新的問題產(chǎn)生了:如何實(shí)現(xiàn)兩個(gè)調(diào)度機(jī)之間的數(shù)據(jù)通信呢?

    這里就涉及到io_service當(dāng)中的post方法了。它可以實(shí)現(xiàn)將一個(gè)函數(shù)綁定到一個(gè)正在運(yùn)行的io_service之上。這樣,只要實(shí)現(xiàn)每當(dāng)io_service1產(chǎn)生了一個(gè)數(shù)據(jù)就可以通過post的方式傳遞給io_service2來(lái)進(jìn)行繼續(xù)的執(zhí)行。

請(qǐng)看下面這段代碼:

         while(1)

         {

              //讀取數(shù)據(jù)

              read_res = read(server_fifo_fd, &my_data, sizeof(my_data));

              cout << read_res << endl;

              if (read_res > 0)

              {

                   //對(duì)于讀取后的數(shù)據(jù)進(jìn)行解析

for (int i = 0; i < my_data.number; i++)

                   {

                       std::string Furl;

                       Furl = my_data.package[i].url;

 

                       post = my_data.package[i].post;

 

                       std::size_t first_sign = Furl.find("http://");

 

                       url = Furl.substr(first_sign + 2);

                       std::size_t second_sign = url.find("/");

                       url = Furl.substr(first_sign + 2, second_sign);

 

                       path = Furl.substr(first_sign + second_sign + 2);

                       //若為域名,則需要先解析為IP地址。

                       if (url.find("www") != url.npos)

                       {

                            host = gethostbyname(url.c_str());

                            char **pptr;

                            char str[32];

                            pptr = host->h_addr_list;

                            inet_ntop(host->h_addrtype, *pptr, str, sizeof(str));

                            url = str;

                            cout << url;

                       }

                       client Client (*socket_io,url, path, post);                                              io->post(boost::bind(&client::process,&Client,url));

                   }//end for

              }//end if

              else

              {

                   close(server_fifo_fd);

                   server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY);

              }

         }

    上面程序當(dāng)中,紅色的部分是不安全的(在異步的情況下)。在掌握了io_service的基本原理之后,這個(gè)問題就變得簡(jiǎn)單易懂了。上面這段程序是由io_service1運(yùn)行的,其將接收來(lái)的地址解析之后就拋給了io_service2來(lái)繼續(xù)處理。倘若io_service2還沒有處理完client當(dāng)中的process方法時(shí)本次for循環(huán)就結(jié)束了,那么Client就會(huì)被析構(gòu)。這時(shí)io_service2當(dāng)中的任務(wù)實(shí)際上已經(jīng)被析構(gòu)掉了,那么io_service2執(zhí)行到這段任務(wù)的時(shí)候就會(huì)引發(fā)非法內(nèi)存的訪問!!!

      異步程序

         發(fā)生上述問題的一個(gè)前提是

io->post(boost::bind(&client::process,&Client,url));的運(yùn)行不會(huì)發(fā)生阻塞。

         如果process是普通的同步程序,這個(gè)問題就不會(huì)發(fā)生。但同步的阻塞會(huì)影響程序并發(fā)的行為,這樣就可能降低系統(tǒng)的吞吐量。

         為了程序運(yùn)行的非阻塞實(shí)現(xiàn),我們采用異步程序設(shè)計(jì)的方式。同步IO與異步IO的本質(zhì)區(qū)別是:同步IO會(huì)block當(dāng)前的調(diào)用線程,而異步IO則允許發(fā)起IO請(qǐng)求的調(diào)用線程繼續(xù)執(zhí)行,等到IO請(qǐng)求被處理后,會(huì)通知調(diào)用線程。關(guān)于異步IO的介紹詳情可以參考http://www.ibm.com/developerworks/cn/linux/l-async/ 

 

         我總結(jié):寫異步程序的關(guān)鍵就是始終提醒自己,這段代碼在什么時(shí)候結(jié)束你永遠(yuǎn)不知道!

 

         這樣,如果我們將網(wǎng)絡(luò)任務(wù)編寫成異步程序就會(huì)實(shí)現(xiàn)多個(gè)線程并發(fā)的執(zhí)行各自的網(wǎng)絡(luò)任務(wù),而這些并發(fā)的線程則由io_service進(jìn)行統(tǒng)一的調(diào)度處理。

shared_ptr內(nèi)存管理機(jī)制

         從上面的模型可以看出,client實(shí)際上是由io_service1創(chuàng)建,然后連同數(shù)據(jù)一起拋給io_service2去執(zhí)行。這樣就產(chǎn)生了一個(gè)問題,如果通過new的方式來(lái)產(chǎn)生一個(gè)client對(duì)象,那么這個(gè)對(duì)象應(yīng)該在什么時(shí)候銷毀呢?答案是在process方法執(zhí)行結(jié)束后銷毀。但是process方法執(zhí)行什么時(shí)候執(zhí)行結(jié)束?只有它自己知道。除非用對(duì)象調(diào)用自己的析構(gòu)函數(shù)來(lái)銷毀掉自己。如果采用上述方式,我們就需要在異步程序結(jié)束之前,調(diào)用這個(gè)類本身的析構(gòu)函數(shù)。但是,假如異步程序中途產(chǎn)生異常而沒有執(zhí)行結(jié)束,那么這段內(nèi)存空間又由誰(shuí)來(lái)釋放呢?為了解決這個(gè)問題,我們用shared_ptr來(lái)管理client對(duì)象。這樣我們先聲明一個(gè)shared_ptr指針,然后更改client的聲明 class client:public boost::enable_shared_from_this<client> client內(nèi)部異步調(diào)用的this指針變成shared_from_this

      這樣,異步調(diào)用結(jié)束的時(shí)候,他的指針計(jì)數(shù)也將變?yōu)?/span>0,這段內(nèi)存空間就被自動(dòng)的析構(gòu)了。

其它問題

Linux操作系統(tǒng)本身限制:打開文件的數(shù)量上限為1024,倘若不對(duì)網(wǎng)絡(luò)并發(fā)進(jìn)行限制,很可能因?yàn)榇蜷_文件數(shù)量達(dá)到上限而被系統(tǒng)拒絕進(jìn)行socket服務(wù)。那么,如何控制并發(fā)在一定的范圍之內(nèi),從而避免數(shù)據(jù)包的丟失?

另外一個(gè)就是:本程序吞吐量限制的瓶頸是網(wǎng)絡(luò)并發(fā)程序的速度問題,本地管道傳送的速度相比之下要快得多了,這也是為什么在程序當(dāng)中,我并沒有把管道通信函數(shù)寫成異步程序的原因。倘若網(wǎng)絡(luò)并發(fā)程序的問題可以得到完美的解決,那么這個(gè)程序的代碼結(jié)構(gòu)恐怕還要發(fā)生如下兩處改變:

1.       程序暫時(shí)采用固定長(zhǎng)度的數(shù)據(jù)包發(fā)送,這在一定程度上降低了管道通信的速度。

2.       管道數(shù)據(jù)的讀取與解析在這里是作為一個(gè)同步串行程序來(lái)執(zhí)行的,可以進(jìn)行如下兩種方案的改進(jìn):

a.       將解析程序?qū)懭?/span>client類當(dāng)中,交給io_service2執(zhí)行。

b.       將管道通信函數(shù)寫成異步形式。

總結(jié)

短短的300多行代碼當(dāng)中卻集中了管道、操作系統(tǒng)原理、線程池管理、內(nèi)存管理、智能指針、異步IO、多線程……等思想。作為初學(xué)者來(lái)說,這個(gè)程序使我學(xué)會(huì)了很多,但是也暴露了很多問題:

1.       對(duì)于異步與多線程的概念陌生,導(dǎo)致在編程的過程當(dāng)中發(fā)生了低級(jí)的訪問非法內(nèi)存錯(cuò)誤。

2.       在編寫多線程異步程序時(shí),要改變思路,不能陷入同步程序的思維模式當(dāng)中。

3.       在解決問題之前,盡量弄清楚要解決的問題到底是什么,即需求一定要做好!對(duì)于服務(wù)器程序運(yùn)行需求的不充分理解,導(dǎo)致了我始終不清楚什么才是符合要求的程序!

4.       如何更有效的解決問題?

所謂“有效”就是用最短的時(shí)間以適當(dāng)?shù)姆桨附鉀Q問題。

如果沒有足夠的基礎(chǔ),那么無(wú)疑會(huì)浪費(fèi)解決問題的時(shí)間。在寫這個(gè)程序之前,我的腦海當(dāng)中對(duì)于多線程異步調(diào)用的概念理解很模糊,更別說按照這個(gè)思想來(lái)寫程序了。但是,如果等到了解了所有知識(shí)之后再去解決問題,時(shí)間成本可能又會(huì)很高。那么,如何在兩者之間做一個(gè)權(quán)衡就是快速解決問題的關(guān)鍵所在。

我覺得,首先應(yīng)該明確問題到底是什么。要盡量考慮一切可能出現(xiàn)的問題,然后再考慮如何簡(jiǎn)化問題,先做什么,后做什么。這樣,就不至于由于需求不明確而導(dǎo)致對(duì)于問題本身的曲解。例如,假設(shè)我的服務(wù)器程序僅僅用來(lái)提供對(duì)幾臺(tái)主機(jī)的服務(wù),那么這個(gè)程序的結(jié)構(gòu)就用不著這么復(fù)雜了。所以,在這種情況下,數(shù)據(jù)通信量的因素就必須納入到問題需求的考慮范圍之內(nèi)。

總結(jié)一句話:在做需求的時(shí)候,一定要弄清所有決定問題本質(zhì)的關(guān)鍵性因素后再開始制定解決問題的計(jì)劃,因?yàn)楹雎粤诉@些因素后,問題的本質(zhì)就發(fā)生了變化,就不再是原來(lái)的問題了。

對(duì)于解決問題可能會(huì)用到的知識(shí)至少要有一個(gè)概念上的清晰認(rèn)識(shí),然后再開始解決問題。否則,解決的過程就會(huì)變成一種盲目的探路,雖然最終問題也會(huì)得到解決,但是會(huì)因?yàn)槊つ啃远鴮?dǎo)致解決問題中走彎路,浪費(fèi)不必要的時(shí)間。