本文是自己學習經驗總結,有不正確的地方,請批評指正。
關於程序和執行緒的網路程式設計模型,在unp卷1的第30章,有詳細的介紹。我這裡,在tiny基礎上,實現了以下幾種:
其中,fdbuffer是指主線程accept得到已連線描述符後,存放進fdbuffer緩衝區,其他執行緒再去處理。
signal(sigpipe,sig_ign);//忽略sigpipe,見unp 5.13
signal(sigchld,sigchld_hander);//**子程序
while (1)
close(connfd);
}void sigchld_hander(int sig)
return;
}
上述**是最簡單的併發模型,每乙個連線,都會新fork乙個程序去處理,顯然這種方式併發程度低。對於初學者,還是有幾個需要注意的地方。
訊號處理,sigpipe(向已經關閉的fd寫資料)缺省會終止程序,這裡忽略它。sigchld_hander用於函式**子程序(注意訊號不排隊問題)。
注意父子程序都需要關閉已連線描述符connfd,到客戶端的連線才會最終關閉
while (1)
void *thread(void *vargp)
多執行緒與多程序基本一致,需要注意的地方:
執行緒是可結合或者是分離的,區別在於分離式執行緒的儲存器資源在它自己終止的時候由系統自動釋放,而可結合執行緒需要其他執行緒**,此處的pthread_detach是將當前執行緒分離。
為每乙個客戶都建立乙個新的執行緒,顯然不是高效的做法,我們可以預先建立執行緒,主線程和其它執行緒通過乙個緩衝區傳遞描述符,或者可以每個執行緒自己accept。
訊號量同步
int i;
for(i=0;i互斥鎖和條件變數同步void sbuf_insert(sbuf_t *sp, int item)
sp->buf[(++sp->rear)%(sp->n)] = item;
sp->nslots--;
pthread_mutex_unlock(&sp->buf_mutex);
int dosignal = 0;
pthread_mutex_lock(&sp->nready_mutex);
if(sp->nready == 0)
dosignal = 1;
sp->nready++;
pthread_mutex_unlock(&sp->nready_mutex)
if(dosignal)
pthread_cond_signal(&sp->cond);
}int sbuf_remove(sbuf_t *sp)
這個版本,主函式與版本3一致,緩衝區的同步我改用了互斥鎖和條件變數。這裡貼出sbuf insert和remove操作的實現。其中sbuf_t結構體中,nready和nslots分別指準備好待消費的描述符和緩衝區剩餘的空位。
在這裡,為什麼在需要兩個同步變數nready和nslots對應兩個互斥鎖?任意使用其中乙個,當nslots小於n,或者nready大於零的時候,喚醒等待在條件變數上的執行緒,這樣只需用乙個同步變數,詳見原始碼。我自己測試了一下,兩種方式效率是差不多的。
我的理解是,當使用兩個同步變數時,生產者在放入產品的時候,不阻塞消費者消費其他產品,因為沒有對nready加鎖,所以如果第乙個階段(放入產品)耗時比較多時,用兩個同步變數更合適一些。而這裡,放入產品並不是耗時操作,因此效率差不多。
還有乙個需要注意的地方是,我把pthread_cond_signal放到了mutex外面,是為了避免上鎖衝突,見unp卷2 7.5。
執行緒各自accept
int i;
for(i=0;i這個版本是預先建立固定數量的,但是由執行緒各自去accept, 對accept上鎖保護。這種方式顯然**實現上容易得多,在效率上,由於只用到了pthread_mutex_lock這一種系統呼叫,效率應該要稍微好一點。
以上就是我實現過的基於程序、執行緒,主要是執行緒的併發模式。程序另外還有幾種模式,我認為那幾種模式和執行緒基本一致,**寫起來也比較類似。實際上,在linux下,執行緒其實就是資源共享的程序,都有自己的task_struct結構(《linux核心設計與實現》)。
在這一部分,主要介紹linux下面,select、poll和epoll的用法和示例。一共下面三個程式:
typedef struct
pool;
static pool client_pool;
init_pool(listenfd,&client_pool);
while(1)
/*mask sigchld!!!!! but some signal will be abondoned
*/ //client_pool.nready = pselect(client_pool.maxfd+1,&client_pool.ready_set,null,null,null,&sig_chld);
if(fd_isset(listenfd,&client_pool.ready_set))
check_clients(&client_pool);
}
在呼叫select函式的地方,用while的原因是,子程序中斷的sigchld的訊號會導致select返回,需要手動重啟。最開始我用了pselect來解決這個問題,但這樣會造成訊號的丟失。
在每次呼叫select之前,需要對ready_set重新賦值,select返回之後,ready_set會被修改,在下次呼叫之前需要恢復。
再看一下這個client_pool的實現:
void init_pool(int listenfd, pool *p)
void add_client(int connfd, pool *p)
}}void check_clients(pool *p)
}}
init_pool初始化,最開始的fd_set裡面只有listenfd,add_client將已連線描述符新增到clientfd,並將read_set置位。check_clients是迴圈依次檢查是哪乙個已連線fd,處理完畢後,將fd從clientfd和read_set中移除。
從上面的過程中,可以看出select有幾個明顯的缺點:
struct pollfd
typedef struct pool;
static pool client_pool;
init_pool(listenfd, &client_pool);
while (1)
if (client_pool.client[0].revents & pollrdnorm)
check_clients(&client_pool);
}
poll的**,基本與select模式一致。poll不同的地方在於它使用pollfd結構來表示fdset,而不是位圖。沒有大小限制,使用起來更為方便。events和revents分別表示需要檢測的事件和發生的事件。poll傳入的client指標,底層應該是所有的pollfd構成的乙個鍊錶。
poll和select的缺點一樣,都需要拷貝和輪詢,隨著fd數量的增大,效率都會大大降低。
typedef struct request_bufferrequest_b
struct epoll_event event; // event to register
event.data.ptr = (void *)request;
event.events = epollin | epollet;
epoll_ctl(epfd, epoll_ctl_add, listenfd, &event);
while (1)
for (int i = 0; i < n; i++) else if (rb->fd == listenfd) else
}make_socket_non_blocking(infd);
if (verbose) printf("the new connection fd :%d\n", infd);
request_b *request = (request_b *)malloc(sizeof(request_b));
request_init(request, infd, epfd);
event.data.ptr = (void *)request;
event.events = epollin | epollet | epolloneshot;
epoll_ctl(epfd, epoll_ctl_add, infd, &event);}}
else
}}
epoll有et和lt兩種模式,詳細定義見man手冊。et模式能夠減少事件觸發的次數,但是**複雜度會增加,io操作必須要等到eagain,容易漏掉事件。而lt模式,事件觸發的次數多一些,**實現上要簡單一點,不容易出錯。目前沒有明確的結論哪種模式更高效,應該是看具體的場景。我這裡使用了et模式,在我的doit_nonblock函式裡面,對於請求結束的判斷還有錯誤,不能夠正確判斷乙個完整的http請求。
前面說到select的缺點,epoll是怎麼解決的呢?
在實際使用中,肯定不會單純的用上面的某一種模式,而是多程序+io multiplexing 或者 多執行緒 + io multiplexing。後面再結合具體的例子,我會再繼續研究。原本,我寫了乙個小的測試程式,但是發現我的測試方法不是十分合理,沒有太大意義,就沒有放出來了,有興趣的可以看一看原始碼裡面。
網路程式設計 客戶 伺服器程式設計正規化
迭代tcp伺服器總是在完全處理某個客戶的請求之後才開始下乙個客戶的請求處理。這樣的伺服器實際中比較少見。基於udp的大多伺服器卻是這樣實現。傳統併發伺服器呼叫fork派生乙個子程序來處理每個客戶,這使得伺服器能夠同時為多個客戶服務,每個程序乙個客戶。客戶數目的唯一限制是作業系統對其能夠同時擁有多少子...
網路程式設計 基本函式
位元組排序函式 include 返回網路位元組序的值 uint16 t htons uint16 t host16bitvalue uint32 t htonl uint32 t host32bitvalue 返回主機位元組序的值 uint16 t ntohs uint16 t net16bitva...
udp基本網路程式設計
udp傳輸資料不需要像tcp一樣建立連線,只需要知道客戶端和伺服器的ip位址即可。首先是伺服器端,初始化套接字結構位址,建立套接字,繫結埠,迴圈監聽。include include 基本標頭檔案 include socket include struct sockaddr in include st...