60.Linux/Unix 系統程式設計手冊(下) -- SOCKET: 伺服器設計
阿新 • • 發佈:2018-12-19
1.迭代型和併發型伺服器 迭代型:伺服器每次只處理一個客戶端,只有當完全處理完一個客戶端的請求之後才去處理下一個客戶端 併發型:這種型別的伺服器被設計為能夠同時處理多個客戶端的請求 併發型伺服器的其他設計方案: 1.在伺服器上預先建立程序或執行緒,其核心理念如下: 1.伺服器程式在啟動階段就立即預先建立好一定數量的子程序(或執行緒),而不是針對每個客戶端來建立一個新的子程序(或執行緒)。 這些子程序構成了一個服務池 2.服務池中的每個子程序一次只處理一個客戶端。在處理完客戶端請求後,子程序並不會終止,而是獲取下一個待處理的客戶端繼續處理。 採用上述技術需要在伺服器應用中仔細的管理子程序。服務池應該足夠大,以確保能充分響應客戶端的請求。這意味著伺服器父程序必須對未佔用 的子程序加以監視,並且在伺服器處於負載高峰時增加服務池的大小,這樣就總會有足夠的子程序存在。如果負載下降了,那麼應該響應的減少服務池 的大小,因為過多的空餘程序會降低系統的整體效能。 此外,服務池中的子程序必須遵循某些協議,使得它們能夠以獨佔的方式選擇一個客戶端連線。在大多數 Unix 實現中(包括Linux),讓服務池中的 每個子程序在監聽描述符的 accept() 呼叫上阻塞就足夠了。換句話說,伺服器父程序在建立任何子程序之前先建立監聽套接字,然後每個子程序在 fork() 呼叫中繼承該套接字的檔案描述符。當一個新的客戶端連線到來時,只有其中一個子程序能夠完成 accept() 呼叫。但是,由於 accpet() 在一些老式的實現 中不是一個原子化的系統呼叫,因此可能需要通過一些互斥技術,如檔案鎖等來支援,以確保每次只有一個子程序可以執行 accept() 呼叫。 還有其他方法可以讓服務池中所有的子程序都執行 accept() 呼叫。如果服務池由分離的程序組成,伺服器父程序可以執行 accept() 呼叫,然後將代表新 連線的檔案描述符傳遞給空閒的程序之一。如果服務池由執行緒組成,主執行緒可以執行 accept() 呼叫,然後通知服務池上的空閒執行緒,有新的已連線上的客戶端 正在等待處理。 2.在單個程序中處理多個客戶端 在某些情況下,我們可以設計讓單個伺服器程序來處理多個客戶端。為了實現這一點,我們必須採用一種能夠允許單個程序同時監視多個檔案描述符上IO 事件的IO模型(IO多路複用,訊號驅動IO 或者 epoll()) 在設計單程序伺服器時,伺服器程序必須做一些通常由核心來處理的排程任務。在每個客戶端一個伺服器程序的解決方案中,我們可以依靠核心來確保每個 伺服器程序能夠公平的訪問到伺服器主機的資源。但當我們用單個伺服器程序來處理多個客戶端時,伺服器程序必須自行確保一個或多個客戶端不會霸佔伺服器, 從而使其他客戶端處於飢餓狀態。 3.採用伺服器叢集 其他用來處理高客戶端負載的方法還包括使用多個伺服器系統---伺服器叢集(server farm) 構建伺服器叢集最簡單的方法就是 --- DNS 輪轉負載均衡,一個地區的域名權威伺服器將同一個域名對映到多個 IP 地址上。DNS 輪詢的優勢是,成本低, 而且容易實施。問題是,遠端 DNS 伺服器上所執行的快取操作,這意味著今後位於某個特定主機上的客戶端發出的請求會繞過迴圈輪轉的 DNS 伺服器,並總是 由一個伺服器來負責處理。此外,DNS 輪詢並沒有任何內建的用來確保到達良好負載均衡或者是高可用性的機制。另外一個需要我們考慮的是伺服器的親和性。這 就是說,確保一個客戶端的請求序列能夠全部定向到同一臺伺服器,這樣由伺服器維護的任何有關客戶端狀態的資訊都能夠保持準確。 一個更靈活的解決方案是伺服器負載均衡。在這種場景下,由一臺負載均衡伺服器將客戶端請求路由到伺服器叢集中的一個成員上。(為了確保高可用,可能還會 有一臺備用伺服器。一旦負載均衡主伺服器崩潰,備用伺服器就立刻接管主伺服器的任務)。這消除了遠端 DNS 快取引起的問題,因為伺服器叢集只對外提供了一 個單獨的IP地址(也就是負載均衡伺服器的IP地址)。負載均衡伺服器結合一些演算法來衡量或計算伺服器的負載,並智慧的將負載分發到伺服器叢集的各個成員上。 負載均衡器也會自動檢測叢集中失效的成員。最後,負載均衡伺服器還可能提供對伺服器的親和力。 2.inetd 守護程序 inetd 被設計用來消除大量非常用伺服器程序的需要,inetd 提供了2個好處: 1.與其為每個服務執行一個單獨的守護程序,現在只需要一個程序--- inetd 守護程序,就可以監聽一組指定的套接字介面,並按照需要啟動其他的服務。因此,可以 降低系統上執行的程序數量。 2.inetd 簡化了啟動其他服務的程式設計工作。 inetd 守護程序所做的操作:通常在系統啟動時執行 1.對於配置在 /etc/inetd.conf 中指定的每項服務,inetd 都會建立恰當型別的套接字,然後繫結到指定的埠上。此外,每個 tcp 套接字都會通過 listen() 呼叫允許客戶端發來連線。 2.通過 select() 呼叫, inetd 對前一步中建立的所有套接字進行監視,看是否有資料報或者連線請求傳送過來。 3.select() 進入阻塞狀態,直到一個 udp 資料報可讀或者 tcp 套接字上接收到了連線請求。在 tcp 連線中,inetd 在進入下一個步驟之前會先為連線執行 accept() 呼叫。 4.要啟動這個套接字上指定的服務,inetd 呼叫 fork() 建立一個新的程序,然後通過 exec() 啟動伺服器程式。在執行 exec() 之前,子程序執行如下的步驟: 1.除了用於 udp 資料報和接受 tcp 連線的檔案描述符之外,將其他所有從父程序繼承而來的檔案描述符都關閉 2.在檔案描述符 0,1,2上覆制套接字檔案描述符,並關閉套接字檔案描述符本身。完成這一步之後,啟動的伺服器程序就能夠通過這3個標準的檔案描述符同套接字通訊了 3.這一步是可選的,為啟動的伺服器程序設定使用者和組 ID, 設定的值可在 /etc/inetd.conf 中設定。 5.在第3步中,如果在 tcp 套接字上接受了一個連線,inetd 就關閉這個連線套接字 6.inetd 服務跳回第2步繼續執行。