一、多进程服务器
最简单的的服务器程序执行流程为:创建socket → \rightarrow → 绑定监听的IP地址和端口 → \rightarrow → 监听客户端连接 → \rightarrow → 接受/发送数据。当服务端调用read阻塞等待一个客户端发来数据时,无法同时响应其它客户端的连接请求,无法接受其它客户端发来的数据。要想同时与多个客户端建立连接并处理请求,可以为每个连接的客户端创建一个子进程,在子进程中接受/发送客户端的数据,各个子进程之间互不干扰。
按照《Unix网络编程》中所述,多进程服务端程序的逻辑为:
主进程只负责Accept客户端连接,将得到的文件描述符交给子进程处理,自己不需要处理。需要注意两点:
父进程中关闭connfd,子进程中关闭listenfd: 每个文件的file_struct中保存了这个文件的引用计数,即有多少个进程持有这个文件的文件描述符,每次close(fd)将文件的引用计数-1,只有当引用计数 == 0时才真正关闭文件。子进程复制父进程的资源,因此fork()创建子进程时复制了connfd和listenfd,这两个socket文件的引用计数+1,在子进程中不需要listenfd,直接关闭,这个操作不会影响父进程中的listenfd,因为这时listenfd对应的socket文件的引用计数继续保持为1,socket不会关闭。同理,父进程中也不需要connfd,可以直接关闭。
子进程执行完成时调用eixt(0): 子进程执行的代码和父进程是一样的,并不是只会执行if((pid=fork()) == 0)后面这部分。因此子进程完成后显示调用exit(0)让子进程推出,否则子进程还会回到Accept这条语句上,这不是子进程需要做的。
以上流程写成代码就是:
// 子进程执行的函数
void client_handler(int clientfd, int sockfd, unsigned short port) {
close(sockfd);
char buffer[1024];
int ret = 0;
while (true) {
memset(buffer, 0, sizeof(buffer));
printf("receive message from port: %d\t", port);
if (!receive_message(clientfd, buffer)) {
break;
}
}
printf("exit child process\n");
}
int main() {
signal(SIGCHLD, SIG_IGN);
int sockfd = open_socket();
int clientfd = 0;
struct sockaddr_in client_addr;
int size = sizeof(struct sockaddr);
while (true) {
clientfd = accept(sockfd, (struct sockaddr*)&client_addr, (socklen_t*)&size);
if (fork() > 0) {
close(clientfd);
continue;
} else {
client_handler(clientfd, sockfd, client_addr.sin_port);
exit(0);
}
}
close(sockfd);
return 0;
}
为什么要signal(SIGCHLD,SIG_IGN): 子进程退出时,会给父进程发送SIGCHLD信号,如果我们没有指定父进程对于SIGCHLD信号的处理函数,会执行默认的处理方式——SIG_DFL,这种处理方式是不处理这个信号(注意:不处理信号不等于丢弃信号),子进程的PCB(task_struct)不会被清理,变为僵尸进程。我们指定signal(SIGCHLD, SIG_IGN)后,代表父进程丢弃SIGCHLD信号,让系统内核负责子进程的销毁。