linux——TCP多进程并发服务器

多线程:来一个客户,开一个线程

多进程:来一个客户,开一个子进程

服务器端

复制代码
#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);   // 子进程关闭监听套接字
}

子进程做什么?

  1. 打印谁连进来了
  2. 调用 client_data_handle 处理读写
  3. 关闭不需要的监听 fd
  4. 函数结束后,子进程退出

⑤父子进程分支2:父进程(pid > 0)

复制代码
if(pid > 0)  // 🔥 父进程运行这里
{
    close(newfd);  // 父进程关闭通信fd
}

父进程做什么?

  1. 关闭不需要的通信 newfd
  2. 回到 while (1) 循环,继续 accept 等下一个客户端

为什么要关闭文件描述符?

子进程里:

复制代码
close(fd);

fd 是监听套接字 子进程只负责聊天,不等新连接 → 关掉

父进程里:

复制代码
close(newfd);

newfd 是通信套接字 父进程只负责等连接,不聊天 → 关掉

一句话:

各用各的,不用就关,防止资源泄漏!

完整流程

  1. 父进程 accept 阻塞等客户端
  2. 客户端连进来
  3. fork () → 分裂出子进程
  4. 父进程关闭 newfd → 回去继续等新客户
  5. 子进程关闭 fd → 调用函数处理聊天
  6. 客户端发消息 → 子进程 read 并打印
  7. 客户端退出 → 子进程退出
  8. 系统给父进程发 SIGCHLD
  9. 信号函数 waitpid 回收子进程(清理僵尸)

客户端要是断开连接,子进程就死了,但是父进程一直在死循环,无法回收子进程,子进程就会成为僵尸进程,过多的僵尸进程会大量占用资源,所以我门还要写个函数专门回收僵尸进程

复制代码
// 信号处理函数:回收子进程
void child_data_handle(int signum)
{
    // 判断收到的信号是不是 SIGCHLD
    if(SIGCHLD == signum)
    {
        // 非阻塞方式回收所有退出的子进程
        waitpid(-1, NULL, WNOHANG);
    }
}

// 注册信号处理:当子进程退出时,内核自动调用上面的函数
signal(SIGCHLD, child_data_handle);

在 Linux 中:

  1. 子进程退出 时,内核不会直接销毁它,而是保留进程信息(PID、退出状态),变成僵尸进程
  2. 父进程必须调用 wait() / waitpid() 读取子进程退出状态,内核才会彻底删除子进程。
  3. 如果父进程不回收,僵尸进程会一直占用系统 PID 资源。
  4. 子进程退出时,内核会向父进程发送 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 非阻塞模式:如果没有子进程退出,函数立刻返回,不卡住父进程

关键特性:

  1. 非阻塞(WNOHANG)
    • 父进程该干嘛干嘛,不会因为等子进程而卡住。
  2. 自动回收
    • 只要有子进程退出,内核发信号 → 调用函数 → 立刻回收。

④注册信号处理: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, ...) 会回收所有已经退出的子进程。

执行结果

相关推荐
网络安全许木2 小时前
自学渗透测试第15天(基础复习与漏洞原理入门)
linux·网络安全·渗透测试·kali linux
Hello World . .2 小时前
linux驱动编程2 : uboot、Linux内核、rootfs来源及制作流程
linux·运维·服务器
观无2 小时前
Modbus RTU 与 Modbus TCP 温湿度采集
网络·网络协议·tcp/ip
啦啦啦_99992 小时前
1. Linux常用命令
linux·运维·服务器
大白菜和MySQL2 小时前
openEuler-20.03-LTS系统 nextcloud网盘搭建
linux
Harvy_没救了2 小时前
【Linux】Shell指令中的变量
linux·运维·服务器
Deitymoon2 小时前
linux——TCP多线程并发服务器
linux·服务器·tcp/ip
航Hang*2 小时前
Windows Server 配置与管理——第7章:配置DNS服务器
运维·服务器·网络·windows·安全·虚拟化
senijusene3 小时前
IMX6ULL Linux 驱动开发流程:从环境搭建到系统启动与内核编译
linux·运维·驱动开发