Web Server 和 HTTP協議
一直在找實習,有點什么東西直接就在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服務器 </ul>
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);
}</pre>
除此之外,值得一提的是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 CRLF chunk-size = 1HEX last-chunk = 1("0") [ chunk-extension ] CRLF
chunk-extension= ( ";" chunk-ext-name [ "=" chunk-ext-val ] ) chunk-ext-name = token chunk-ext-val = token | quoted-string chunk-data = chunk-size(OCTET) trailer = (entity-header CRLF)</pre>
也就是說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--;
}</pre>
-
但是由于使用的是阻塞的recv,所以實現的不是非常合理,存在一些問題。 例如此時恰好距離第一次recv只有4.99秒,所以(now - startTime <= 5)滿足,繼續進入while循環,然后阻塞在recv上,recv設置的超時是1秒,那么其實最后跳出while循環的時間距離第一次recv已經過去 了5.99秒。暫時沒想到什么好辦法,因為把recv設置成理解返回的話,while循環的次數太多,效率也不高。所以最好是要有一種通知的機制。
</li> -
CGI Server
CGI Server一般是要fork一個進程來執行http request的URI中指定的CGI Script的,并且通過環境變量,向CGI Script傳遞本次請求的信息,具體怎么做可以看看這篇文章。但是注意到paekdusan是一個多線程的服務器,所以這里涉及到多線程和多進程,這是個很蛋疼的情況。多線程和多進程的混合會有很多問題,這篇文章有詳細的介紹,好吧,你可能會發現它被墻了,那我還是簡單的介紹一下會有什么問題吧。
首先需要說明的是,在一個子線程中調用fork會發生什么:產生的子進程中只會有一個線程,也就是調用fork的這個線程。
那么問題來了,假如父進程中的其他線程獲取了一個鎖,正在改線程間共享的數據,這時共享數據處于半修改狀態。但是在子進程中,其他線程都消 失了,那這些共享數據的修改怎么辦?并且,鎖的狀態也得未定義了。另外,即使你的代碼是線程安全的,你也不能保證你用的Lib的實現是線程安全的。
所以唯一合理的在多線程的環境下使用多進程的情況,只有fork之后立即exec,也就是馬上講子進程替換成一個新的程序,這樣的話,子進 程中所有的數據都變得不重要,都拋棄了。所以,其實多線程的CGI Server也算是合理,但是需要注意安全性問題。因為打開的子進程默認是繼承了父進程的文件描述符的,也就是說,子進程可以有父進程對文件的讀寫權限。
我大概知道的就這么多,更詳細的還是KX上網去讀原文吧。
</li> -
C++11的thread
C++11的thread用起來感覺比以前的linux上的pthread或者是windows上的beginthread都好用太多來,來一段簡短的代碼展示一下基本用法吧。
</li> </ul>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;
}</pre>
運行以上代碼,會發現交替輸出“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中會報錯。
</li> -
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的用法,這篇博客說的比較詳細
</li> -
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) </ol>
我遇到的就這些,估計還有很多,只是我還沒遇到。
</li> </ul> 來自: http://cstdlib.com/tech/2015/05/17/http-and-web-server/
-