標籤:cer 共用 http請求 函數 cte 輸出 位元組 c++11 運行
https://toutiao.io/posts/xm2fr/preview
一直在找實習,有點什麼東西直接就在evernote裡面記了,也沒時間來更新到這裡。找實習真是個蛋疼的事,一直找的是困難模式的C++的後台開發這種職位,主要是因為其他的更不會了。雖然找的是C++的職位,但是我的簡曆有倆項目都是php的,因為老趙的項目就是用php做網站。最近越來越感覺這樣的簡曆不靠譜,想換個C++的和網路有關的多線程的項目吧。所以最近準備點幾個網路和多線程的技能點。於是我看了tinyhttpd、LightCgiServer和吳導的husky。基本上對著吳導的husky抄了個paekdusan,但是也不能純粹的抄一遍啊,所以還是改了一些小東西,大的架構沒變。主要的改變包括以下幾方面:
- 線上程池部分中,使用C++11的thread替代了pthread,從而實現跨平台的目標
- 在支援並發的隊列中,使用C++11的mutex和lock替代了pthread的mutex和lock,從而實現跨平台的目標
- 在socket部分,使用了先行編譯宏的方式,從而實現跨平台的目標
- 接收資料部分更健壯,以面對不能一次性讀完一個HTTP頭部的情況;發送也一樣
- 實現了一個具有簡易的KeepAlive策略的HTTP伺服器
- 實現了一個靜態檔案的HTTP伺服器
tinyhttpd和LightCgiServer
首先,還是先介紹一下tinyhttpd吧。網上的評價還是很高的,能讓人僅從500-600行的代碼中瞭解HTTP Server的本質。 貼一張tinyhttpd的流程圖吧:
關於tinyhttpd更詳細的資訊,大家還是直接去看代碼吧,因為真的很易讀、易懂。tinyhttpd的代碼給人的感覺就是,怎麼易讀、易懂怎麼來,例如伺服器回複一個501 Method Not Implemented的response是這麼寫的,看到我就驚呆了,只能怪我以前看過的代碼太少,我第一反應就是先sprintf到一個長的buff裡面,然後一起send,但是它這樣的寫法確實更加易懂、易讀。
void unimplemented(int client) { char buf[1024]; sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n"); send(client, buf, strlen(buf), 0); sprintf(buf, SERVER_STRING); send(client, buf, strlen(buf), 0); sprintf(buf, "Content-Type: text/html\r\n"); send(client, buf, strlen(buf), 0); sprintf(buf, "\r\n"); send(client, buf, strlen(buf), 0); sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n"); send(client, buf, strlen(buf), 0); sprintf(buf, "</TITLE></HEAD>\r\n"); send(client, buf, strlen(buf), 0); sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n"); send(client, buf, strlen(buf), 0); sprintf(buf, "</BODY></HTML>\r\n"); send(client, buf, strlen(buf), 0);}
除此之外,值得一提的是tinyhttpd實現的是一個CGI Server的功能,但是在CGI的功能上實現得比較簡陋,LightCgiServer實現得更完整一些,關於CGI Server更詳細的情況請看CGI Server
husky和paekdusan
正如本文開頭所說,大的程式結構上,paekdusan基本是對著husky抄的,只是做了一些小的改變。程式在大的結構上,可以看作是一個生產者消費者模型。
先看一個不倫不類的流程圖:
從可以看出,主線程是生產者,線程池中的線程們是消費者,它們之間通過task隊列來通訊。主線程作為生產者,accept成功返回之後,將處理該client的task添加到task隊列中,然後繼續accept等待client的到來;線程池的線程們作為消費者,不斷的從task隊列中取出task,調用task的run介面。
值得注意的是,task隊列是一個BoundedBlockingQueue,也就是說,task隊列是一個有容量限制,並且阻塞的隊列。當消費者試圖從task隊列中取task時,如果task隊列是空的,則消費者會被阻塞,直到生產者往task隊列中放入task,將消費者喚醒。同樣的,當生產者試圖向task隊列中放入task時,如果task隊列是滿的,則生產者會被阻塞,直到消費者從task隊列中取出task,將生產者喚醒。
再看一個不倫不類的時序圖:
這裡要求task實現了run介面,task隊列的設計可以認為是command模式的實踐。看上去有很多類,但是其實是因為每個類的功能比較單一,程式只是把一些功能單一的類組合在一起了而已,其實類之間的耦合性比較低。
具體實現的代碼見這裡paekdusan
問題記錄
- HTTP協議的基本格式
HTTP的request的第一部分是request line,以空格分割得到的三部分依次是method,URI和version
HTTP的request的第二部分是header,header以\r\n結尾,header中的每一行也以\r\n結尾,也就是說,當header是空時,以一個\r\n結尾;當header不空時,一定是以兩個連續的\r\n結尾的。heder中的每一行格式是 key : value,其中value可以是空,所以簡單的說,header是一個map,鍵和值之間用:分隔,索引值對之間用\r\n分隔,在map的最後還有一個\r\n。值得注意的是,cookie是在header裡面的。
HTTP的request的第三部分是body,協議規定body後面不能再有其他字元,所以body不能靠去找\r\n來結束,要靠header裡面的content-length來指明,content-length就是body的位元組數。
HTTP的response的第一部分是response line,以空格分割得到的三部分依次是version,status code和Reason Phrase
HTTP的response的第二部分是header,格式和request類似
HTTP的response的第三部分是body,格式和request類似
另外,還有一點,我不知道request裡面有沒有可能出現,反正在response裡面是會出現的,那就是如果header中指明了transfer-coding是chunked,那麼body將會是一串chunked的塊。在HTTP協議的rfc2616中是這麼定義chunk-body的格式的:
Chunked-Body = *chunk last-chunk trailer CRLFchunk = chunk-size [ chunk-extension ] CRLF chunk-data CRLFchunk-size = 1*HEXlast-chunk = 1*("0") [ chunk-extension ] CRLFchunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )chunk-ext-name = tokenchunk-ext-val = token | quoted-string chunk-data = chunk-size(OCTET)trailer = *(entity-header CRLF)也就是說chunk由四部分組成,首先是若干個chunk塊(每個chunk塊由chunk-size,可選的chunk-extension,\r\n, chunk-data 和 \r\n組成),接著是last-chunk塊(chunk-size是0,沒有chunk-data的特殊chunk塊),然後是trailer(若干和header一樣格式的資料群組成),最後是一個\r\n。其實不看中間“可選的chunk-extension”還是比較簡單的。
- KeepAlive的實現
在之前husky的代碼中,server端發送了response之後,就close socket了,即關閉該socket,如果client需要再次發送http request需要再次建立一個新的tcp串連。而開啟一個常見的網頁,通常有很多http request從client發送到server,那麼就需要很多次tcp的建立和斷開,比較低效。之所以用KeepAlive就是為了避免多次請求需要重複的建立TCP串連,也就是說server端發送完response之後,不關閉串連,而是在該串連上繼續等待資料。KeepAlive在HTTP1.1是預設開啟的,如果要關閉,需要在header中聲明connection: close。
但是樸素的KeepAlive會引起一些問題,例如client一直不中斷連線,那麼和client的串連一直保持,client多了的時候,新來的client無法獲得server的資源,所以需要一些其他的折衷。例如如果接下來的5s內都沒有收到資料則中斷連線,或者是接下來的5s內伺服器接收了100個用戶端請求就中斷連線。
由於“5s內伺服器接收了100個用戶端請求就中斷連線”這需要在根據一個線程外部的資訊控制線程的運行,使得線程運行過程中對於外部的以來過多,故而paekdusan沒有這麼實現,而是在同一個串連上接收了50個http request之後中斷連線。另外paekdusan還實現了“一個串連的期間超過5s就中斷連線”,具體來說是這樣的,recv逾時時間1s,每次recv完資料之後判斷距離第一次recv的時間是否超過5s,超過則中斷連線。詳見如下代碼:
//簡單起見 刪除了一些處理不完整http請求的代碼,並且簡化了now 和 startTime的設定 //詳見https://github.com/aholic/paekdusan/blob/master/KeepAliveWorker.hpp while ((now - startTime <= 5) && requestCount > 0) { recvLen = recv(sockfd, recvBuff, RECV_BUFFER_SIZE, 0); if (recvLen <= 0 && getLastErrorNo() == ERROR_TIMEOUT) { LogInfo("recv returns %d", recvLen); continue; } if (recvLen <= 0) { LogError("recv failed: %d", getLastErrorNo()); break; } //do with recvBuff, get a http request requestCount--;}但是由於使用的是阻塞的recv,所以實現的不是非常合理,存在一些問題。 例如此時恰好距離第一次recv只有4.99秒,所以(now - startTime <= 5)滿足,繼續進入while迴圈,然後阻塞在recv上,recv設定的逾時是1秒,那麼其實最後跳出while迴圈的時間距離第一次recv已經過去了5.99秒。暫時沒想到什麼好辦法,因為把recv設定成理解返回的話,while迴圈的次數太多,效率也不高。所以最好是要有一種通知的機制。
- CGI Server
CGI Server一般是要fork一個進程來執行http request的URI中指定的CGI Script的,並且通過環境變數,向CGI Script傳遞本次請求的資訊,具體怎麼做可以看看這篇文章。但是注意到paekdusan是一個多線程的伺服器,所以這裡涉及到多線程和多進程,這是個很蛋疼的情況。多線程和多進程的混合會有很多問題,這篇文章有詳細的介紹,好吧,你可能會發現它被牆了,那我還是簡單的介紹一下會有什麼問題吧。
首先需要說明的是,在一個子線程中調用fork會發生什麼:產生的子進程中只會有一個線程,也就是調用fork的這個線程。
那麼問題來了,假如父進程中的其他線程擷取了一個鎖,正在改線程間共用的資料,這時共用資料處於半修改狀態。但是在子進程中,其他線程都消失了,那這些共用資料的修改怎麼辦?並且,鎖的狀態也得未定義了。另外,即使你的代碼是安全執行緒的,你也不能保證你用的Lib的實現是安全執行緒的。
所以唯一合理的在多線程的環境下使用多進程的情況,只有fork之後立即exec,也就是馬上講子進程替換成一個新的程式,這樣的話,子進程中所有的資料都變得不重要,都拋棄了。所以,其實多線程的CGI Server也算是合理,但是需要注意安全性問題。因為開啟的子進程預設是繼承了父進程的檔案描述符的,也就是說,子進程可以有父進程對檔案的讀寫權限。
我大概知道的就這麼多,更詳細的還是FQ去讀原文吧。
- C++11的thread
C++11的thread用起來感覺比以前的linux上的pthread或者是windows上的beginthread都好用太多來,來一段簡短的代碼展示一下基本用法吧。
void sayWord(const string& word) { for (int i = 0; i < 1000; i++) { cout << word << endl; }} void saySentence(const string& sentence) { for (int i = 0; i < 1000; i++) { cout << sentence << endl; }} int main() { string word = "hello"; string sentence = "this is an example from cstdlib.com"; thread t1(std::bind(sayWord, ref(word))); thread t2(saySentence, ref(sentence)); t1.join(); t2.join(); return 0;}運行以上代碼,會發現交替輸出“hello”和“this is an example from cstdlib.com”。注意到t1和t2的構造參數看起來怪怪的,“std::bind(sayWord, ref(word))”和“saySentence, ref(sentence)”,主要線程函數的參數是引用,有個模版裡面的bind在這,我也很難解釋清楚,感覺模版叼叼的。另外,以非靜態類成員函數建立線程時,需要在參數中帶上this或者是ref一個對象的執行個體,不然無法調用。
- C++11的mutex和lock
注意到上面thread的代碼其實是有問題的,兩個線程交替輸出的東西可能會混合,所以要加鎖。C++11的mutex和lock也很好用。
mutex mtx; void sayWord(const string& word) { for (int i = 0; i < 1000; i++) { lock_guard<mutex> lock(mtx); cout << word << endl; }} void saySentence(const string& sentence) { for (int i = 0; i < 1000; i++) { lock_guard<mutex> lock(mtx); cout << sentence << endl; }}代碼裡面用的是lock_guard,lock_guard就是在建構函式裡面調用mutex的lock方法,解構函式裡面調用mutex的unlock方法,用起來比較方便。unique_lock和lock_guard類似,但是多了一些其他的成員函數來配合其他的mutex類。關於mutex和lock的用法,[這篇部落格](http://www.cnblogs.com/haippy/p/3237213.html)說的比較詳細。主要和pthread裡面的lock的區別就是,在pthread中,重複擷取一個已經獲得的lock不會報錯,而在C++11中會報錯。
- C++11的condition_variable
在paekdusan的task隊列是BoundedBlockingQueue,也就是說有阻塞和喚醒的操作。所以涉及到condition_variable。condition_variable主要用兩個函數:wait(unique_lock<mutex>& lck)和notify_one()。線程被wait阻塞時,會先調用lck.unlock()函數釋放鎖;線程被notify_one喚醒時,會調用lck.lock()擷取鎖,以回複當時wait前的樣子。關於condition_variable的用法,這篇部落格說的比較詳細
- windows和linux上socket的API的不同點
- windows上需包含winsock.h;linux上需包含cerrno,sys/socket.h,sys/types.h,arpa/inet.h和unistd.h
- windows上調用socket之前要調用WSAStartup,並且用#pragma comment(lib,”Ws2_32”)連結Ws2_32.lib
- windows上關閉socket的函數叫做closesocket;linux上叫做close
- windows上擷取錯誤碼用GetLastError();linux上查看全域變數errno,錯誤碼的意義也不一樣
- windows上設定SO_RCVTIMEO和SO_SNDTIMEO選項時,單位是毫秒;linux是秒
- windows上accept的原型是accept(SOCKET, struct sockaddr, int);linux上accept的原型是accept(int, struct sockaddr, socklen_t)
我遇到的就這些,估計還有很多,只是我還沒遇到。
相關閱讀
[1]:Threads and fork(): think twice before mixing them
[2]:RFC2616
[3]:寫了一個簡單的CGI Server
[4]:tinyhttpd源碼分析
[5]:HTTP協議頭部與Keep-Alive模式詳解
[6]:C++11並髮指南
Web Server 和 HTTP 協議