在使用socket進行網絡編程時,首先要選擇一個合適的服務器模型是很重要的。在網絡程序里,通常都是一個服務器服務多個客戶機,為了處理多個客戶機的請求,服務器端的程序有不同的處理方式。
目前最常用的服務器模型分為兩大類,循環服務器模型和並發服務器模型
循環服務器模型
UDP循環服務器模型
UDP循環服務器每次獲取一個客戶端的請求,處理后將結果返回給客戶端。
//UDP循環服務器模型偽代碼 main() { listenfd = socket(...);//創建監聽套接字 bind(...);//將地址信息和listenfd綁定 while(1) {
recvfrom(...);//從客戶端讀取
process(...);//處理 sendto(...);//發送回客戶端 }
close(listenfd); }
TCP循環服務器模型
同樣是每次從等待客戶端取出一個,對其進行處理然后將結果返回客戶端
//TCP循環服務器模型偽代碼 main() { listenfd = socket(...);//創建監聽套接字 bind(...);//將地址信息和listenfd綁定 listen(..);//監聽 while(1) { accept(...);//接受客戶端連接請求 while(1) { read/recv(...);//接受 procecc(...);//處理 write/send(..);//返回 }
close(sockfd); }
close(listenfd); }
並發服務器模型
為了彌補循環服務器一次只能服務於一個客戶端的缺陷,人們又設計了並發服務器模型。
多進程並發服務器模型
多進程並發服務器模型。為了避免一個客戶端獨占服務器,在客戶端建立連接時會為每個客戶端創建一個子進程。這樣一來多個客戶端同時響應,就會對操作系統的效率有所影響,但不可否認滿足了同時服務多個客戶端的需求。具體做法是在監聽到客戶端連接請求時,首先fork一個子進程服務於客戶端,父進程繼續監聽新的客戶端連接。實際可用進程池解決使用時才創建進程的資源開銷問題。
//多進程並發服務器模型偽代碼 main() { listenfd = socket(...);//創建監聽套接字 //裝填服務器地址信息 bind(...);//將監聽套接字listenfd與地址信息綁定 listen(listenfd, 10);//開始監聽,並設置監聽數量 while(1) { //有客戶端連接請求時,獲取到客戶端sockfd,沒有請求時阻塞 sockfd = accept(listenfd, ..., ...); pid = fork();//創建子進程,服務於客戶端 if (pid == 0) { while(1) { close(listenfd);//首先在子進程關閉掉監聽套接字,防止子進程對其他客戶端請求進行監聽 recv(...); //處理; send(...); } close(sockfd);//處理結束后關閉套接字 exit(0);//結束子進程 } close(sockfd); } close(listenfd) }
多線程並發服務器模型
多線程服務器與多進程服務器模型類似。相較於多進程並發服務器,使用多線程技術完成並發服務器對系統開銷要小得多。使用多線程並發服務器模型時,要注意對臨界資源(能被多個線程訪問,但同時只應被一個線程訪問)進行保護。實際使用時,可以采用線程池技術避免每次客戶端連接請求到來時創建子線程時,不必要的系統開銷。
//多線程並發服務器模型偽代碼 //服務程序 void *serv_routine(void *arg) { sockfd = (int )arg; while(1) { read(sockfd, buf, sizeof buf); //處理 write(sockfd, buf, ret); } } main() { //初始化線程池 thread_pool_init(); //創建監聽套接字 listenfd = socket(...); //填充地址信息 bind(...);//將地址信息與監聽套接字綁定 listen(listenfd, 10);//開始監聽 while(1) { //接受客戶端連接請求,獲取器sockfd sockfd = accept(...); //向進程池添加客戶端服務程序 thread_pool_addtask(..., serv_routine, (void*)sockfd); } close(sockfd); close(listenfd); //銷毀線程池 thread_pool_destroy(...); }
I/O多路服用並發服務器
I/O多路復用可以解決多線程和多進程資源限制的問題。此模型實際上是將UDP循環模型用在了TCP上面。但是它也存在問題,由於它也是一次處理客戶端的請求,可能會導致有些客戶端等待時間過長。
//I/O多路復用——select模型 int main() { //創建監聽套接字描述符 listenfd = socket(AF_INET, SOCK_STREAM, 0); //裝填地址 //將監聽套接字描述符與裝填好的地址綁定 bind(listenfd, (struct sockaddr*)&myaddr, len)); //開始監聽 listen(listenfd, 10); fd_set readfds; //設置監聽讀文件描述符集合 fd_set writefds; //設置監聽寫文件描述符集合 FD_ZERO(&readfds); //清空這些集合 FD_ZERO(&writefds); FD_SET(listenfd, &readfds); //將listenfd添加到讀文件描述符集合中 fd_set temprfds = readfds; //定義這個兩個temp集合是為了在每次有可讀寫文件描述符時,都可以在處理完成后繼續監聽之前加入的文件描述符 fd_set tempwfds = writefds; int maxfd = listenfd; #define BUFSIZE 100 #define MAXNFD 1024 int nready; char buf[MAXNFD][BUFSIZE] = {0}; while(1) { temprfds = readfds; tempwfds = writefds; //獲取可可讀或可寫的文件描述符,放到集合中, select返回可讀、寫的文件描述符個數 nready = select(maxfd+1, &temprfds, &tempwfds, NULL, NULL); //有客戶端訪問時監聽套接字描述符可讀,可以通過FD_ISSET來判斷具體是哪個文件描述符 if(FD_ISSET(listenfd, &temprfds)) { //接收客戶端連接請求、並獲取其sockfd int sockfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len); //將獲取到的套接字描述符加入到讀操作監聽集合中 FD_SET(sockfd, &readfds); maxfd = maxfd>sockfd?maxfd:sockfd; if(--nready==0) continue; } int fd = 0; //遍歷文件描述符集合,對就緒的文件秒速符進行處理 for(;fd<=maxfd; fd++) { if(fd == listenfd) continue; //讀操作就緒的套接字描述符 if(FD_ISSET(fd, &temprfds)) { int ret = read(fd, buf[fd], sizeof buf[0]); if(0 == ret) { close(fd); //處理完成后,將其沖監聽集合中移除 FD_CLR(fd, &readfds); if(maxfd==fd) --maxfd; continue; } //以為要把處理后的結果發送回客戶端,因此將套接字描述符添加到寫操作監聽集合中 FD_SET(fd, &writefds); } //寫操作就緒的套接字描述符 if(FD_ISSET(fd, &tempwfds)) { int ret = write(fd, buf[fd], sizeof buf[0]); printf("ret %d: %d\n", fd, ret); FD_CLR(fd, &writefds); } } } close(listenfd); }