多线程:来一个客户,开一个线程
多进程:来一个客户,开一个子进程
服务器端
#include<stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include<stdlib.h>
#include <strings.h>
#include <arpa/inet.h>
#include<string.h>
#include <pthread.h>
#include<signal.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(fd<0)
{
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/write)
fork () 一劈为二,父子各干各的!
①变量定义
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 == 0)
if(pid == 0) // 🔥 子进程运行这里
{
// 打印客户端IP和端口
inet_ntop(...);
printf("Client:(%s,%d) is connect\n",...);
// 🔥 调用函数,专门服务客户端
client_data_handle(&newfd);
close(fd); // 子进程关闭监听套接字
}
子进程做什么?
- 打印谁连进来了
- 调用 client_data_handle 处理读写
- 关闭不需要的监听 fd
- 函数结束后,子进程退出
⑤父子进程分支2:父进程(pid > 0)
if(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_handle
void 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 |
回收任意子进程(所有退出的子进程) |
| status | NULL |
不关心子进程的退出状态(不需要保存) |
| options | WNOHANG |
非阻塞模式:如果没有子进程退出,函数立刻返回,不卡住父进程 |
关键特性:
- 非阻塞(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, ...)会回收所有已经退出的子进程。
执行结果
