【linux下c語言服務器開發系列5】功能齊全的聊天室 sever [IO複用+多進程+信號處...

    上個博客的最後,說要寫一個功能齊全一些服務器,因此,這邊博客主要對這個服務器進行一個簡單的介紹。這個服務器,是一個聊天室服務器。 當客戶端鏈接到服務器後,就能夠收到全部其餘客戶端發送給服務器的內容。主要實現原理以下: 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了,再把多進程改爲進程池。而後把多進程改爲多線程,再改爲線程池,敬請期待。

相關文章
相關標籤/搜索