使用多进程并发服务器时要考虑以下几点:
父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
系统内创建进程个数(与内存大小相关)
进程创建过多是否降低整体服务性能(进程调度)
#include
#include
#include
#include
#include
#include "wrap.h"
void free_process(int sig)
{
pid_t pid;
while(1)
{
//如果设置了选项 WNOHANG,而调用中
//waitpid() 发现没有已退出的子进程可等待,则返回 0;
pid = waitpid(-1,NULL,WNOHANG);
if(pid <=0 )// pid<=0 表示无待回收子进程
{
break;
}
else
{
printf("child pid =%d\n",pid);
}
}
}
int main(int argc, char *argv[])
{
sigset_t set;
sigemptyset(&set);// //将set集合置空
/* 1) 子进程终止时
* 2) 子进程接收到SIGSTOP信号停止时
* 3) 子进程处在停止态,接受到SIGCONT后唤醒时
*/
sigaddset(&set,SIGCHLD);//将SIGCHLD信号加入到set集合
//信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞
//(在信号发生时记住它,直到进程准备好时再将信号通知进程)防止fork()前子进程退出导致信号丢失
sigprocmask(SIG_BLOCK,&set,NULL);
//创建套接字,绑定
int lfd = tcp4bind(8008,NULL);
//监
Listen(lfd,128);
//提取
//回射
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
while(1)
{
//进程分工:
//子进程:处理客户端请求(cfd)
//父进程:继续监听新连接(lfd)
//关键资源管理:
//子进程关闭lfd:避免文件描述符泄漏
//父进程关闭cfd:减少引用计数,客户端断开时子进程可正常退出
char ip[16]="";
//提取连接,
int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);
printf("\nnew client ip=%s port=%d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),ntohs(cliaddr.sin_port));
//fork创建子进程
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("");
exit(0);
}
else if(pid == 0)//子进程
{
// 关闭监听套接字(子进程不需要)
close(lfd);
while(1)
{
char buf[1024]="";
int n = read(cfd,buf,sizeof(buf));
if(n < 0)
{
perror("");
close(cfd);
exit(0);
}
else if(n == 0)//对方关闭j
{
printf("\nclient close\n");
close(cfd);
exit(0);
}
else
{
struct sockaddr_in remote_addr;
socklen_t len = sizeof(remote_addr);
getpeername(cfd, (struct sockaddr *)&remote_addr, &len);
int remote_port = ntohs(remote_addr.sin_port);
printf("客户端%d:%s",remote_port,buf);
write(cfd,buf,n);
// exit(0);
}
}
}
else//父进程
{
// 关闭客户端套接字(父进程不需要)
close(cfd);
//回收
//注册信号回调
struct sigaction act;
act.sa_flags =0;
act.sa_handler = free_process;//free_process函数指针传给act结构体
sigemptyset(&act.sa_mask);//把信号阻赛集清空
sigaction(SIGCHLD,&act,NULL);//遇到SIGCHLD信号调用free_process函数
sigprocmask(SIG_UNBLOCK,&set,NULL);//:从信号阻塞集合中删除 set 信号集
/*整个代码意思就是在注册回调函数之前先把SIGCHLD信号阻塞,回调函数注册好了
* 之后,在从set阻塞信号集中删除SIGCHLD信号,使得sigaction函数能够捕捉到SIGCHLD信号,从而调用free_process函数回收进程资源*/
}
}
//关闭
return 0;
}
在 Linux 系统中,当父进程通过 fork()
创建子进程时,子进程会复制父进程的文件描述符表,包括 socket 描述符。这种复制并非创建新的 socket 资源,而是共享相同的底层文件表项(即内核中的打开文件结构)。
文件描述符的复制机制
fd=3
,子进程的 fd=3
也指向同一个 socket 对象。父子进程对描述符的操作独立性
read
/write
/close
),但操作会影响同一个 socket 资源:
read
返回 0)。fd
不影响父进程的 fd
,但会减少引用计数。close(cfd)
),仅保留监听套接字。close(lfd)
),仅保留客户端连接描述符。客户端使用:
nc 127.0.0.1 8008
模拟客户端链接服务器
结果显示如下:
在使用线程模型开发服务器时需考虑以下问题:
调整进程内最大文件描述符上限
线程如有共享数据,考虑线程同步
服务于客户端线程退出时,退出处理。(退出值,分离态)
系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU
#include
#include
#include
#include
#include
#include
#include
#include "wrap.h"
// 作用:封装客户端连接信息
// 设计意图:避免线程参数传递时出现竞争(主线程的accept循环与线程启动存在时间差)
// 动态内存分配:每个连接独立分配malloc,确保线程安全(避免共用栈变量)
typedef struct c_info
{
int cfd;
struct sockaddr_in cliaddr;
}CINFO;
void* client_fun(void *arg);
int main(int argc, char *argv[])
{
if(argc < 2)
{
printf("argc < 2??? \n ./a.out 8000 \n");
return 0;
}
//PTHREAD_CREATE_DETACHED:线程自动回收资源,无需主线程调用 pthread_join
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
short port = atoi(argv[1]);
int lfd = tcp4bind(port,NULL);//创建套接字 绑定
Listen(lfd,128);
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
CINFO *info;
while(1)
{
// Accept():阻塞等待新连接,返回客户端socket (cfd)
// 动态创建CINFO对象存储客户端信息
// pthread_create:启动线程处理连接,传入info指针
int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);
char ip[16]="";
pthread_t pthid;
info = malloc(sizeof(CINFO));
info->cfd = cfd;
info->cliaddr= cliaddr;
pthread_create(&pthid,&attr,client_fun,info);
}
return 0;
}
void* client_fun(void *arg)
{
CINFO *info = (CINFO *)arg;
char ip[16]="";
printf("new client ip=%s port=%d\n",inet_ntop(AF_INET,&(info->cliaddr.sin_addr.s_addr),ip,16),ntohs(info->cliaddr.sin_port));
while(1)
{
char buf[1024]="";
int count=0;
count = read(info->cfd,buf,sizeof(buf));
if(count < 0)
{
perror("");
break;
}
else if(count == 0)
{
printf("client close\n");
break;
}
else
{
printf("%s\n", buf);
write(info->cfd,buf,count);
}
}
close(info->cfd);
free(info);
}
客户端使用:
nc 127.0.0.1 8008
模拟客户端链接服务器
结果显示如下:
总结:
进程高并发
fork()
)耗时长。线程高并发
进程高并发
线程高并发
进程高并发
memcpy
),延迟高。线程高并发
相比之下,这两种方式在高并发服务器场景中各具优势,因而催生了多路I/O转接服务器(select、poll、epoll)的设计理念。其核心思想是将I/O事件的轮询工作交给内核,应用程序仅在事件就绪时被唤醒处理,从而避免为每个连接创建独立线程/进程的开销。
下一篇文章将详细讲解select、poll和epoll三种I/O模型。