> 技术文档 > 【linux 多进程并发】0203 网络资源的多进程处理,子进程完全继承网络套接字,避免“惊群”问题_继承套接字

【linux 多进程并发】0203 网络资源的多进程处理,子进程完全继承网络套接字,避免“惊群”问题_继承套接字


0203 网络资源的多进程处理

专栏内容

  • postgresql使用入门基础
  • 手写数据库toadb
  • 并发编程

个人主页:我的主页
管理社区:开源数据库
座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物.

✅ 🔥🔥🔥重大消息🔥🔥🔥 ❤️❤️❤️❤️ 关注公众号【开源无限】可免费领取《手写数据库内核toadb》源代码一份 ❤️❤️❤️❤️

一、概述


上一章节介绍了堆栈资源,文件在父子进程中的表现,在应用程序中还有一类经常用到同样非常重要的资源,它就是网络套接字。

二、资源创建场景


当我们需要搭建一个网络服务器时,很多时候是多任务协作处理高并发的客户端请求,可能是如下几种模型。

接待-工作者模式

  • 一个任务负责监听来自客户端的连接请求;
  • 当有客户端连接请求时,建立连接,将新的客户端连接分发给工作者任务;
  • 工作者任务负责接收客户端的消息,处理,并响应客户端;
  • 一般只有一个负责接待的任务,同时会有多个工作者任务,并且根据并发多少不断增减工作者任务的数量;

在这里插入图片描述

监听-接待工作者模式

  • 一般由主任务初始化服务端并启动监听;
  • 然后再由主任务启动多个工作者任务,数量一般是CPU核的数倍;
  • 每个工作者任务都继承了这个正在监听的套接字;
  • 当有客户端连接请求时,在工作者任务中进行accept,建立与客户端的连接;
  • 每个工作者任务可以对应多个客户端,无阻塞方式处理来自多个客户端的消息;

在这里插入图片描述

大家可以从两个网络架构中看出,第一种模式应适于独占的式的应用;而第二种模式更适和与互联网应用。

三、套接字的继承


在父子进程的多任务架构中,父进程创建的网络套接字,fork出来子进程后,子进程是完全进行了复制。

3.1 验证代码

下面我们就以第二种网络模型为例进行验证。

代码如下:

/* * ex020302_netprocess.c */#include  #include  #include  #include  #include  #include  #include  #include   #define MAX_EVENTS 10 #define BUFFER_SIZE 1024 #define PORT 5809void initializeServerNet();void closeServerFd();void dispatchLoop();void subprocess();int listen_fd;int main(int argc ,char *argv[]){initializeServerNet(); subprocess(); subprocess(); dispatchLoop(); closeServerFd();return 0;}void subprocess(){ int pid = -1;pid = fork();if(pid < 0){printf(\"fork error[%s]\\n\",strerror(errno));exit(-1);}else if(pid > 0){// parent.return;}else {// child  dispatchLoop();exit(0);}}void initializeServerNet(){ struct sockaddr_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, (struct sockaddr*)&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); }}void closeServerFd(){ close(listen_fd); }void dispatchLoop(){ 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  #include  #include  #include  #include   #define SERVER_IP \"127.0.0.1\" #define SERVER_PORT 4808 #define BUFFER_SIZE 1024 int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in server_addr; char buffer[BUFFER_SIZE] = {0}; const char *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, (struct sockaddr *)&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\"); } else if (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); return 0; }

说明

  • 客户端带一个参数,是服务端的端口号;
  • 创建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 5809Message sent: Hello, Server!Error in receiving: Connection reset by peer[senllang@hatch ex02]$ ./client_opt 5809Message sent: Hello, Server!Error in receiving: Connection reset by peer[senllang@hatch ex02]$ ./client_opt 5809Message sent: Hello, Server!Error in receiving: Connection reset by peer[senllang@hatch ex02]$ ./client_opt 5809Message sent: Hello, Server!Error in receiving: Connection reset by peer[senllang@hatch ex02]$ ./client_opt 5809Message sent: Hello, Server!Error in receiving: Connection reset by peer[senllang@hatch ex02]$ ./client_opt 5809Message 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
如有错误或者疏漏欢迎指出,互相学习。

注:未经同意,不得转载!