上个博客的最后,说要写一个功能齐全一些服务器,所以,这边博客主要对这个服务器进行一个简单的介绍。这个服务器,是一个聊天室服务器。 当客户端连接到服务器后,就可以收到所有其他客户端发送给服务器的内容。主要实现原理如下:
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了,再把多进程改成进程池。然后把多进程改成多线程,再改成线程池,敬请期待。
来源:oschina
链接:https://my.oschina.net/u/151387/blog/172932