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

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

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

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

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