linux——TCP多进程并发服务器
多线程来一个客户开一个线程多进程来一个客户开一个子进程服务器端#includestdio.h #include sys/types.h /* See NOTES */ #include sys/socket.h #include unistd.h #includestdlib.h #include strings.h #include arpa/inet.h #includestring.h #include pthread.h #includesignal.h #include sys/wait.h #define QUIT_STR QUIT #define BUFSIZE 1024 #define BACKLOG 5 #define SERV_IP 5001 #define SERV_IP_ADDR 192.168.88.129 void child_data_handle(int signum) { if(SIGCHLD signum) { waitpid(-1,NULL,WNOHANG); } } void* client_data_handle(void* arg); int main() { int fd -1; signal(SIGCHLD,child_data_handle); struct sockaddr_in sin; //1.socket fd socket(AF_INET,SOCK_STREAM,0); if(fd0) { perror(socket); exit(1); } bzero(sin,sizeof(sin)); sin.sin_family AF_INET; sin.sin_port htons(SERV_IP); //sin.sin_addr.s_addr inet_addr(SERV_IP_ADDR); sin.sin_addr.s_addr INADDR_ANY; /*if(inet_pion(AF_INET,SERV_IP_ADDR,(void *)sin.sin_addr.s_addr) ! 1) { perror(inet_pton); exit(1); } */ //2.bind if(bind(fd,(struct sockaddr *)sin,sizeof(sin)) 0) { perror(bind); exit(0); } //3.listen if(listen(fd,BACKLOG) 0) { perror(listen); exit(1); } //4.accept pid_t pid; int newfd -1; struct sockaddr_in cin; socklen_t addrlen sizeof(cin); while(1) { newfd accept(fd,(struct sockaddr *)cin,addrlen); if(newfd 0) { perror(accept); break; } pid fork(); if(pid 0) { perror(fork); break; } if(pid 0) { char ipv4_addr[16]; if(!inet_ntop(AF_INET,(void *)cin.sin_addr,ipv4_addr,sizeof(cin))) { perror(inet_ntop); exit(1); } printf(Client:(%s,%d) is connect\n,ipv4_addr,ntohs(cin.sin_port)); client_data_handle(newfd); close(fd); } if(pid 0) { close(newfd); } } close(fd); return 0; } void* client_data_handle(void* arg) { int newfd *(int *)arg; char buf[BUFSIZE]; int ret -1; printf(client handle process:newfd %d\n,newfd); while(1) { do { bzero(buf,BUFSIZE); ret read(newfd,buf,BUFSIZE-1); }while(ret 1); if(ret 0) { exit(1); } if(!ret) { break; } printf(receive data:%s\n,buf); if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR))) { printf(Client is exiting!\n); break; } } close(newfd); return NULL; }其实与多线程的变化就在最后accept那部分的内容里父进程 只负责等客户端accept子进程 只负责服务客户端read/writefork () 一劈为二父子各干各的①变量定义pid_t pid; // 进程号用来判断父子 int newfd -1; // 客户端连接的套接字 struct sockaddr_in cin;// 用来存客户端IP、端口 socklen_t addrlen sizeof(cin);②死循环父进程永远等客户端while(1) { // 父进程阻塞在这里直到客户端来连接 newfd accept(fd, (struct sockaddr *)cin, addrlen);③客户端来了 → 创建子进程pid fork(); // 进程分裂一父一子fork () 作用把当前进程复制一份一模一样。调用一次返回两个值返回值 0 → 父进程返回值 0 → 子进程④父子分支1子进程pid 0if(pid 0) // 子进程运行这里 { // 打印客户端IP和端口 inet_ntop(...); printf(Client:(%s,%d) is connect\n,...); // 调用函数专门服务客户端 client_data_handle(newfd); close(fd); // 子进程关闭监听套接字 }子进程做什么打印谁连进来了调用 client_data_handle 处理读写关闭不需要的监听 fd函数结束后子进程退出⑤父子进程分支2父进程pid 0if(pid 0) // 父进程运行这里 { close(newfd); // 父进程关闭通信fd }父进程做什么关闭不需要的通信 newfd回到 while (1) 循环继续 accept 等下一个客户端为什么要关闭文件描述符子进程里close(fd);fd 是监听套接字子进程只负责聊天不等新连接→ 关掉父进程里close(newfd);newfd 是通信套接字父进程只负责等连接不聊天→ 关掉一句话各用各的不用就关防止资源泄漏完整流程父进程 accept 阻塞等客户端客户端连进来fork () → 分裂出子进程父进程关闭 newfd → 回去继续等新客户子进程关闭 fd → 调用函数处理聊天客户端发消息 → 子进程 read 并打印客户端退出 → 子进程退出系统给父进程发 SIGCHLD信号函数 waitpid 回收子进程清理僵尸客户端要是断开连接子进程就死了但是父进程一直在死循环无法回收子进程子进程就会成为僵尸进程过多的僵尸进程会大量占用资源所以我门还要写个函数专门回收僵尸进程// 信号处理函数回收子进程 void child_data_handle(int signum) { // 判断收到的信号是不是 SIGCHLD if(SIGCHLD signum) { // 非阻塞方式回收所有退出的子进程 waitpid(-1, NULL, WNOHANG); } } // 注册信号处理当子进程退出时内核自动调用上面的函数 signal(SIGCHLD, child_data_handle);在 Linux 中子进程退出时内核不会直接销毁它而是保留进程信息PID、退出状态变成僵尸进程。父进程必须调用wait()/waitpid()读取子进程退出状态内核才会彻底删除子进程。如果父进程不回收僵尸进程会一直占用系统 PID 资源。子进程退出时内核会向父进程发送SIGCHLD信号。这段代码就是监听子进程退出信号 → 自动回收 → 消灭僵尸进程。①信号处理函数child_data_handlevoid child_data_handle(int signum)这是自定义的信号处理函数当内核给父进程发信号时父进程会自动执行这个函数。参数signum内核传过来的信号编号告诉我们收到了哪个信号。②判断信号类型if(SIGCHLD signum)SIGCHLD子进程退出 / 停止 / 继续时内核发送给父进程的固定信号编号 17。作用安全校验确保我们只处理子进程退出信号不处理其他信号。③核心回收函数waitpid(-1, NULL, WNOHANG)这是整个代码的灵魂3 个参数必须讲清楚waitpid(pid_t pid, int *status, int options)参数我们的取值含义pid-1回收任意子进程所有退出的子进程statusNULL不关心子进程的退出状态不需要保存optionsWNOHANG非阻塞模式如果没有子进程退出函数立刻返回不卡住父进程关键特性非阻塞WNOHANG父进程该干嘛干嘛不会因为等子进程而卡住。自动回收只要有子进程退出内核发信号 → 调用函数 → 立刻回收。④注册信号处理signal(SIGCHLD, child_data_handle)signal(SIGCHLD, child_data_handle);作用告诉内核“当你收到SIGCHLD子进程退出信号时请自动调用child_data_handle这个函数处理。”这行代码必须写在父进程中一般放在程序开头。1. 为什么不用wait()要用waitpid()wait()是阻塞的父进程会卡住。waitpid(..., WNOHANG)是非阻塞的父进程不卡顿。2. 为什么要判断if(SIGCHLD signum)一个函数可能处理多个信号加上判断更安全避免误处理。3. 这段代码能回收多个子进程吗能waitpid(-1, ...)会回收所有已经退出的子进程。执行结果