上個博客的最後,說要寫一個功能齊全一些服務器,因此,這邊博客主要對這個服務器進行一個簡單的介紹。這個服務器,是一個聊天室服務器。 當客戶端鏈接到服務器後,就能夠收到全部其餘客戶端發送給服務器的內容。主要實現原理以下: git
1.IO複用: 數組
利用epoll函數,對多個套接字進行監聽,包括:listenfd,標準輸入套接字,與子進程通信的套接字還有信號處理的套接字。 服務器
listenfd:這個套接字主要是服務器端用來監聽是否有新的客戶端的鏈接的。一旦有鏈接,則視爲新的客戶到來,而後,準備鏈接,分配用戶內存,對應各類信息,鏈接成功後,fork一個子進程進行對這個鏈接進行一對一的處理。這裏的處理,主要是對各類套接字進行監聽,並進行相應的處理,下文的多進程部分會有。 多線程
標準輸入套接字:爲的是在服務器端也能輸入一些信息,並讓這個服務器根據輸入的信息進行反應。不過,我這裏的反應主要是讓服務器原樣輸出。 socket
與子進程通信的套接字:由於沒個客戶,都是用一個子進程在單獨的處理,各個子進程之間的通訊,首先須要經過父進程,而後再進行廣播,從而實現子進程與其餘全部子進程之間的通訊。這裏,子進程與父進程之間的通訊,是靠管道完成的。可是,傳統的管道是pipe()函數建立的,只能單工的通訊,我這裏爲了雙工的通訊,使用的是socketpair()建立的管道,管道的兩端均可以進行讀寫操做。若是子進程有數據寫給父進程,通常是它有小弟到達,因而,父進程告訴全部其餘子進程,說數據可讀了,因而各子進程往對應的客戶端寫數據。 函數
信號處理文件描述符:爲了將事件源統一,因而將信號處理的管道的描述符也用epoll來統一監聽。這裏,信號處理函數要作的事情是若是有信號出現,則向管道里寫消息。因而epoll接收到這個消息後,再調用更具體的信號處理函數,進行具體的處理。 this
上面說的都是父進程要作的內容,下面說說子進程須要完成的內容: spa
子進程:每一個客戶端須要一個子進程對其進行處理。父子進程間的通信方法、各類聯繫已經在fork()函數調用之間記錄好了。子進程須要創建本身的epoll註冊事件表。對本身的一些文件描述符使用epoll函數進行監聽。這裏的epoll主要監聽一下:與客戶端的鏈接套接字,與父進程的通訊管道套接字和信號處理套接字。 .net
與客戶端的鏈接套接字:這個是用來讀寫的主要依據。是服務器與客戶端通訊的窗口。只需對這個套接字進行讀寫便可。 線程
與父進程的通訊套接字:上文提到了該通訊管道。用於父子進程間交換信息。通常是客戶有數據到達了,子進程要通知父進程,父進程知道這個消息到達後,告訴其餘子進程,有人發言了,大家把這個發言發送到各自的客戶端去。子進程得知父進程的通知後,對各自的鏈接套接字進行寫操做。
子進程的主要處理函數是runchild函數,該函數以下:
int runchild(user* curuser,char *shmem) { assert(curuser!=NULL); int child_epollfd=epoll_create(5); assert(child_epollfd!=-1); epoll_event events[MAX_EVENT_NUMBER]; int conn=curuser->conn; addfd(child_epollfd,conn); addfd(child_epollfd,curuser->pipefd[1]); int stop_child=0; int ret=0; while(!stop_child) { printf("in child\n"); int number=epoll_wait(child_epollfd,events,MAX_EVENT_NUMBER,-1); if(number<0 && errno!=EINTR) { printf("epoll error in child"); break; } for(int i=0;i<number;i++) { int sockfd=events[i].data.fd; if(sockfd ==conn && (events[i].events & EPOLLIN) ) { memset(shmem+curuser->user_number*BUF_SIZE,'\0',BUF_SIZE); ret=recv(conn,shmem+curuser->user_number*BUF_SIZE,BUF_SIZE-1,0); if(ret<0 && errno!=EAGAIN) stop_child=1; else if (ret==0) stop_child=1; else //通知父進程,有了數據啦,讓父進程告訴別的子進程去讀吧。哈哈 { shmem[curuser->user_number*BUF_SIZE+ret]='\0'; printf("some thing\n"); send(curuser->pipefd[1],(char*)&(curuser->user_number),sizeof(curuser->user_number),0); } } else if (sockfd==curuser->pipefd[1] && events[i].events & EPOLLIN) { printf("some thing from father\n"); int client_number; ret=recv(sockfd,(char*)&client_number,sizeof(client_number),0); if(ret<0 && errno!=EAGAIN) { stop_child=1; } else if (ret==0) stop_child=1; else //從收到的客戶端那裏讀取內容,往本身這裏寫 { // printf("rec from father,then write to his client\n"); char tmpbuf[BUF_SIZE*2]; sprintf(tmpbuf,"client %d says: ",client_number); //memcpy(tmpbuf+sizeof(tmpbuf),shmem+(client_number)*BUF_SIZE,BUF_SIZE); sprintf(tmpbuf+15,"%s",shmem+(client_number*BUF_SIZE)); // send(conn,tmpbuf,sizeof(tmpbuf),0); send(conn,tmpbuf,strlen(tmpbuf),0); } } } } }
進程間通訊:
上文說到了進程間通訊的方法,一個是管道,主要是信號處理部分。一個是套接字,父子進程間的管道實際是本地域的套接字。還有一個是共享內存。父進程開闢一塊共享內存,用於各類進程間的內容共享。因而,每一個子進程能夠經過這塊內從,與其餘子進程進行信息的共享。這就是聊天室的內容可以共享的一個緣由。並且,還不須要大量的數據拷貝,具備必定的效率。共享內存代碼以下
//開闢一塊內存,返回一個共享內存對象 shmfd=shm_open(shm_name,O_CREAT|O_RDWR,0666); assert(shmfd!=-1); //將這個共享內存對象的大小設定爲 ** int ret=ftruncate(shmfd,USER_LIMIT*BUF_SIZE); assert(ret!=-1); //將剛纔開闢的共享內存,關聯到調用進程 share_mem=(char *) mmap(NULL,USER_LIMIT*BUF_SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,shmfd,0); assert(share_mem!=MAP_FAILED);
管道以下:
typedef struct user { int conn; int stop; int pipefd[2]; //父子進程間的管道。 int user_number; sockaddr_in client_addr; char bufread[BUF_SIZE]; char bufwrite[BUF_SIZE]; pid_t pid; // which process deal with this user } user;//父子進程間的管道int piperet = socketpair ( AF_UNIX , SOCK_STREAM , 0 , users [ user_number ]. pipefd );assert ( piperet == 0 );
//用來做爲信號與epoll連接的管道 int retsigpipe=socketpair(AF_UNIX,SOCK_STREAM,0,sig_pipefd);
信號處理:
這裏主要處理3個信號,sigterm,sigint,sigchld。對於前兩個信號,父子進程的處理方法有一些不一樣。添加要處理的信號及其處理函數以下:
int add_sig(int signum, void(handler)(int) ) { struct sigaction sa; memset(&sa,'\0',sizeof(sa)); sa.sa_handler=handler; sa.sa_flags|=SA_RESTART; //這個的意思是將全部的信號都監聽,而後都是調用一個handler來處理。這樣看上去好像不太合理。可是,後面咱們就知道,爲了統一事件源,在此將全部信號一視同仁的處理,在接下來的IO複用中,會有好處的。 sigfillset(&sa.sa_mask); assert(sigaction(signum,&sa,NULL)!=-1); }統一的信號處理函數以下:(關聯到epoll)
/* 當信號發生時,調用這個函數來處理該信號*/ void sig_handler(int sig) { int save_errno=errno; int msg=sig; send(sig_pipefd[1],(char*)&msg,sizeof(msg),0); errno=save_errno; }
epoll中,出現信號事件了,調用具體的函數處理函數:
/*具體用來處理信號事件的函數*/ void sig_concrete_handler(int sig,int epollfd) { printf("signal chld occur\n"); pid_t pid; int stat_loc; while(pid=waitpid(-1,&stat_loc,WNOHANG)>0) //pid=waitpid(-1,&stat_loc,WNOHANG)>0; if(pid>0) { /*做一些具體的回收工做,好比關閉子進程中打開的socket,可是,咱們目前沒法直接得到該socket,沒法關閉;只能獲取目前的子進程的pid,因此,須要創建pid與鏈接socket之間的聯繫,能夠用一個簡單的數組做對應。也能夠用一個結構體(記錄多種數據)+一個全局化的數組來做對應,這裏用subprocess將pid於user對應*/ printf("close process %d\n",pid); subprocess[pid].u->stop=1; subprocess[pid].u->pid=-1; //再也不監聽與父進程通訊的管道了 epoll_ctl(epollfd,EPOLL_CTL_DEL,subprocess[pid].u->pipefd[0],0); //關閉與父進程通訊的管道了 close(subprocess[pid].u->pipefd[0]); //關閉與客戶端的鏈接 close(subprocess[pid].u->conn); } }
完成的代碼見:http://git.oschina.net/mengqingxi89/codelittle/blob/master/codes/echoserver/final_echo_server.cpp
編譯於運行:
由於使用了共享內存,須要在編譯的時候加上-lrt選項。即 g++ -lrt -o outfile file.cpp.而後運行該文件。
./outfile 127.0.0.1 12345
再開多個telnet 127.0.0.1 12345 就能夠進行多個telnet之間的聊天了。
我接下來寫一個簡單的客戶端,不用telnet了,再把多進程改爲進程池。而後把多進程改爲多線程,再改爲線程池,敬請期待。