socket模型


服務器程序,簡單的說就是接收網絡數據,處理后並返回結果數據。網絡模塊是其必不可少的部分,它本質上就是處理socket的五類事件:accept(客戶端接入),connect(連接上服務器),read,write和error。socket接口有兩種工作模式,一種是阻塞模式,一種是非阻塞模式。阻塞模式通常不會用,因為它有兩個弊端:一是會阻塞線程,要想處理多個連接就必須要一個連接一個線程,這樣線程開銷大且復雜度高;二是read時,如果對端鏈路異常了,協議棧無法正常得到關閉通知,那么就永遠阻塞住了。非阻塞模式現今有兩類:一類是同步事件分離器,以select,epoll為代表;一類是異步io,以iocp為代表。在非阻塞模式的實踐上,通常會使用reactor和proactor這兩種設計模式。


涉及的系統接口

 


 

reactor

reactor由資源、事件處理器、資源管理器、調度器和同步事件分離器構成。資源是抽象出的能產生某些事件的對象,資源的種類表示着reactor功能的數量,在本文特指socket。事件處理器是資源產生事件時執行的函數。資源管理器是資源和事件處理器的容器。調度器是一個過程,調用同步事件分離器檢測多個資源產生的事件,然后喚醒對應的事件處理器。同步事件分離器就是select,epoll等系統函數。reactor基於事件循環(調度器不斷執行),能以單線程同步檢測處理多路socket的事件。其輸入就是資源和事件處理器,其輸出為喚醒事件處理器。libev就是典型的實現。

其優點為:

  • 單線程處理多路socket,規范了一套處理流程。

其缺點為:

  • 使用者的知識度沒有減少,需要自己進行accept,read,write和錯誤判斷。
  • 假設為單線程環境,接口都不是線程安全的,在多線程項目中用起來不是很平滑。(與模式無關,libev等實現的缺陷)

proactor

它由資源、請求、請求完成處理器、資源管理器、調度器和異步處理器構成。資源同reactor。請求是對資源的實際操作,如read,write,accept,每個請求對應於一個異步處理器,請求不會立刻執行,而是先緩存在請求隊列中,由調度器統一處理。請求完成處理器是請求完成后的回調函數。資源管理器是資源、請求和請求完成處理器的容器。調度器也是一個過程,從資源管理器中取出請求,調用相應的異步處理器,在異步處理完畢后調用請求完成處理器。異步處理器為操作系統提供的異步操作函數,如iocp,或者用線程去模擬。它也是基於事件循環,能以單線程並發處理多路socket的事件。其輸入為資源和請求,其輸出為請求完成通知。libuv就是典型的實現。

其優點為:

  • 單線程處理多路socket,規范了一套處理流程。
  • 在操作系統支持真異步處理器時,有並發性。
  • 使用者所需知識度低,不需要了解實際的事件處理。

其缺點為:

  • 假設為單線程環境,接口都不是線程安全的,在多線程項目中用起來不是很平滑。(與模式無關,libuv等實現的缺陷)

 

reactor和proactor的區別


reactor側重的是告訴使用者a資源發生了b事件,怎么處理你看着辦吧。proactor側重的是告訴使用者,主人你對a資源進行的b請求完成了。以做軟件項目為例子,在reactor時,它就是老板,你就是程序員,某天它對你下達一個開發x軟件的任務,然后你就去調研需求,設計編碼;在proactor時,它就是程序員,你就是老板,某天你對它下達一個開發x軟件的任務,它開發完畢后告訴你,老板,x軟件開發完畢。

從實現細節來看,reactor就是缺少異步處理器,如果內部為每種事件提供一個異步處理器,將輸入的事件偵聽變為提交請求,那么reactor就變為proactor了。


 

更好的proactor

我希望所有的請求操作都是線程安全的,請求完成通知不是通過回調函數由內部發起,而是轉化為一條消息,由外部主動獲取。這樣能更好的整合到項目中。它的接口是這樣的:

 1 #ifndef SSOCKET_H  2 #define SSOCKET_H
 3 
 4 #define    SS_MSG_ACCEPT 1 //客戶端連接消息
 5 #define SS_MSG_OPEN 2 //連接服務器成功消息
 6 #define SS_MSG_DATA 3 //數據消息
 7 #define SS_MSG_CLOSE 4 //對方斷開消息
 8 #define SS_MSG_ERROR 5 //錯誤消息,發生時,socket已經斷開
 9 #define SS_MSG_FILE 6 //文件發送成功消息
10 
11 struct ss_message{ 12     int type; 13     int id; 14     void *ud; 15     int size; //在type = SS_MSG_ACCEPT時,size為客戶端的id;在SS_MSG_DATA時,size為data的大小 16 
17     //在SS_MSG_DATA時,data為數據;在SS_MSG_ACCEPT時,data為ip地址字符串;在SS_MSG_ERROR時,data為錯誤消息;在SS_MSG_FILE時,data為文件路徑; 18     //SS_MSG_DATA,SS_MSG_ACCEPT,SS_MSG_FILE,data需要外部釋放
19     char *data; 20 }; 21 
22 int ss_init(); 23 //獲取一條網絡消息,return >0有消息
24 int ss_poll(struct ss_message *m); 25 
26 //以下接口都是線程安全的
27 int ss_listen(const char *addr,int port,int bks,void *ud); 28 int ss_connect(const char *addr,int port,void *ud); 29 void ss_close(int id); 30 void ss_bind(int id,void *ud); 31 void ss_start(int id); 32 void ss_send(int id,char *data,int n); 33 int ss_send_file(int id,const char *path); 34 
35 #endif

socket句柄不暴露給外部,而是暴露內部的id,內部的id可以映射到實際的socket句柄,這樣做更安全,通過id,內部可以檢測該socket是否有效。

由外部循環調用ss_poll來獲取請求完成通知。

ss_bind可以為id綁定一個用戶數據,該id的請求完成消息里會帶上該用戶數據。

ss_start用於開始accept,read這些持續性請求,而不需要一次一次發起。

ss_send將要發送的數據送入id的發送隊列,內部會保證有序的發送出去。

ss_send_file:因為內部有發送隊列要緩存數據,所以並不適合文件這種大數據量的需求,該接口會使用sendfile要處理文件發送,從而避免緩存文件數據。

ss_close在關閉時會保證發送隊列已經全部發送,否則不會關閉socket.

具體的實現放在github上。


在服務器程序上使用


多線程的服務器程序通常是這樣的結構:

網關用於提供入口點,會收到所有的socket消息,在accept時會創建一個agent,此后該socket的消息就會發送到該agent。agent負責解碼網絡數據,分解出一個個協議包,然后調用協議包處理器處理該包,再將處理的結果編碼為協議包發送到該socket。因為proactor是單線程的,為了並發,一般有兩種方式:

  1. agent解碼出協議包后,用線程池來執行包處理邏輯。
  2. agent緩存住網絡數據,由線程池來調度agent,以一定的頻率來進行解碼、包處理。

網關和agent的作用是隔離網絡層,進行網絡協議和內部協議的轉換。agent除了轉換協議,還可以保存中間狀態,在有狀態的協議中,agent和socket是一一配對的。在無狀態的協議中,所有的socket可以共用一個agent。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM