《unix網絡編程》第一卷中將客戶服務器程序設計方法講得透徹,這篇文章將其中編碼的細節略去,經過僞代碼的形式展示,主要介紹各類方法的思想;算法
示例是一個經典的TCP回射程序:
客戶端發起鏈接請求,鏈接後發送一串數據;收到服務端的數據後輸出到終端;
服務端收到客戶端的數據後原樣回寫給客戶端;編程
客戶端僞代碼:數組
sockfd = socket(AF_INET,SOCK_STREAM,0); //與服務端創建鏈接 connect(sockfd); //鏈接創建後從終端讀入數據併發送到服務端; //從服務端收到數據後回寫到終端 while(fgets(sendline,MAXLINE,fileHandler)!= NULL){ writen(sockfd,sendline,strlen(sendline)); if(readline(sockfd,recvline,MAXLINE) == 0){ cout << "recive over!"; } fputs(recvline,stdout); }
下面介紹服務端程序處理多個客戶請求的開發範式;服務器
對於多個客戶請求,服務器端採用fork的方式建立新進程來處理;網絡
處理流程:
1. 主進程綁定ip端口後,使用accept()等待新客戶的請求;
2. 每個新的用戶請求到來,都建立一個新的子進程來處理具體的客戶請求;
3. 子進程處理完用戶請求,結束本進程;多線程
服務端僞代碼:併發
listenFd = socket(AF_INET,SOCK_STREAM,0); bind(listenFd,addR); listen(listenFD); while(true){ //服務器端在這裏阻塞等待新客戶鏈接 connfd = accept(listenfd); if( fork() ==0){//子進程 close(listenfd); while(n=read(connfd,buf,MAXLINE)>0){ writen(connfd,buf); } } close(connfd); }
這種方法開發簡單,但對操做系統而言,進程是一種昂貴的資源,對於每一個新客戶請求都使用一個進程處理,開銷較大;
對於客戶請求數很少的應用適用這種方法;socket
上一種方法中,每來一個客戶都建立一個進程處理請求,完畢後再釋放;
不間斷的建立和結束進程浪費系統資源;
使用進程池預先分配進程,經過進程複用,減小進程重複建立帶來的系統消耗和時間等待;函數
優勢:消除新客戶請求到達來建立進程的開銷;
缺點:須要預先估算客戶請求的多少(肯定進程池的大小)性能
源自Berkeley內核的系統,有如下特性:
派生的全部子進程各自調用accep()監聽同一個套接字,在沒有用戶請求時都進入睡眠;
當有新客戶請求到來時,全部的客戶都被喚醒;內核從中選擇一個進程處理請求,剩餘的進程再次轉入睡眠(回到進程池);
利用這個特性能夠由操做系統來控制進程的分配;
內核調度算法會把各個鏈接請求均勻的分散到各個進程中;
處理流程:
1. 主進程預先分配進程池,全部子進程阻塞在accept()調用上;
2. 新用戶請求到來,操做系統喚醒全部的阻塞在accpet上的進程,從其中選擇一個創建鏈接;
3. 被選中的子進程處理用戶請求,其它子進程回到睡眠;
4. 子進程處理完畢,再次阻塞在accept上;
服務端僞代碼:
listenFd = socket(AF_INET,SOCK_STREAM,0); bind(listenFd,addR); listen(listenFD); for(int i = 0;i< children;i++){ if(fork() == 0){//子進程 while(true){ //全部子進程監聽同一個套接字,等待用戶請求 int connfd = accept(listenfd); close(listenfd); //鏈接創建後處理用戶請求,完畢後關閉鏈接 while(n=read(connfd,buf,MAXLINE)>0){ writen(connfd,buf); } close(connfd); } } }
如何從進程池中取出進程?
全部的進程都經過accept()阻塞等待,等鏈接請求到來後,由內核從全部等待的進程中選擇一個進程處理;
處理完的進程,如何放回到池子中?
子進程處理完客戶請求後,經過無限循環,再次阻塞在accpet()上等待新的鏈接請求;
注意: 多個進程accept()阻塞會產生「驚羣問題」:儘管只有一個進程將得到鏈接,可是全部的進程都被喚醒;這種每次有一個鏈接準備好卻喚醒太多進程的作法會致使性能受損;
上述不上鎖的實現存在移植性的問題(只能在源自Berkeley的內核系統上)和驚羣問題,
更爲通用的作法是對accept上鎖;即避免讓多個進程阻塞在accpet調用上,而是都阻塞在獲取鎖的函數中;
服務端僞代碼:
listenFd = socket(AF_INET,SOCK_STREAM,0); bind(listenFd,addR); listen(listenFD); for(int i = 0;i< children;i++){ if(fork() == 0){ while(true){ my_lock_wait();//獲取鎖 int connfd = accept(listenfd); my_lock_release();//釋放鎖 close(listenfd); while(n=read(connfd,buf,MAXLINE)>0){ writen(connfd,buf); } close(connfd); } } }
上鎖可使用文件上鎖,線程上鎖;
- 文件上鎖的方式可移植到全部的操做系統,但其涉及到文件系統操做,可能比較耗時;
- 線程上鎖的方式不只適用不一樣線程之間的上鎖,也適用於不一樣進程間的上鎖;
關於上鎖的編碼細節詳見《網絡編程》第30章;
與上面的每一個進程各自accept接收監聽請求不一樣,這個方法是在父進程中統一接收accpet()用戶請求,在鏈接創建後,將鏈接描述符傳遞給子進程;
處理流程:
1. 主進程阻塞在accpet上等待用戶請求,全部子進程不斷輪詢探查是否有可用的描述符;
2. 有新用戶請求到來,主進程accpet創建鏈接後,從進程池中取出一個進程,經過字節流管道將鏈接描述符傳遞給子進程;
3. 子進程收到鏈接描述符,處理用戶請求,處理完成後向父進程發送一個字節的內容(無實際意義),告知父進程我任務已完成;
4. 父進程收到子進程的單字節數據,將子進程放回到進程池;
服務端僞代碼:
listenFd = socket(AF_INET,SOCK_STREAM,0); bind(listenFd,addR); listen(listenFD); //預先創建子進程池 for(int i = 0;i< children;i++){ //使用Unix域套接字建立一個字節流管道,用來傳遞描述符 socketpair(AF_LOCAL,SOCK_STREAM,0,sockfd); if(fork() == 0){//預先建立子進程 //子進程字節流到父進程 dup2(sockfd[1],STDERR_FILENO); close(listenfd); while(true){ //收到鏈接描述符 if(read_fd(STDERR_FILENO,&connfd) ==0){; continue; } while(n=read(connfd,buf,MAXLINE)>0){ //處理用戶請求 writen(connfd,buf); } close(connfd); //通知父進程處理完畢,本進程能夠回到進程池 write(STDERR_FILENO,"",1); } } } while(true){ //監聽listen套接字描述符和全部子進程的描述符 select(maxfd+1,&rset,NULL,NULL,NULL); if(FD_ISSET(listenfd,&rset){//有客戶鏈接請求 connfd = accept(listenfd);//接收客戶鏈接 //從進程池中找到一個空閒的子進程 for(int i = 0 ;i < children;i++){ if(child_status[i] == 0) break; } child_status[i] = 1;//子進程從進程池中分配出去 write_fd(childfd[i],connfd);//將描述符傳遞到子進程中 close(connfd); } //檢查子進程的描述符,有數據,代表已經子進程請求已處理完成,回收到進程池 for(int i = 0 ;i < children;i++){ if(FD_ISSET(childfd[i],&rset)){ if(read(childfd[i])>0){ child_status[i] = 0; } } } }
爲每一個用戶建立一個線程,這種方法比爲每一個用戶建立一個進程要快出許多倍;
處理流程:
1. 主線程阻塞在accpet上等待用請求;
2. 有新用戶請求時,主線程創建鏈接,而後建立一個新的線程,將鏈接描述符傳遞過去;
3. 子線程處理用戶請求,完畢後線程結束;
服務端僞代碼:
listenFd = socket(AF_INET,SOCK_STREAM,0); bind(listenFd,addR); listen(listenFD); while(true){ connfd = accept(listenfd); //鏈接創建後,建立新線程處理具體的用戶請求 pthread_create(&tid,NULL,&do_function,(void*)connfd); close(connfd); } -------------------- //具體的用戶請求處理函數(子線程主體) void * do_function(void * connfd){ pthread_detach(pthread_self()); while(n=read(connfd,buf,MAXLINE)>0){ writen(connfd,buf); close((int)connfd); }
處理流程:
1. 主線程預先建立線程池,第一個建立的子線程獲取到鎖,阻塞在accept()上,其它子線程阻塞在線程鎖上;
2. 用戶請求到來,第一個子線程創建鏈接後釋放鎖,而後處理用戶請求;完成後進入線程池,等待獲取鎖;
3. 第一個子線程釋放鎖以後,線程池中等待的線程有一個會獲取到鎖,阻塞在accept()等待用戶請求;
listenFd = socket(AF_INET,SOCK_STREAM,0); bind(listenFd,addR); listen(listenFD); //預先建立線程池,將監聽描述符傳給每一個新建立的線程 for(int i = 0 ;i <threadnum;i++){ pthread_create(&tid[i],NULL,&thread_function,(void*)connfd); } -------------------- //具體的用戶請求處理 //經過鎖保證任什麼時候刻只有一個線程阻塞在accept上等待新用戶的到來;其它的線程都 //在等鎖; void * thread_function(void * connfd){ while(true){ pthread_mutex_lock(&mlock); // 線程上鎖 connfd = accept(listenfd); pthread_mutex_unlock(&mlock);//線程解鎖 while(n=read(connfd,buf,MAXLINE)>0){ writen(connfd,buf); close(connfd); } }
使用源自Berkeley的內核的Unix系統時,咱們沒必要爲調用accept而上鎖,
去掉上鎖的兩個步驟後,咱們發現沒有上鎖的用戶時間減小(由於上鎖是在用戶空間中執行的線程函數完成的),而系統時間卻增長不少(每個accept到達,全部的線程都變喚醒,引起內核的驚羣問題,這個是在線程內核空間中完成的);
而咱們的線程都須要互斥,讓內核執行派遣還不讓本身經過上鎖來得快;
這裏沒有必要使用文件上鎖,由於單個進程中的多個線程,老是能夠經過線程互斥鎖來達到一樣目的;(文件鎖更慢)
處理流程:
激活條件等待的方式有兩種:pthread_cond_signal()激活一個等待該條件的線程,存在多個等待線程時按入隊順序激活其中一個;而pthread_cond_broadcast()則激活全部等待線程。
注:通常應用中條件變量須要和互斥鎖一同使用;
在調用pthread_cond_wait()前必須由本線程加鎖(pthread_mutex_lock()),而在更新條件等待隊列之前,mutex保持鎖定狀態,並在線程掛起進入等待前解鎖。在條件知足從而離開pthread_cond_wait()以前,mutex將被從新加鎖,以與進入pthread_cond_wait()前的加鎖動做對應。
服務端僞代碼:
listenFd = socket(AF_INET,SOCK_STREAM,0); bind(listenFd,addR); listen(listenFD); for(int i = 0 ;i <threadnum;i++){ pthread_create(&tid[i],NULL,&thread_function,(void*)connfd); } while(true){ connfd = accept(listenfd); pthread_mutex_lock(&mlock); // 線程上鎖 childfd[iput] = connfd;//將描述符的句柄放到數組中傳給獲取到鎖的線程; if(++iput == MAX_THREAD_NUM) iput= 0; if(iput == iget) err_quit("thread num not enuough!"); pthread_cond_signal(&clifd_cond);//發信號,喚醒一個睡眠線程(輪詢喚醒其中的一個) pthread_mutex_unlock(&mlock);//線程解鎖 } -------------------- void * thread_function(void * connfd){ while(true){ pthread_mutex_lock(&mlock); // 線程上鎖 //當無沒有收到鏈接句柄時,睡眠在條件變量上,並釋放mlock鎖 //知足條件被喚醒後,從新加mlock鎖 while(iget == iput) pthread_cond_wait(&clifd_cond,&mlock); connfd = childfd[iget]; if(++iget == MAX_THREAD_NUM) iget = 0; pthread_mutex_unlock(&mlock);//線程解鎖 //處理用戶請求 while(n=read(connfd,buf,MAXLINE)>0){ writen(connfd,buf); close(connfd); } }
測試代表這個版本的服務器要慢於每一個線程各自accpet的版本,緣由在於這個版本同時須要互斥鎖和條件變量,而上一個版本只須要互斥鎖;
線程描述符的傳遞和進程描述符的傳遞的區別?
在一個進程中打開的描述符對該進程中的全部線程都是可見的,引用計數也就是1;
全部線程訪問這個描述符都只須要經過一個描述符的值(整型)訪問;
而進程間的描述符傳遞,傳遞的是描述符的引用;(比如一個文件被2個進程打開,相應的這個文件的描述符引用計數增長2);
《unix網絡編程》第一卷 套接字聯網API
Posted by: 大CC
博客:blog.me115.com
微博:新浪微博