關(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í)間。