進程和線程的使用在前面博文已經講述完畢,在完成一個最簡單的服務器之后,就是要考慮下如何實現並發服務器了。
要實現服務的並發,只能通過進程和線程兩種方式。
之前提到過listen_fd和connect_fd,listen用於監聽是否有客戶端連接,維護兩個fd隊列,沒完成握手的和完成就緒的。
connect從就緒隊列取描述符,這個connect_fd描述符將用於數據通信,所以要實現並發,就是將connect_fd分發到線程或進程上,由他們去獨立完成通信。
在實際並發服務器應用場合,在IO層大多通過兩個地方來提高代碼效率,一個是描述符處理,一個是線程/進程調度處理。
下圖簡單描述了並發服務器的原理:
在處理IO時,會用到IO復用技術提高效率,在線程/進程分配時,會先構造線程池或進程池,並以某種方式調度,這些在后續博文詳細描述。
下面是並發實現的簡單代碼,利用線程和進程實現服務器的並發。
進程實現:
1 /* File Name: server.c */ 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <unistd.h> 6 #include <sys/wait.h> 7 #include <errno.h> 8 #include <sys/types.h> 9 #include <sys/socket.h> 10 #include <sys/unistd.h> 11 #include <netinet/in.h> 12 13 const int DEFAULT_PORT = 2500; 14 const int BUFFSIZE = 1024; 15 const int MAXLINK = 10; 16 17 class sigOp 18 { 19 public: 20 void addSigProcess(int sig,void (*func)(int)); 21 void sendSig(const int sig, const int pid); 22 }; 23 24 void sigOp::addSigProcess(int sig,void (*func)(int)) 25 { 26 struct sigaction stuSig; 27 memset(&stuSig, '\0', sizeof(stuSig)); 28 stuSig.sa_handler = func; 29 stuSig.sa_flags |= SA_RESTART; 30 sigfillset(&stuSig.sa_mask); 31 sigaction(sig, &stuSig, NULL); 32 } 33 34 void sigOp::sendSig(const int sig, const int pid) 35 { 36 kill(pid, sig); 37 } 38 39 void waitchlid(int sig) 40 { 41 pid_t pid; 42 int stat; 43 while((pid = waitpid(-1, &stat, WNOHANG)) > 0); 44 } 45 46 int main(int argc, char** argv) 47 { 48 int socket_fd, connect_fd; 49 struct sockaddr_in servaddr; 50 char buff[BUFFSIZE]; 51 52 if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 53 { 54 printf("create socket error: %s(errno: %d)\n",strerror(errno),errno); 55 exit(0); 56 } 57 58 memset(&servaddr, 0, sizeof(servaddr)); 59 servaddr.sin_family = AF_INET; 60 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 61 servaddr.sin_port = htons(DEFAULT_PORT); 62 63 if (bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) 64 { 65 printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno); 66 exit(0); 67 } 68 69 if (listen(socket_fd, MAXLINK) == -1) 70 { 71 exit(0); 72 } 73 74 sigOp sig; 75 sig.addSigProcess(SIGCHLD, waitchlid);//回收進程 76 77 while(1) 78 { 79 if ((connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1) 80 { 81 break; 82 } 83 else 84 { 85 if (0 == fork()) 86 { 87 close(socket_fd); //關閉監聽描述符 88 while(1) 89 { 90 memset(&buff,'\0', sizeof(buff)); 91 recv(connect_fd, buff, BUFFSIZE - 1, 0); 92 send(connect_fd, buff, BUFFSIZE - 1, 0); 93 printf("recv msg from client: %s\n", buff); 94 } 95 close(connect_fd); 96 exit(0); 97 } 98 close(connect_fd);//父進程關閉連接描述符 99 } 100 } 101 close(connect_fd); 102 close(socket_fd); 103 }之前提到過listen描述符和connect描述符是完全獨立的,關閉都不會影響另一個描述符工作。所以在代碼中,父子進程都會關閉不需要的描述符。
測試結果如下:
ps -aux查看系統進程,可以看到三個進程,一個是主進程用於listen監聽,兩個子進程進行通信。
下面是線程並發實現:
1 /* File Name: server.c */ 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <unistd.h> 6 #include <pthread.h> 7 #include <errno.h> 8 #include <sys/types.h> 9 #include <sys/socket.h> 10 #include <sys/unistd.h> 11 #include <netinet/in.h> 12 13 const int DEFAULT_PORT = 2500; 14 const int BUFFSIZE = 1024; 15 const int MAXLINK = 10; 16 17 void* run(void* pConnect_fd) 18 { 19 char buff[BUFFSIZE]; 20 int *connect_fd = reinterpret_cast<int *>(pConnect_fd); 21 pthread_detach(pthread_self()); 22 while(1) 23 { 24 memset(&buff,'\0', sizeof(buff)); 25 recv(*connect_fd, buff, BUFFSIZE - 1, 0); 26 send(*connect_fd, buff, BUFFSIZE - 1, 0); 27 printf("recv msg from client: %s\n", buff); 28 } 29 } 30 31 int main(int argc, char** argv) 32 { 33 int socket_fd, connect_fd; 34 struct sockaddr_in servaddr; 35 36 if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 37 { 38 printf("create socket error: %s(errno: %d)\n",strerror(errno),errno); 39 exit(0); 40 } 41 42 memset(&servaddr, 0, sizeof(servaddr)); 43 servaddr.sin_family = AF_INET; 44 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 45 servaddr.sin_port = htons(DEFAULT_PORT); 46 47 if (bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) 48 { 49 printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno); 50 exit(0); 51 } 52 53 if (listen(socket_fd, MAXLINK) == -1) 54 { 55 exit(0); 56 } 57 58 while(1) 59 { 60 if ((connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1) 61 { 62 break; 63 } 64 else //accept獲取到描述符創建線程 65 { 66 pthread_t _Tread; 67 pthread_create(&_Tread, NULL, run, &connect_fd);//傳參為連接描述符 68 } 69 } 70 close(connect_fd); 71 close(socket_fd); 72 }測試結果如下:
效果和進程一樣,執行netstat查看tcp狀態
兩組連接相互通信。
線程並發和進程並發各有優劣,目前大多服務器還是用線程進行並發的,進程要對父進程進行拷貝,資源消耗大,但相互直接資源互不影響,線程效率高但是要注意鎖的使用,一個線程可能會影響整個服務器的運行。
詳細優缺點詳細可參考:http://blog.chinaunix.net/uid-20556054-id-3067672.html