0203 网络资源的多进程处理
专栏内容:
- postgresql使用入门基础
- 手写数据库toadb
- 并发编程
个人主页:我的主页
管理社区:开源数据库
座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物.
一、概述
上一章节介绍了堆栈资源,文件在父子进程中的表现,在应用程序中还有一类经常用到同样非常重要的资源,它就是网络套接字。
二、资源创建场景
当我们需要搭建一个网络服务器时,很多时候是多任务协作处理高并发的客户端请求,可能是如下几种模型。
接待-工作者模式
- 一个任务负责监听来自客户端的连接请求;
- 当有客户端连接请求时,建立连接,将新的客户端连接分发给工作者任务;
- 工作者任务负责接收客户端的消息,处理,并响应客户端;
- 一般只有一个负责接待的任务,同时会有多个工作者任务,并且根据并发多少不断增减工作者任务的数量;
监听-接待工作者模式
- 一般由主任务初始化服务端并启动监听;
- 然后再由主任务启动多个工作者任务,数量一般是CPU核的数倍;
- 每个工作者任务都继承了这个正在监听的套接字;
- 当有客户端连接请求时,在工作者任务中进行accept,建立与客户端的连接;
- 每个工作者任务可以对应多个客户端,无阻塞方式处理来自多个客户端的消息;
大家可以从两个网络架构中看出,第一种模式应适于独占的式的应用;而第二种模式更适和与互联网应用。
三、套接字的继承
在父子进程的多任务架构中,父进程创建的网络套接字,fork出来子进程后,子进程是完全进行了复制。
3.1 验证代码
下面我们就以第二种网络模型为例进行验证。
代码如下:
/*
* ex020302_netprocess.c
*/#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<arpa/inet.h>#include<sys/epoll.h>#include<fcntl.h>#include<errno.h>#defineMAX_EVENTS10#defineBUFFER_SIZE1024#definePORT5809voidinitializeServerNet();voidcloseServerFd();voiddispatchLoop();voidsubprocess();int listen_fd;intmain(int argc ,char*argv[]){initializeServerNet();subprocess();subprocess();dispatchLoop();closeServerFd();return0;}voidsubprocess(){int pid =-1;
pid =fork();if(pid <0){printf("fork error[%s]\n",strerror(errno));exit(-1);}elseif(pid >0){// parent.return;}else{// child dispatchLoop();exit(0);}}voidinitializeServerNet(){structsockaddr_in server_addr;// 创建监听socket
listen_fd =socket(AF_INET, SOCK_STREAM,0);if(listen_fd ==-1){perror("socket");exit(EXIT_FAILURE);}// 绑定地址和端口
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port =htons(PORT);if(bind(listen_fd,(structsockaddr*)&server_addr,sizeof(server_addr))==-1){perror("bind");close(listen_fd);exit(EXIT_FAILURE);}// 开始监听if(listen(listen_fd, SOMAXCONN)==-1){perror("listen");close(listen_fd);exit(EXIT_FAILURE);}}voidcloseServerFd(){close(listen_fd);}voiddispatchLoop(){int conn_fd;// 主循环 while(1){// 新的连接
conn_fd =accept(listen_fd,NULL,NULL);if(conn_fd ==-1){printf("[%d] accept wakeup, but failure.\n",getpid());sleep(1);continue;}printf("[%d] accept a new client [%d]. \n",getpid(), conn_fd);close(conn_fd);}closeServerFd();}
说明
- 在主进程中创建网络套接字,绑定地址,并启动监听;
- 创建多个工作子进程;
- 工作子进程中继承了监听套接字;
- 每个工作子进程可以独立与客户端建立连接,并处理消息;
这里会创建两个子进程,在父进程和子进程中都会对同一个socket监听连接请求。
3.2 网络客户端
为了方便验证,我们编写一个简单的客户端程序。
/*
* ex020302_client.c
*/#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<arpa/inet.h>#defineSERVER_IP"127.0.0.1"#defineSERVER_PORT4808#defineBUFFER_SIZE1024intmain(int argc,char*argv[]){int sockfd;structsockaddr_in server_addr;char buffer[BUFFER_SIZE]={0};constchar*message ="Hello, Server!";int port = SERVER_PORT;if(argc >1){
port =atoi(argv[1]);}// 创建套接字 if((sockfd =socket(AF_INET, SOCK_STREAM,0))<0){perror("socket creation failed");exit(EXIT_FAILURE);}// 配置服务器地址信息
server_addr.sin_family = AF_INET;
server_addr.sin_port =htons(port);// 将IP地址从字符串转换为二进制形式 if(inet_pton(AF_INET, SERVER_IP,&server_addr.sin_addr)<=0){perror("Invalid address/ Address not supported");close(sockfd);exit(EXIT_FAILURE);}// 连接到服务器 if(connect(sockfd,(structsockaddr*)&server_addr,sizeof(server_addr))<0){perror("Connection Failed");close(sockfd);exit(EXIT_FAILURE);}for(int i =0; i <20; i++){// 发送消息到服务器 send(sockfd, message,strlen(message),0);printf("Message sent: %s\n", message);// 接收服务器的响应 int bytes_received =recv(sockfd, buffer, BUFFER_SIZE -1,0);if(bytes_received <0){perror("Error in receiving");}elseif(bytes_received ==0){printf("Server closed the connection\n");}else{
buffer[bytes_received]='\0';// 确保字符串以空字符结尾 printf("Message received from server: %s\n", buffer);}sleep(1);}// 关闭套接字 close(sockfd);return0;}
说明
- 客户端带一个参数,是服务端的端口号;
- 创建socket,并且与服务端(IP:port)连接;
- 不断发送和接收消息,重复20次;
- 实际上服务端只是接收建立连接,不会接收和发送响应,这已经达到了测试的目的;
3.3 结果验证
- 编译服务端,并且运行
这里没有使用deamon后台服务的方式运行,会停在这里。
[senllang@hatch ex02]$ gcc ex020105_forksocket.c -o test[senllang@hatch ex02]$ ./test
- 编译客户端,并运行
在另外一个终端编译和运行网络客户端程序;
服务端默认的端口号是5809,作为参数输入。
[senllang@hatch ex02]$ gcc ex020302_client.c -o client_opt
[senllang@hatch ex02]$ ./client_opt 5809
Message sent: Hello, Server!
Error in receiving: Connection reset by peer
[senllang@hatch ex02]$ ./client_opt 5809
Message sent: Hello, Server!
Error in receiving: Connection reset by peer
[senllang@hatch ex02]$ ./client_opt 5809
Message sent: Hello, Server!
Error in receiving: Connection reset by peer
[senllang@hatch ex02]$ ./client_opt 5809
Message sent: Hello, Server!
Error in receiving: Connection reset by peer
[senllang@hatch ex02]$ ./client_opt 5809
Message sent: Hello, Server!
Error in receiving: Connection reset by peer
[senllang@hatch ex02]$ ./client_opt 5809
Message sent: Hello, Server!
Error in receiving: Connection reset by peer
为了模拟多客户端的场景,我们将客户端启动多次;
可以看到客户端启动后,打印了发送的消息,之后就退出了,
因为服务端与客户端建立连接成功后,随即就关闭了连接。
- 运行结果
可以看到服务端打印的信息。
[1205954] accept a new client [4].
[1205955] accept a new client [4].
[1205956] accept a new client [4].
[1205954] accept a new client [4].
[1205955] accept a new client [4].
[1205956] accept a new client [4].
服务端启动了三个进程,可以看到三个进程的PID分别为
1205954
,
1205955
,
1205956
,它们都可以收到来自客户端的连接请求。
同时出现了一件很有意思的事情,三个进程轮流进行处理连接请求,这里主要避免了惊群的问题。
四、总结
本节主要分享了网络套接字在父子进程中的继承的情况。
可以通过验证发现,父启动监听后,此时创建子进程,在子进程中也继承了监听套接字,它也可以与客户端建立连接;而父进程中的监听套接字,也仍然可以与客户端建立连接。
当然其它网络操作步骤也是一样的,在父子进程中相同的套接字都会收到相同的网络事件,但最终只有一个进行处理,这样就带来一个问题,其它不处理事件的套接字会被频繁唤醒。
结尾
非常感谢大家的支持,在浏览的同时别忘了留下您宝贵的评论,如果觉得值得鼓励,请点赞,收藏,我会更加努力!
作者邮箱:study@senllang.onaliyun.com
如有错误或者疏漏欢迎指出,互相学习。
注:未经同意,不得转载!
版权归原作者 韩楚风 所有, 如有侵权,请联系我们删除。