C++网络编程(六)代码示例---简单的多进程并发服务器
在服务器开发中,处理多个客户端连接是一个常见需求。多进程模型是一种经典的并发处理方式,它通过创建子进程来分担任务,提高服务器的吞吐量和响应能力。本文将基于技术笔记,详细解析多进程服务器的核心概念和实现细节,帮助您理解其工作原理。
进程的特点(有血缘关系的进程)
在多进程模型中,进程通常通过 fork()系统调用创建子进程,这些进程之间存在血缘关系(即父子关系)。以下是关键特点:
-
创建子进程:使用
fork()函数创建子进程。父进程调用fork()后,会复制自身创建一个新的子进程。子进程与父进程几乎完全相同,但拥有独立的进程ID。 -
虚拟地址空间复制:子进程会复制父进程的整个虚拟地址空间,包括用户区和内核区。这意味着:
-
用户区:复制堆区、栈区、全局数据区和代码区。子进程初始时拥有父进程数据的副本,但后续修改互不影响(例如,父进程修改数据不会影响子进程)。
-
内核区:复制文件描述符表等资源。这使得子进程可以继承父进程打开的文件或网络连接。
-
-
数据关系:父进程的数据在创建子进程时被复制,但之后各自独立。如果父进程修改了某些数据(如变量),子进程不会受到影响,因为它们运行在隔离的地址空间中。
-
资源回收:子进程结束后,需要通过父进程回收资源(如使用
wait()或waitpid()系统调用),以避免僵尸进程的产生。父进程负责监控和清理子进程的资源,确保系统稳定性。
这些特点使得多进程模型在并发处理中非常有效,但需要注意内存开销和进程间通信的复杂性。
多进程版的服务器程序实现
在多进程服务器中,服务器端需要处理监听客户端连接和与客户端通信两个主要任务。以下是典型的实现方式:
-
服务器端功能:
-
监听连接:服务器父进程调用
accept()函数监听端口,等待客户端连接。这是一个阻塞操作,意味着父进程会一直等待直到有客户端连接。 -
处理通信:每建立一个客户端连接,服务器就会创建一个子进程专门负责与该客户端通信。这样,多个客户端可以同时被服务,而父进程继续监听新连接。
-
-
进程分工:
-
父进程(1个):专职监听。它运行在一个循环中,不断调用
accept()来接受新连接。一旦有连接到来,父进程调用fork()创建子进程,然后返回监听状态。 -
子进程(多个):专职通信。每个子进程负责与一个客户端进行数据交换:
-
接收数据:使用
read()或recv()函数(默认阻塞)从客户端读取数据。 -
发送数据:使用
write()或send()函数(默认阻塞)向客户端发送响应。子进程在处理完通信后正常退出,由父进程回收资源。
-
-
这种设计的好处是简单易懂,且能有效利用多核CPU。然而,它也有局限性,如进程创建和销毁的开销较大,以及需要 careful 处理进程间同步和资源管理。在实际应用中,常结合信号处理(如 SIGCHLD)来优化资源回收
服务端
//服务端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h> //多进程头文件
#include<arpa/inet.h> //套接字头文件
#include<signal.h>
//子进程通信函数
void working(int cfd);
//子进程回收
void recycle(int signum);
int main ()
{
//1.创建客户端插座
int lfd =socket(AF_INET,SOCK_STREAM,0);
//2.绑定插座的端口与ip
struct sockaddr_in addr;
//设定地址族
addr.sin_family=AF_INET;
//设定端口
addr.sin_port=htons(9999);
//设定ip
addr.sin_addr.s_addr=INADDR_ANY;
int ret=bind(lfd,(struct sockaddr*)&addr,sizeof(addr));
//3.启动插座
ret=listen(lfd,128);
//4.建立连接
struct sockaddr_in cli;
int clilen=sizeof(cli);
//注册信号
//通过信号捕捉来控制回收子进程的时机
struct sigaction act;
act.sa_handler=recycle;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD,&act,NULL);
while(1)
{
int cfd=accept(lfd,(struct sockaddr*)&cli,&clilen);
if (cfd==-1)
{
perror("建立连接失败,重试尝试连接中...\n");
continue;
}
//连接建立成功后输出检验一下
char myip[24];
printf("客户端ip = %s,端口 = %d\n"
,inet_ntop(AF_INET,&cli.sin_addr.s_addr,myip,sizeof(myip))
,ntohs(cli.sin_port)
);
//连接已建立,开一个子进程去通信
pid_t pid=fork();
if (pid==0)//子进程进入
{
//子进程关闭监听文件描述符(因为子进程用cfd通信就行了)
close(lfd);
//和客户端通信
working(cfd);
//退出子进程
exit(0);
}
else
{
//父进程关闭通信文件描述符(因为通信的cfd交给子进程去干了,父不需要cfd)
close(cfd);
}
}
return 0;
}
void working (int cfd)
{
while (1)
{
//接受客户端数据
char buf[1024];
memset(buf,0,sizeof(buf));
int len=read(cfd,buf,sizeof(buf));
if (len==0)
{
printf("客户端断开连接...\n");
break;
}
else if(len==-1)
{
perror("读取异常\n");
break;
}
else
{
printf("从客户端读到了数据,内容为:%s\n",buf);
//回复数据
write(cfd,buf,len);
}
}
//关闭客户端通信文件
close(cfd);
}
//子进程回收
void recycle(int signum)
{
while(1)
{
pid_t pid=waitpid(-1,NULL,WNOHANG); //设置为非阻塞式
//回收成功返回>0
if (pid>0)
{
//这里要加\n,不加这个print不出来
//因为linux是一行一行输出到终端的
printf("子进程被回收了,pid = %d\n",pid);
}
else break;
}
}
客户端
//客户端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
int main ()
{
//1.创建客户端插座
int cfd=socket(AF_INET,SOCK_STREAM,0);
if (cfd==-1)
{
perror("socket创建失败\n");
exit(0);
}
//2.连接服务器
struct sockaddr_in addr;
addr.sin_family=AF_INET;//ipv4
addr.sin_port=htons(9999);//端口,转为网络字节序
inet_pton(AF_INET,"192.168.88.129",&addr.sin_addr.s_addr);//ip转换为网络字节序
int ret=connect(cfd,(struct sockaddr*)&addr,sizeof(addr));
if (ret==-1)
{
perror("连接失败\n");
exit(0);
}
//3.通信
int num=0;
while (1)
{
//发送数据
char buf[1024];
sprintf(buf,"这是来自客户端的数据,%d,......",num++);
send(cfd,buf,strlen(buf)+1,0);
//接受数据
memset(buf,0,sizeof(buf));
int len=recv(cfd,buf,sizeof(buf),0);//没数据时阻塞在这里
//判断读写情况
if (len==0)
{
printf("服务器端已断开连接\n");
break;
}else if(len>0)
{
printf ("收到了服务端的回复,内容为: %s\n",buf);
}
else
{
perror("接受数据失败\n");
break;
}
sleep(1);
}
//6.断开连接
close(cfd);//通信编号
return 0;
}
更多推荐


所有评论(0)