【linux下c语言服务器开发系列5】功能齐全的聊天室 sever [IO复用+多进程+信号处...

*爱你&永不变心* 提交于 2019-12-03 01:03:41

    上个博客的最后,说要写一个功能齐全一些服务器,所以,这边博客主要对这个服务器进行一个简单的介绍。这个服务器,是一个聊天室服务器。 当客户端连接到服务器后,就可以收到所有其他客户端发送给服务器的内容。主要实现原理如下:

1.IO复用:

    利用epoll函数,对多个套接字进行监听,包括:listenfd,标准输入套接字,与子进程通讯的套接字还有信号处理的套接字。

 listenfd:这个套接字主要是服务器端用来监听是否有新的客户端的连接的。一旦有连接,则视为新的客户到来,然后,准备连接,分配用户内存,对应各种信息,连接成功后,fork一个子进程进行对这个连接进行一对一的处理。这里的处理,主要是对各种套接字进行监听,并进行相应的处理,下文的多进程部分会有。

标准输入套接字:为的是在服务器端也能输入一些信息,并让这个服务器根据输入的信息进行反应。不过,我这里的反应主要是让服务器原样输出。

与子进程通讯的套接字:因为没个客户,都是用一个子进程在单独的处理,各个子进程之间的通信,首先需要通过父进程,然后再进行广播,从而实现子进程与其他所有子进程之间的通信。这里,子进程与父进程之间的通信,是靠管道完成的。但是,传统的管道是pipe()函数创建的,只能单工的通信,我这里为了双工的通信,使用的是socketpair()创建的管道,管道的两端都可以进行读写操作。如果子进程有数据写给父进程,一般是它有小弟到达,于是,父进程告诉所有其他子进程,说数据可读了,于是各子进程往对应的客户端写数据。

信号处理文件描述符:为了将事件源统一,于是将信号处理的管道的描述符也用epoll来统一监听。这里,信号处理函数要做的事情是如果有信号出现,则向管道里写消息。于是epoll接收到这个消息后,再调用更具体的信号处理函数,进行具体的处理。

上面说的都是父进程要做的内容,下面说说子进程需要完成的内容:

子进程:每个客户端需要一个子进程对其进行处理。父子进程间的通讯方法、各种联系已经在fork()函数调用之间记录好了。子进程需要建立自己的epoll注册事件表。对自己的一些文件描述符使用epoll函数进行监听。这里的epoll主要监听一下:与客户端的连接套接字,与父进程的通信管道套接字和信号处理套接字。

与客户端的连接套接字:这个是用来读写的主要依据。是服务器与客户端通信的窗口。只需对这个套接字进行读写即可。

与父进程的通信套接字:上文提到了该通信管道。用于父子进程间交换信息。一般是客户有数据到达了,子进程要通知父进程,父进程知道这个消息到达后,告诉其他子进程,有人发言了,你们把这个发言发送到各自的客户端去。子进程得知父进程的通知后,对各自的连接套接字进行写操作。

子进程的主要处理函数是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了,再把多进程改成进程池。然后把多进程改成多线程,再改成线程池,敬请期待。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!