HTTP Server : 一個差生的逆襲
我剛畢業(yè)那會兒,國家還是包分配工作的,我的死黨張大胖被分配到了一個叫數(shù)據(jù)庫的大城市,天天都可以坐在高端大氣上檔次的機房里,在那里專門執(zhí)行SQL查詢優(yōu)化,工作穩(wěn)定又舒適。
隔壁宿舍的小白被送到了編譯器鎮(zhèn),在那里專門把C源文件編譯成EXE程序,雖然累,但是技術含量非常高,工資高,假期多。
我成績不太好,典型的差生,四級補考了兩次才過,被發(fā)配到了一個不知道什么名字的村莊,據(jù)說要處理什么HTTP請求,這個村莊其實就是一個破舊的電腦,令我欣慰的是可以上網(wǎng),時不時能和死黨們通個信什么的。
不過輔導員說了,我們都有光明的前途。
HTTP Server 1.0
HTTP是個新鮮的事物,能夠激起我一點點工作的興趣,不至于沉淪下去。
一上班,操作系統(tǒng)老大扔給我一大堆文檔:“這是HTTP協(xié)議,兩天看完!”
我這樣的英文水平, 這幾十頁的英文HTTP協(xié)議我不吃不喝不睡兩天也看不完,死豬不怕開水燙,慢慢磨吧。
兩個星期以后,我終于大概明白了這HTTP是怎么回事:無非是有些電腦上的瀏覽器向我這個破電腦發(fā)送一個預先定義好的文本(HTTP Request), 然后我這邊處理一下(通常是從硬盤上取一個后綴名是html的文件),然后再把這個文件通過文本方式發(fā)回去(HTTP Response),就這么簡單。
唯一麻煩的實現(xiàn),我得請操作系統(tǒng)給我建立HTTP層下面的TCP連接通道, 因為所有的文本數(shù)據(jù)都得通過這些TCP通道接收和發(fā)送,這個通道是用socket建立的。
弄明白了原理,我很快就搞出了第一版程序,這個程序長這個樣子:

(注:詳情參見文章《張大胖的socket》)
看看,這些socket, bind, listen , accept... 都是操作系統(tǒng)老大提供的接口,我能做的也就是把他們組裝起來:先在80端口監(jiān)聽,然后進入無限循環(huán),如果有連接請求來了,就接受(accept),創(chuàng)建新的socket, 最后才可以通過這個socket來接收,發(fā)送http數(shù)據(jù)。
老大給我的程序起了個名稱,Http Server, 版本1.0 。
這個名字聽起來挺高端的,我喜歡。
我興沖沖地拿來實驗, 程序啟動了,在80端口“蹲守”,過了一會兒就有連接請求了,趕緊Accept,建立新的socket,成功 !接下來就需要從socket 中讀取HTTP Request了。
可是這個receive 調用好慢,我足足等了100毫秒還沒有響應 !我被阻塞(block)住了!
操作系統(tǒng)老大說:“別急啊,我也在等著從網(wǎng)卡那里讀數(shù)據(jù),讀完以后就會復制給你。”
我樂得清閑,可以休息一下。
可是操作系統(tǒng)老大說:“別介啊,后邊還有很多瀏覽器要發(fā)起連接,你不能在這兒歇著啊?!?/p>
我說不歇著怎么辦?receive調用在你這里阻塞著,我除了加入阻塞隊列,讓出CPU讓別人用還能干什么?
老大說:“唉,大學里沒聽說過多進程嗎?你現(xiàn)在很明顯是單進程,一旦阻塞就完蛋了,想辦法用下多進程,每個進程處理一個請求!”
老大教訓的是,我忘了多進程并發(fā)編程了。
HTTP 2.0 :多進程
多進程的思路非常簡單,當accept連接以后,對于這個新的socket,不在主進程里處理,而是新創(chuàng)建子進程來接管。這樣主進程就不會阻塞在receive 上,可以繼續(xù)接受新的連接了。

我改寫了代碼,把HTTP server 升級為V2.0,這次運行順暢了很多,能并發(fā)地處理很多連接了。
這個時候Web 剛剛興起,我這個HTTP Server 訪問的人還不多,每分鐘也就那么幾十個連接發(fā)過來,我輕松應對。
由于是新鮮事物,我還有資本給搞數(shù)據(jù)庫的小明和做編譯的小白吹吹牛,告訴他們我可是網(wǎng)絡高手。
沒過幾年,Web迅速發(fā)展,我所在的破舊機器也不行了,換成了一個性能強悍的服務器,也搬到了四季如春的機房里。
現(xiàn)在每秒都有上百個連接請求了,有些連接持續(xù)的時間還相當?shù)瞄L,所以我經(jīng)常得創(chuàng)建成百上千的進程來處理他們,每個進程都得耗費大量的系統(tǒng)資源,很明顯操作系統(tǒng)老大已經(jīng)不堪重負了。
他說:“咱們不能這么干了,這么多進程,光是做進程切換就把我累死了?!?/p>
“要不對每個Socket連接我不用進程了,使用線程?”
“可能好一點,但我還是得切換線程啊,你想想辦法限制一下數(shù)量吧?!?/p>
我怎么限制?我只能說同一時刻,我只能支持x個連接,其他的連接只能排隊等待了。
這肯定不是一個好的辦法。
HTTP Server 3.0 : Select模型
老大說:“我們仔細合計合計,對我來說,一個Socket連接就是一個所謂的文件描述符(File Descriptor ,簡稱 fd ,是個整數(shù)),這個fd 背后是一個簡單的數(shù)據(jù)結構,但是我們用了一個非常重量級的東西‘進程’來表示對它的讀寫操作,有點浪費啊?!?/p>
我說:“要不咱們還切換回單進程模型?但是又會回到老路上去,一個receive 的阻塞就什么事都干不了了。”
“單進程也不是不可以,但是我們要改變一下工作方式?!?/p>
“改成什么?” 我猜不透老大在賣什么關子。
“你想想你阻塞的本質原因,還不是因為人家瀏覽器還沒有把數(shù)據(jù)發(fā)過來,我自然也沒法給你,而你又迫不及待地想去讀,我只好把你阻塞。在單進程情況下,一阻塞,別的事兒都干不了。“
“對,就是這樣?!?/p>
“所以你接受了客戶端連接以后,不能那么著急地去讀,咱們這么辦,你的每個socket fd 都有編號,你每次把一批socket的編號告訴我,就可以阻塞休息了?!?/p>

