云计算百科
云计算领域专业知识百科平台

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

子进程做什么?

  • 打印谁连进来了
  • 调用 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, …) 会回收所有已经退出的子进程。

    执行结果

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » linux——TCP多进程并发服务器
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!