眾所周知,現在的服務器可以處理多個socket連接,背后並發的實現主要有兩種途徑。
- 多線程同步阻塞
- I/O多路復用
socket的建立
聊到socket,就不得不提到socket的建立的流程。祭出經典的老圖:
服務器依次使用socket,bind,listen之后就會監聽對應的地址,此時accept會一直阻塞直到有連接建立,如果客戶端和服務器建立了連接,那么accept就會返回一個連接句柄,可以對連接進行讀數據或者寫數據。
同步阻塞
那么問題來了,服務器如果不做特殊處理的話,一次只能處理一個連接,新的連接來是需要等待上一個連接結束才能連接成功,這就是最開始的服務器同步阻塞方法。
同步阻塞:進程發起IO系統調用后,進程被阻塞,轉到內核空間處理,整個IO處理完畢后返回進程。操作成功則進程獲取到數據。
多線程並發
可能以前的擁有的電腦人不多,這種方式一次只能連接一個倒也沒有問題,之后訪問的人開始多起來,設計者覺得這樣下去不行,就設計了多線程同步阻塞的方法。每次accept獲得一個句柄,就創建一個線程去處理連接,這下子就能同時處理多個連接呢。這就是經典的多線程同步阻塞的方法。
典型的多線程(進程)並發模型就是cgi。
特點
服務器和客戶端之間的並發,有以下特點:
- 外部連接很多,但很多連接是不活躍的連接,典型的如聊天的im系統。
- 少量的CPU消耗。
- 大部分的時間耗費在I/O阻塞和其他網絡服務。
- 外部網絡不穩定,客戶端收發數據慢很多。
- 對業務的請求處理很快,大部分時候毫秒級就可以完成。
問題
根據以上特點,我們可以得知服務器有以下問題:
如果采用多線程同步阻塞,1個tcp連接需要建立1個線程,10k個連接需要建立10k個連接,然而大部分連接是不活躍,即便是需要處理業務邏輯,也可以快速返回結果,大部分時間也是處於I/O阻塞或網絡等待。這就使得多個線程的創建很耗費資源,且線程的切換也是極其耗費CPU,這就很可能導致了CPU處理業務的消耗的資源不多,但是卻花了很多資源在進程切換上面。
I/O多路復用
多線程並發的問題是大部分socket都是閑置的狀態或者是處於IO阻塞的狀態,那能不能把阻塞的socket先扔到一邊去處理其他事情,來避免等待所帶來的資源耗費,也就是非阻塞IO的概念。
非阻塞IO
當用戶進程發出read操作時,如果kernel中的數據還沒有准備好,那么它並不會block用戶進程,而是立刻返回一個error。從用戶進程角度講 ,它發起一個read操作后,並不需要等待,而是馬上就得到了一個結果。用戶進程判斷結果是一個error時,它就知道數據還沒有准備好,於是它可以再次發送read操作。一旦kernel中的數據准備好了,並且又再次收到了用戶進程的system call,那么它馬上就將數據拷貝到了用戶內存,然后返回。
因此:使用非阻塞IO是需要不斷輪詢IO數據是否好了。
IO多路復用原理就是不斷輪詢多個socket,當其中的某個socket准備好了數據就返回,否則整個進程繼續阻塞,就可以讓一個進程在不太耗費資源的情況下處理多個連接,但是這個輪詢的操作是交給內核態去完成,也就避免了內核態和用戶態的切換的問題。
而目前的實現方法有select, poll, epoll,其中epoll的性能最好,用的也是最廣泛。
優點
- 避免了創建多個線程所耗費的資源以及時間。
- 對socket的輪詢是內核態的完成,不需要像多線程那樣切換需要耗費資源。
而epoll的實現可以做到性能幾乎不受連接數(單單是連接而沒有其他的操作)的影響。
當然多路復用IO也有自己的問題,也就是本身不支持多核的使用,需要另外解決多核的利用。
其中使用enroll的成熟程序有nginx,redis,nodej等。
服務器的發展
根據知乎大佬的介紹,服務器經過發展可以分為兩階段:
第一代服務器模型
把傳輸層的tcp並發的連接放到IO多路復用去處理,應用層繼續使用多線程並發模型去做。這樣就可以大幅度減少線程的創建切換的資源耗費。
如:nginx + php-fpm(其實是php-fpm是多進程)
第二代服務器模型
第二代服務器模型是把應用層也使用IO多路復用去處理,減少應用層的等待外部接口調用阻塞等待,一般是大廠大流量並發需要用到。
如:
- nodejs的異步回調
- Go的goroutine
參考資料:
[1]:許懷遠的知乎回答
[2]:Linux IO模式及 select、poll、epoll詳解