1、迭代型和并發型服務器
對于使用 socket 的網絡服務器程序,有兩種常見的設計方式:
迭代型:服務器每次只處理一個客戶端,只有當完全處理完一個客戶端的請求后才去處理下一個客戶端
并發型:能夠同時處理多個客戶端的請求
1.1、代型 UDP echo 服務器
server
int?main(int?argc,char*?argv[]) { ????int?sfd; ????ssize_t?numRead; ????socklen_t?addrLen,len; ????struct?sockaddr_storage?claddr; ????char?buf[BUF_SIZE]; ????char?addrStr[IS_ADDR_STR_LEN]; ????if(becomeDaemon(0)?==?-1) ????????errExit("becomeDaemon()"); ???? ????sfd?=?inetBind(SERVICE,SOCK_DGRAM,&addrLen); ????if(sfd?==?-1) ????{ ????????syslog(LOG_ERR,"Could?not?create?server?socket?(%s)",strerror(errno)); ????????exit(EXIT_FAILURE); ????} ????for(;;) ????{ ????????len?=?sizeof(struct?sockaddr_storage); ????????numRead?=?recvfrom(sfd,buf,BUF_SIZE,0,(struct?sockaddr*)&claddr,&len); ????????if(numRead?==?-1) ????????????errExit("recvfrom()"); ???????? ????????if(sendto(sfd,buf,numRead,0,(struct?sockaddr*)&claddr,len)?!=?numRead) ????????{ ????????????syslog(LOG_WARNING,"Error?echoing?response?to?%s?(%s)",inetAddressStr((struct?sockaddr*)&claddr,len,addrStr,IS_ADDR_STR_LEN),strerror(errno)); ????????} ????} }
client
int?main(int?argc,char*?argv[]) { ????int?sfd,j; ????size_t?len; ????ssize_t?numRead; ????char?buf[BUF_SIZE]; ????if(argc?2?||?strcmp(argv[1],"--help")?==?0) ????{ ????????printf("%s?host?msg... ",argv[0]); ????????exit(EXIT_SUCCESS); ????} ????sfd?=?inetConnect(argv[1],SERVICE,SOCK_DGRAM); ????if(sfd?==?-1) ????????errExit("Colud?not?connect?to?server?port"); ???? ????for(j?=?2;j?1.2、并發型 TCP echo 服務器
static?void?grimReaper(int?sig) { ????int?savedErrno; ????savedErrno?=?errno; ????while(waitpid(-1,NULL,WNOHANG)?>?0) ????????continue; ???? ????errno?=?savedErrno; } static?void?handleRequest(int?cfd) { ????char?buf[BUF_SIZE]; ????ssize_t?numRead; ????while((numRead?=?read(cfd,buf,BUF_SIZE))?>?0) ????{ ????????if(write(cfd,buf,numRead)) ????????{ ????????????syslog(LOG_ERR,"write()?failed?:?%s",strerror(errno)); ????????????exit(EXIT_SUCCESS); ????????} ????} ????if(numRead?==?-1) ????{ ????????syslog(LOG_ERR,"Error?from?read()?:?%s",strerror(errno)); ????????exit(EXIT_SUCCESS); ????} } int?main(int?argc,char*?argv[]) { ????int?lfd,cfd; ????struct?sigaction?sa; ????if(becomeDaemon(0)?==?-1) ????????errExit("becomeDaemon()"); ???? ????sigemptyset(&sa.sa_mask); ????sa.sa_flags?=?SA_RESTART; ????sa.sa_handler?=?grimReaper; ????if(sigaction(SIGCHLD,&sa,NULL)?==?-1) ????{ ????????syslog(LOG_ERR,"Error?from?sigaction()?:?%s",strerror(errno)); ????????exit(EXIT_FAILURE); ????} ????lfd?=?inetListen(SERVICE,10,NULL); ????if(lfd?==?-1) ????{ ????????syslog(LOG_ERR,"Could?not?create?server?socket?:?(%s)",strerror(errno)); ????????exit(EXIT_FAILURE); ????} ????for(;;) ????{ ????????cfd?=?accept(lfd,NULL,NULL); ????????if(cfd?==?-1) ????????{ ????????????syslog(LOG_ERR,"Failure?in?accept?:?(%s)",strerror(errno)); ????????????exit(EXIT_FAILURE); ????????} ???????? ????????switch(fork()) ????????{ ????????????case?-1: ????????????????syslog(LOG_ERR,"Can?not?create?child?:?(%s)",strerror(errno)); ????????????????close(cfd); ????????????????break; ????????????case?0: ????????????????close(lfd); ????????????????handleRequest(cfd); ????????????????_exit(EXIT_SUCCESS); ????????????default: ????????????????close(cfd); ????????????????break; ????????} ????} }1.3、并發型服務器的其他設計方案
對于一個負載很高的服務器來說,為每個客戶端創建一個新的子進程或者線程所帶來的開銷對服務器來說是沉重的負擔。
可以考慮下面的幾種方案:
在服務器上預先創建進程或線程
服務器程序在啟動階段(即在任何客戶端請求到來之前)就立刻預先創建好一定數量的子進程(線程),而不是針對每個客戶端來創建一個新的子進程(線程),這些子進程(線程)構成一個服務池
服務池中每個子進程一次只處理一耳光客戶端,在處理完客戶端請求后,子進程并不會終止,而是獲取下一個待處理的客戶端繼續處理
采用上述的服務池時,在負載高峰期應該動態增加服務池的大小,在負載降低時,應該相應地降低服務池大小。
在單個進程中處理多個客戶端
為了實現這一點,必須采用一種允許單個進程同時監視多個文件描述符 IO 事件的 IO 模型。
必須依靠內核來確保每個服務進程能公平地訪問到服務器主機的資源。
采用服務器集群
用來處理高客戶端負載的方法還包括使用多個服務器系統,即服務器集群。
構建服務器集群最簡單的方法就是 DNS 輪轉負載共享(DNS round-robin load sharing)或者負載分發(load distribution)。一個地區的域名權威服務器將同一個域名映射到多個 IP 地址上,后續對 DNS 服務器的域名解析請求將以循環輪轉的方式以不同的順序返回這些 IP 地址。
DNS 循環輪轉的優勢是成本低,而且容易實施。但是也存在一些問題,其中一個問題是遠端 DNS 服務器上所執行的緩存操作,這意味著今后位于某個特定主機上的客戶端發出的請求會繞過循環輪轉 DNS 服務器,并總是由同一個服務器來負責處理。此外,循環輪轉 DNS 并沒有任何內建的用來確保到達良好負載均衡或者是確保高可用性的機制。
inetd(Internet 超級服務器)守護進程
守護進程 inetd 被設計用來消除運行大量非常用服務器進程的需要,inetd 可提供兩個主要的好處:
與其為每個服務運行一個單獨的守護進程,現在只用一個進程 inetd 守護進程,就可以監視一組指定的套接字端口,并按照需要啟動其他的服務,從而可以降低系統上運行的進程數量
inetd 簡化了啟動其他服務的編程工作,因為由 inetd 執行的一些步驟通常在所有的網絡服務啟動時都會用到
inetd 守護進程所做的操作
inetd 守護進程通常在系統啟動時運行,在成為守護進程后,inetd 執行的步驟:
對于在配置文件?/etc/inetd.conf?中指定的每個服務,inetd 都會創建一個恰當類型的套接字,然后綁定到指定的端口上,每個 TCP 都會通過?listen()?調用允許客戶端來連接
通過?select()?調用,inetd 對前一步中創建的所有套接字進行監視,看是否有數據報或請求連接發送過來
select()?調用進入阻塞,直到一個 UDP 套接字上有數據報可讀或者 TCP 套接字上收到了連接請求,在 TCP 連接中,inetd 在進入下一個步驟之前會先為連接執行?accept()
要啟動這個套接字上指定的服務,inetd 調用?fork()?創建一個新的進程,然后通過?exec()?啟動服務器程序,在執行?exec()?之前,子進程執行如下步驟:
除了用于 UDP 數據報和接受 TCP 連接的文件描述符外,將其他所有從父進程繼承而來的文件描述符都關閉
在文件描述符 0,1,2 上復制套接字文件描述符,并關閉套接字文件描述符本身
這一步是可選的,為啟動的服務器進程設定用戶和組 ID,設定的值可以在?/etc/inetd.conf?中相應條目找到
在 TCP 連接上接受一個連接,inetd 就關閉這個套接字
跳回到?select()?步驟繼續執行
/etc/inetd.conf?文件
/etc/inetd.conf?文件中的每一行都描述一種由 inetd 處理的服務,包含以下字段:
服務名稱
套接字類型
協議
標記,該字段的內容要么是?wait,要么是?nowait。表明了由 inetd 啟動的服務器是否會接管用于該服務的套接字,如果啟動的服務器需要管理這個套接字,那么就指定為?wait
登錄名
服務器程序
服務器程序參數
當修改了?/etc/inetd.conf?文件之后,需要發送一個?SIGHUP?信號給 inetd,請求其重新讀取配置文件:
kill?-HUP?inted
編輯:黃飛
評論