[注:實際上,HTTP Server和操作系統(tǒng)之間傳遞的并不是socket fd的編號,而是一個叫做fd_set的數(shù)據(jù)結構]
我問道:“這不和以前一樣嗎?原來是調用receive 時阻塞,現(xiàn)在還是阻塞?!?/p>
“聽我說完,我會在后臺檢查這些編號的socket,如果發(fā)現(xiàn)這些socket 可以讀寫,我會把對應的socket 做個標記,把你喚醒去處理這些socket 的數(shù)據(jù),你處理完了,再把你的那些socket fd 告訴我,再次進入阻塞,如此循環(huán)往復。”
我有點明白了:“這是我們倆的一種通信方式,我告訴你我要等待什么東西,然后阻塞, 如果事件發(fā)生了,你就把我喚醒,讓我做事情。”
“對,關鍵點是你等我的通知,我把你從阻塞狀態(tài)喚醒后,你一定要去遍歷一遍所有的socket fd(實際上就是那個fd_set的數(shù)據(jù)結構),看看誰有標記,有標記的做相應處理。我把這種方式叫做 select模型?!?/p>
我用select的方式改寫了HTTP server,拋棄了一個socket請求對于一個進程的模式, 現(xiàn)在我用一個進程就可以處理所有的socket了。
HTTP Server4.0 : epoll
這種稱為select的方式運行了一段時間,效果還不錯,我只管把socket fd 告訴老大,然后等著他通知我就行了。
有一次我無意中問老大:“我每次最多可以告訴你多少個socket fd?”
“1024個?!?/p>
“那就是說我一個進程最多只能監(jiān)控1024個socket了?”
“是的,你可以考慮多用幾個進程啊?!?/p>
這倒是一個辦法,不過“select”的方式用的多了,我就發(fā)現(xiàn)了弊端,最大的問題就是我需要把socket的編號(實際上是fd_set數(shù)據(jù)結構)不斷地復制給操作系統(tǒng)老大,這挺耗資源的,還有就是我從阻塞中恢復以后,需要遍歷這1000多個socket fd,看看有沒有標志位需要處理。
實際的情況是,很多socket 并不活躍, 在一段時間內瀏覽器并沒有數(shù)據(jù)發(fā)過來,這1000多個socket 可能只有那么幾十個需要真正的處理,但是我不得不查看所有的socket fd,這挺煩人的。
難道老大不能把那些發(fā)生了變化的socket 告訴我嗎?
我把這個想法給老大說了下,他說:“嗯,現(xiàn)在訪問量越來越大,select 方式已經(jīng)不滿足要求,我們需要與時俱進了,我想了一個新的方式,叫做epoll?!?/p>

“看到?jīng)]有,使用epoll和select 其實類似,” 老大接著說 :“不同的地方是,我只會告訴你那些可以讀寫的socket , 你呢只需要處理這些準備就緒的socket 就可以了。”
“看來老大想得很周全,這種方式對我來說就簡單得多了。”
我用epoll 把HTTP Server 再次升級,由于不需要遍歷全部集合,只需要處理那些有變化的、活躍的socket 文件描述符,系統(tǒng)的處理能力有了飛躍的提升。
我的HTTP Server 受到了廣泛的歡迎,全世界有無數(shù)人在使用,最后死黨數(shù)據(jù)庫小明也知道了,他問我:“大家都說你能輕松地支持好幾萬的并發(fā)連接,真是這樣嗎?”
我謙虛地說:“過獎,其實還得做系統(tǒng)的優(yōu)化啦。”
他說:“厲害啊,你小子走了狗屎運了啊?!?/p>
我回答:“畢業(yè)那會兒輔導員不是說過嗎,每個人都有光明的前途?!?/p>
后記:最近有幾個人問我select和epoll的事情,其實我?guī)啄昵皩戇^一篇文章的,只是有些小錯誤,今天整理一下再發(fā)一次。
如需轉載,請通過作者微信公眾號coderising獲取授權