UNIX网络编程笔记(3):简单的并发服务器
解決辦法是將服務器改為并發服務器。這樣,即使有一個客戶正在和服務器連接,其它的客戶也能與服務器建立連接獲得服務。最簡單的辦法就是fork函數。
1、fork函數
fork函數是UNIX中派生進程的唯一方法,定義在<unistd.h>頭文件中:
pid_t fork(void);這個函數的奇特之處在于一次調用,兩次返回。在父進程中返回子進程的ID,在子進程中返回0。所以,我們可以通過返回值判斷當前進程是父進程還是子進程。fork函數在子進程中返回0而不是父進程ID的原因是:任何子進程只有一個父進程,而這個父進程的ID可以通過函數getppid獲得;但是對于父進程,可以有多個子進程,所以父進程無法通過函數獲得子進程ID,如果父進程想跟蹤所有子進程的ID,那么父進程必須在每次調用fork時記住子進程ID。
2、并發服務器
上面介紹了fork函數,可以通過調用fork函數達到并發的效果。下面是典型的并發服務器框架:
pid_t pid; int listenfd,connfd; listenfd=socket(...); bind(listenfd,...); listen(listenfd,...); for(;;) {connfd=accept(listenfd,...);if((pid=fork)==0){close(listenfd);//關閉listenfddoit(connfd);//處理請求close(connfd);//關閉子進程connfdexit(0);//子進程退出}close(onnfd);//關閉父進程connfd }這個框架中究竟發生了什么?可以用下面的圖示展示出來。(1)服務器調用socket、bind和listen函數后,處于監聽狀態,等待客戶發送連接請求,這個時候服務器還沒有調用accept:
(2)這時服務器只有一個套接字描述符listenfd,即監聽描述符。當服務器中accept函數返回時,就有了兩個套接字描述符listenfd和connfd,套接字描述符connfd是一個連接描述符,可以進行讀寫操作:
(3)這個時候客戶與服務器建立了連接。隨后服務器調用fork函數,自己變成父進程,復制自己形成一個子進程。由于兩個進程相同,所以客戶與兩個進程的兩個connfd都處于連接狀態:
(4)然后,父進程中關閉連接套接字connfd,子進程關閉監聽套接字listenfd:
這就達到了我們希望的狀態,由子進程處理客戶請求,父進程繼續監聽。
需要注意的是,close函數會導致發送一個FIN,隨后TCP連接應該終止。那為什么父進程中close(connfd)后沒有終止與客戶的連接呢?每個文件描述符都有一個引用計數,是當前打開著的引用該文件的描述符的個數,只有引用計數為0時文件才會關閉。而fork函數執行后,對connfd套接字的引用計數是2,父進程關閉connfd后引用計數變為1,所以不會關閉連接。然而在子進程中,關閉connfd后引用計數變為0,會關閉與客戶的連接。
3、時間獲取程序的改進
這里我們只需要改進服務器程序,將它變成一個并發服務器。代碼如下:
#include <sys/socket.h> #include <string.h> #include <strings.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <time.h> #include <unistd.h> #define MAXLINE 1024int main(int argc,char *argv[]) {int listenfd;struct sockaddr_in servaddr;char buff[MAXLINE];time_t ticks;if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0){printf("socket error\n");return 0;}bzero(&servaddr,sizeof(servaddr));servaddr.sin_family=AF_INET;servaddr.sin_port=htons(5000);servaddr.sin_addr.s_addr=htonl(INADDR_ANY);if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0){printf("bind error\n");return 0;}if(listen(listenfd,5)<0){printf("listen error\n");return 0;}int connfd;socklen_t len;struct sockaddr_in cliaddr;pid_t pid;for(;;){len=sizeof(cliaddr);if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&len))<0){printf("accept error\n");return 0;}if((pid=fork())==0){if(close(listenfd)<0){printf("close listenfd error\n");return 0;}printf("[PID]%ld Receive a connection from:%s.%d\n",(long)getpid(),inet_ntop(AF_INET,&cliaddr.sin_addr,buff,sizeof(buff)),ntohs(cliaddr.sin_port));ticks=time(NULL);snprintf(buff,sizeof(buff),"%.24s\r\n",ctime(&ticks));if(write(connfd,buff,strlen(buff))<0){printf("write error\n");return 0;}printf("[PID]%ld sleep 5s.\n",(long)getpid());sleep(5);printf("[PID]%ld sleep done.\n",(long)getpid());if(close(connfd)<0){printf("close child connfd error\n");return 0;}return 0;}if(close(connfd)<0){printf("close parent connfd error\n");return 0;}} }這里我們為了增加服務器處理每個請求的時間,加了sleep(5),會看到,服務器在處理一個客戶的請求時,也會與另一個客戶建立連接處理請求。運行結果如下:(1)打開服務器:
(2)客戶1連接(進程ID是3594):
(3)客戶2連接(進程ID是3596):
(4)下圖是服務器處理情況:
可以看到,在客戶1(進程ID是3594)仍在sleep的時候,服務器接受了客戶2(進程ID是3596)的連接。
總結
以上是生活随笔為你收集整理的UNIX网络编程笔记(3):简单的并发服务器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 巫蛊笔记剧情介绍
- 下一篇: UNIX网络编程笔记(4):简单的回射程