天天看點

Socket程式設計模式了解與對比

本文主要分析了幾種Socket程式設計的模式。主要包括基本的阻塞Socket、非阻塞Socket、I/O多路複用。其中,阻塞和非阻塞是相對于套接字來說的,而其他的模式本質上來說是基于Socket的并發模式。I/O多路複用又主要分析了分析linux和windows下的常用模型。最後,比較這幾種Socket程式設計模式的優缺點,并讨論多線程與Socket的組合使用和伺服器開發的常用模式。

阻塞模式是最基本的Socket程式設計模式,在各種關于網絡程式設計的書籍中都是入門的例子。就像其名所說,阻塞模式的Socket會阻塞目前的線程,直到結果傳回,否則會一直等待。

非阻塞模式是相對阻塞模式來說,Socket并不會阻塞目前線程,非阻塞模式不會等到結果傳回,而會立即運作下去。

這裡需要注意,阻塞/非阻塞、同步/異步之前的差別。在本質上它們是不同的。同步和異步是相對操作結果來說,會不會等待結果結果傳回。而阻塞和非阻塞是相對線程是否被阻塞來說的。其實,這兩者存在本質的差別,它們的修飾對象是不同的。阻塞和非阻塞是指程序通路的資料如果尚未就緒,程序是否需要等待,簡單說這相當于函數内部的實作差別,也就是未就緒時是直接傳回還是等待就緒。而同步和異步是指通路資料的機制,同步一般指主動請求并等待I/O操作完畢的方式,當資料就緒後在讀寫的時候必須阻塞,異步則指主動請求資料後便可以繼續處理其它任務,随後等待I/O,操作完畢的通知,這可以使程序在資料讀寫時也不阻塞。因為兩者在表現上經常相同,是以經常被混淆。

I/O多路複用是一種并發伺服器開發技術(處理多個用戶端的連接配接)。通過該技術,系統核心緩沖I/O資料,當某個I/O準備好後,系統通知應用程式該I/O可讀或可寫,這樣應用程式可以馬上完成相應的I/O操作,而不需要等待系統完成相應I/O操作,進而應用程式不必因等待I/O操作而阻塞。

在linux下主要有select、poll、epoll三種模型,在freeBSD下則有kqueue,windwos下select、事件選擇模型、重疊I/O和完成端口等。

select本質是通過設定或檢查存放fd标志位的資料結構來進行下一步的處理。select是采用輪詢fd集合來進行處理的。

但是,select存在一定的缺陷。單個程序可監視的fd數量被限制,linux下一般為1024。雖然是可以修改的,但是總是有限制的。在每次調用select時,都需要把fd集合從使用者态拷貝到核心态,而且需要循環整個fd集合,這個開銷很多時候是比較大的。

poll的實作和select非常相似,本質上是相同,隻是描述fd集合的方式不同。poll是基于連結清單來存儲的。這雖然沒有了最大連接配接數的限制,但是仍然還有fd集合拷貝和循環帶來的開銷。而且poll還有一個特點是水準觸發,核心通知了fd後,沒有被處理,那麼核心就會不斷的通知,直到被處理。

epoll是對select和poll的改進。相較于poll,epoll使用“事件”的就緒通知,通過epoll_ctl注冊fd,一旦該fd就緒,核心就會采用類似callback的回調機制來激活該fd,把就緒fd放入就緒連結清單中,并喚醒在epoll_wait中進入睡眠的程序,這樣不在需要輪詢,判斷fd合計合集是否為空。而且epoll不僅支援水準觸發,還支援邊緣觸發。邊緣觸發是指核心通知fd之後,不管處不處理都不在通知了。在存儲fd的集合上,epoll也采用了更為優秀的mmap,而且會保證fd集合拷貝隻會發生一次。

事件選擇模型是基于消息的。它允許程式通過Socket,接收以事件為基礎的網絡事件通知。

重疊I/O模型是異步I/O模型。重疊模型的核心是一個重疊資料結構。重疊模型是讓應用程式使用重疊資料結構(WSAOVERLAPPED),一次投遞一個或多個Winsock I/O請求。若想以重疊方式使用檔案,必須用FILE_FLAG_OVERLAPPED 标志打開它。當I/O操作完成後,系統通知應用程式。利用重疊I/O模型,應用程式在調用I/O函數之後,隻需要等待I/O操作完成的消息即可。

IOCP完成端口是目前Windows下性能最好的I/O模型,當然也是最複雜的。簡單的說,IOCP 是一種高性能的I/O模型,是一種應用程式使用線程池處理異步I/O請求的機制。IOCP将所有使用者的請求都投遞到一個消息隊列中去,然後線程池中的線程逐一從消息隊列中去取出消息并加以處理,就可以避免針對每一個I/O請求都開線程。不僅減少了線程的資源,也提高了線程的使用率。

在以上I/O複用模型的讨論中,其實都含有線程的使用。重疊I/O和I/O完成端口都是利用了線程。這也可以看出在高并發伺服器的開發中,采用線程也是十分必要的。在I/O完成端口的使用中,還會使用到線程池,這也是現在應用十分廣泛的。通過線程池,可以降低頻繁建立線程帶來的開銷。

在Windows下一般使用windows提供I/O模型就足夠應付很多場景。但是,在linux下I/O模型都是和線程不相關的。有時為了更高的性能,也會采取線程池和I/O複用模型結合使用。比如許多Linux服務端程式就采用epoll和線程池結合的形式,當然引入線程也帶來了更多的複雜度,需要注意線程的控制和性能開銷(線程的主要開銷線上程的切換上)。而epoll本來也足夠優秀,是以僅用epoll也是可以的,像libevent這種著名的網絡庫也是采用epoll實作的。當然,在linux下也有隻使用多程序或多線程來達到并發的。這樣會帶來一定缺點,程式需要維護大量的Scoket。在服務端開發中使用線程,也要勁量保證無鎖,鎖也是很高的開銷的。