【网络篇】第八篇——多进程版的TCP网络程序

2023/11/30 10:15:39

前言

多进程版的TCP网络程序

捕捉SIGCHLD信号

让孙子进程提供服务


前言

之前我们已经利用socket编程实现了一个单进程的TCP网络程序(tcp详解),但上一章遗留了一个问题,当我们再开启一个终端去连接服务端,可以发现的是第二个客户端不能和服务器正常通信了,除非我们第一个客户端退出之后,第二个客户端才能和服务器正常通信。可以注意到大部分的socket接口都是阻塞型的。实际上除非特别指定,几乎所有的IO接口(包括socket接口)都是阻塞型的就像我们之前实现的代码,在accept接受了一个请求之后就会一直在while循环里面尝试去read,而没有继续去调用accpet,导致不能接受到新的请求。

可以肯定的是这样的设计是不合理的,所以我们将之前的代码进行改进。

多进程版的TCP网络程序

可以将当前的单执行流服务器改为多进程版的服务器.

当服务端调用accept函数获取到新连接后不是由当前执行流为该连接提供服务,而是当前执行流调用fork函数创建子进程,然后让子进程为父进程获取到的连接提供服务。

由于父子进程是两个不同的执行流,当父进程调用fork创建出子进程后,父进程就可以继续从监听套接字获取新连接,而不用关心获取上来的连接是否服务完毕。

子进程继承父进程的文件描述符表

需要注意的是,文件描述符表是隶属于一个进程的, 子进程创建后会继承父进程的文件描述符表。比如父进程打开了一个文件,该文件对应的文件描述符是3,此时父进程创建的子进程的3号文件描述符也会指向这个打开的文件,而如果子进程再创建一个子进程,那么子进程创建的子进程的3号文件描述符也同样会指向这个打开的文件。

但当父进程创建子进程后,父子进程之间会保持独立性,此时父进程文件描述符表的变化不会影响子进程。最典型的代表就是匿名管道,父子进程在使用匿名管道进行通信时,父进程先调用pipe函数得到两个文件描述符,一个是管道读端的文件描述符,一个是管道写端的文件描述符,此时父进程创建出来的子进程就会继承这两个文件描述符,之后父子进程一个关闭管道的读端,另一个关闭管道的写端,这时父子进程文件描述符表的变化是不会相互影响的,此后父子进程就可以通过这个管道进行单向通信了。
对于套接字文件也是一样的,父进程创建的子进程也会继承父进程的套接字文件,此时子进程就能够对特定的套接字文件进行读写操作,进而完成对对应客户端的服务。

等待子进程问题

当父进程创建出子进程后,父进程是需要等待子进程退出的,否则子进程会变成僵尸进程,进而造成内存泄露,因此服务端创建子进程后需要调用wait或waitpid函数对子进程进行等待。

 阻塞式等待与非阻塞式等待:

  • 如果服务端采用阻塞的方式等待子进程,那么服务端还是需要等待服务完当前客户端,才能继续获取下一个连接请求,此时服务端仍然是以一种串行的方式为客户端提供服务。
  • 如果服务端采用非阻塞的方式等待子进程,虽然在子进程为客户端提供服务期间服务端可以继续获取新连接,但此时服务端就需要将所有子进程的PID保存下来,并且需要不断花费时间检测子进程是否退出。

总之,服务端要等待子进程退出,无论采用阻塞式等待还是非阻塞式等待,都不尽人意。此时我们可以考虑让服务端不等待子进程退出。

不等待子进程退出的方式

 让父进程不等待子进程退出,常见的方式有两种:

  • 捕捉SIGCHLD信号,将其处理动作设置为忽略。
  • 让父进程创建子进程,子进程再创建孙子进程,最后让孙子进程为客户端提供服务。

捕捉SIGCHLD信号

这里忘记的可以看我这边博客:信号

实际当子进程退出时会给父进程发送SIGCHLD信号了如果父进程将SIGCHLD信号进行捕捉,并将该信号的处理动作设置为忽略,此时父进程就只需要专心处理自己的工作,不必关心子进程了。

 该方式实现起来非常简单,也是比较推荐的一种做法。(客户端和服务端其他地方不变)

class TcpServer
{
public:
	void Start()
	{
		signal(SIGCHLD, SIG_IGN); //忽略SIGCHLD信号
		for (;;){
			//获取连接
			struct sockaddr_in peer;
			memset(&peer, '\0', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
			
			pid_t id = fork();
			if (id == 0){ //child
				//处理请求
				Service(sock, client_ip, client_port);
				exit(0); //子进程提供完服务退出
			}
		}
	}
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
};

 代码测试

 重新编译程序运行服务端后,可以通过以下监控脚本对服务进程进行监控。

while :; do ps axj | head -1 && ps axj | grep tcp_server | grep -v grep;echo "######################";sleep 1;done

此时可以看到,一开始没有客户端连接该服务器,此时服务进程只有一个,该服务进程就是不断获取新连接的进程,而获取到新连接后也是由该进程创建子进程为对应客户端提供服务的。

此时我们运行一个客户端,让该客户端连接服务器,此时服务进程就会调用fork函数创建出一个子进程,由该子进程为这个客户端提供服务。

如果再有一个客户端连接服务器,此时服务进程会再创建出一个子进程,让该子进程为这个客户端提供服务。

最重要的是,由于这两个客户端分别由两个不同的执行流提供服务,因此这两个客户端可以同时享受到服务,它们发送给服务端的数据都能够在服务端输出,并且服务端也会对它们的数据进行响应。

当客户端一个个退出后,在服务端对应为之提供服务的子进程也会相继退出,但无论如何服务端都至少会有一个服务进程,这个服务进程的任务就是不断获取新连接。

让孙子进程提供服务

我们也可以让服务端创建出来的子进程再次进行fork,让孙子进程为客户端提供服务, 此时我们就不用等待孙子进程退出了。

 命名说明:

  • 爷爷进程:在服务端调用accept函数获取客户端连接请求的进程。
  • 爸爸进程:由爷爷进程调用fork函数创建出来的进程。
  • 孙子进程:由爸爸进程调用fork函数创建出来的进程,该进程调用Service函数为客户端提供服务。

我们让爸爸进程创建完孙子进程后立刻退出,此时服务进程(爷爷进程)调用wait/waitpid函数等待爸爸进程就能立刻等待成功,此后服务进程就能继续调用accept函数获取其他客户端的连接请求。

不需要等待孙子进程退出

 而由于爸爸进程创建完孙子进程后就立刻退出了,因此实际为客户端提供服务的孙子进程就变成了孤儿进程,该进程就会被系统领养,当孙子进程为客户端提供完服务退出后系统会回收孙子进程,所以服务进程(爷爷进程)是不需要等待孙子进程退出的

关闭对应的文件描述符

 服务进程(爷爷进程)调用accpet函数获取到新连接后,会让孙子进程为该连接提供服务,此时服务进程已经将文件描述符继承给了爸爸进程,而爸爸进程又会调用fork函数创建出孙子进程,然后将文件描述符又继承给了孙子进程。

而父子进程创建后,他们各自的文件描述符表是独立的,不会互相影响。因此服务进程在调用fork函数后,服务进程就不需要再关心刚才从accept函数获取到的文件描述符了,此时服务进程就可以调用close函数将该文件描述符进行关闭。

同样的,对于爸爸进程和孙子进程来说,它们是不需要关心从服务进程(爷爷进程)继承下来的监听套接字的,因此爸爸进程可以将监听套接字关掉。

关闭文件描述符的必要性:

  • 对于服务进程来说,当它调用fork函数后就必须将从accept函数获取的文件描述符关掉。因为服务进程会不断调用accept函数获取新的文件描述符(服务套接字),如果服务进程不及时关掉不用的文件描述符,最终服务进程中可用的文件描述符就会越来越少。
  • 而对于爸爸进程和孙子进程来说,还是建议关闭从服务进程继承下来的监听套接字。实际就算它们不关闭监听套接字,最终也只会导致这一个文件描述符泄漏,但一般还是建议关上。因为孙子进程在提供服务时可能会对监听套接字进行某种误操作,此时就会对监听套接字当中的数据造成影响。
class TcpServer
{
public:
	void Start()
	{
		for (;;){
			//获取连接
			struct sockaddr_in peer;
			memset(&peer, '\0', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
			
			pid_t id = fork();
			if (id == 0){ //child
				close(_listen_sock); //child关闭监听套接字
				if (fork() > 0){
					exit(0); //爸爸进程直接退出
				}
				//处理请求
				Service(sock, client_ip, client_port); //孙子进程提供服务
				exit(0); //孙子进程提供完服务退出
			}
			close(sock); //father关闭为连接提供服务的套接字
			waitpid(id, nullptr, 0); //等待爸爸进程(会立刻等待成功)
		}
	}
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
};

服务器测试

重新编译程序运行客户端后,继续使用监控脚本对服务进程进行实时监控。

while :; do ps axj | head -1 && ps axj | grep tcp_server | grep -v grep;echo "######################";sleep 1;done

此时没有客户端连接服务器,因此也是只监控到了一个服务进程,该服务进程正在等待客户端的请求连接。

 此时我们运行一个客户端,让该客户端连接当前这个服务器,此时服务进程会创建出爸爸进程,爸爸进程再创建出孙子进程,之后爸爸进程就会立刻退出,而由孙子进程为客户端提供服务。因此这时我们只看到了两个服务进程,其中一个是一开始用于获取连接的服务进程,还有一个就是孙子进程,该进程为当前客户端提供服务,它的PPID为1,表明这是一个孤儿进程。

 当客户端全部退出后,对应为客户端提供服务的孤儿进程也会跟着退出,这时这些孤儿进程会被系统回收,而最终剩下那个获取连接的服务进程。


http://www.jnnr.cn/a/126529.html

相关文章

【词性的选择与所放位置练习题】but vs however

1. 改写训练 1.Alice was clearly the best candiate. However, she did not get the job. 2.The audience was small but they were clearly appreciative. 3.She has considerable musical ability but her technique is poor. 4.Nobody liked him. However, everybody agre…

XMLPath的基本使用

本文最开始发表于择维士社区 文章目录什么是XPathXPath基本格式XPath的表达式更多的预测格式XPath在Java中的使用示例获取一堆节点根据某个id获取节点:根据某个tag获取节点:参考什么是XPath XPath是一种用于在xml格式的内容中提取信息的方式. 它与从JSON中提取信息的JSONPath类…

C++之变量、数组、结构与枚举

变量 初始化变量的方式 C使得可以将大括号初始化器用于任何类型数据的初始化.这里介绍单值变量用大括号初始化. int tom{1}; int tom{1}; int tom{};//大括号内可以不包含值,这种情形变量将默认初始化为0 int tom{};常量 预处理方式: #define NUM 100 预处理使得编译前代码…

计算机毕业设计SSM电商后台管理系统【附源码数据库】

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

《封号码罗》关于js逆向猿人学第一题m值的获取[纯补环境](二十四)

网上有很多资料&#xff0c;包括视频都讲解了m值的生成方式&#xff0c;但是我自己总是看过之后&#xff0c;有很多疑惑&#xff0c;所以我自己再总结一遍。 抓包看看请求 m值得生成位置 用AST简单解混淆一下&#xff0c;源码就是整个混淆的js复制到本地文件 const parser r…

伴读计划丨《我们时代的神经症人格》:我是真的爱自己的宠物吗?

Hello&#xff0c; 这里是壹脑云读书圈&#xff0c;我是则则~ 大家有多久没有好好的读一本书了呢&#xff1f; 我做为一个爱买书又时常没有毅力坚持看完的一本书人&#xff08;又菜又有瘾&#xff09;&#xff0c;看着买回来的书在书桌上积灰&#xff0c;时不时会心生愧疚&a…

【Mongoose笔记】HTTP 服务器

【Mongoose笔记】HTTP 服务器 简介 Mongoose 笔记系列用于记录学习 Mongoose 的一些内容。 Mongoose 是一个 C/C 的网络库。它为 TCP、UDP、HTTP、WebSocket、MQTT 实现了事件驱动的、非阻塞的 API。 项目地址&#xff1a; https://github.com/cesanta/mongoose学习 下面…

HO-PEG-NH2,Hydroxyl-PEG-amine,32130-27-1,羟基peg氨基可修饰多肽

CAS编号为32130-27-1的化学试剂氨基聚乙二醇羟基&#xff0c;其英文名为HO-PEG-NH2&#xff0c;Hydroxyl-PEG-amine&#xff0c;它所属分类为Amine PEG Hydroxyl PEG。 peg试剂的分子量均可定制&#xff0c;有&#xff1a;HO-PEG 2000-NH2、羟基-聚乙二醇 20000-氨基、Hydroxy…

如何看待三测?天王级项目Aleo三测预期收益的深度解读

2021年出现了不少隐私项目引起了市场的关注&#xff0c;web3也正式以保护个人在互联网的隐私为开头为大家叙事的&#xff0c;不少隐私为主的新兴公链也为大家所知&#xff0c;隐私项目也获得了机构的大力支持&#xff0c;融资金额以及估值也相对其他项目来说&#xff0c;高出不…

BurpSuit官方实验室之目录穿越

BurpSuit实验室之目录穿越 这是BurpSuit官方的实验室靶场&#xff0c;以下将记录个人目录穿越共6个Lab的通关过程 Web Security Academy: Free Online Training from PortSwigger lab1&#xff1a; File Path traversal,simple case 文件路径遍历&#xff0c;简单情况 在…

知识提取-属性抽取-学习笔记

目录 Part 1&#xff1a;属性抽取基本描述 Part 2&#xff1a;属性抽取基本研究内容 2.1基于无监督的属性抽取方法 &#xff08;1.1&#xff09;基于规则的槽填充算法 &#xff08;1.2&#xff09;基于聚类的属性抽取方法 2.2. 基于依存关系的半监督的槽填充算法 2.3. 基…

Java宝塔部署实战后台管理系统CMS源码

大家好啊&#xff0c;我是测评君&#xff0c;欢迎来到web测评。 本期给大家带来一套Java开发的后台管理系统CMS源码。 技术架构 技术框架&#xff1a;Spring PublicCMS Mybatis HibernateValidator shiro mysql5.7运行环境&#xff1a;jdk8 IntelliJ IDEA maven 宝塔面…

Apache反向代理负载均衡

知识准备(apache)http://t.csdn.cn/dxqgw、(tomcat)http://t.csdn.cn/drkds依赖模块https://httpd.apache.org/docs/2.4 1.编辑D:\program\Apache24\conf\httpd.conf-->打开注释-->重启服务 LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_http_module …

基于stm32的秒表计时器设计系统Proteus仿真

资料编号&#xff1a;126 下面是相关功能视频演示&#xff1a; 126-基于stm32的秒表计时器设计系统Proteus仿真&#xff08;源码仿真全套资料&#xff09;功能讲解&#xff1a; 采用stm32单片机作为控制器&#xff0c;数码管来显示秒表时间&#xff0c;三个按键作为开始 清零…

java计算机毕业设计基于安卓Android的学生个人支出管理APP

项目介绍 基于APP的学生个人支出管理系统主要针对广大学生,本设计分为用户客户端和管理员后台管理,前台用户管理使用Android Studio制作,使用了JS、HTML和uniapp开发框架,后台管理使用JAVA&#xff1a;MySQL数据库来保存数据以及上传数据。MySQL体积小、速度快,为数据的存储和传…

有含金量的AI证书

文章目录证书展示企业认证介绍华为认证人工智能工程师课程内容我的总结和资料证书展示 这是华为云的AI认证&#xff1a; 这是阿里云的大数据认证&#xff1a; 企业认证介绍 一般的企业是办不起来认证的&#xff0c;大家肯定都知道的。本文主要介绍华为云的认证&#xff0c…

嵌入式单片机学习入门到大牛

更新时间:2022年5月15日 之前写了ARM+LINUX嵌入式学习路线,收到了很多同学的喜欢和支持,有点倍感惶恐,所以也是一直在更新补充,希望不要给信任我的同学以误导,我也会尽力帮大家解决疑惑。 ARM+嵌入式Linux学习路线 但是作为本科琢磨三四年单片机,毕业后第一份工作也是…

[附源码]计算机毕业设计JAVA潮流服饰网店平台

[附源码]计算机毕业设计JAVA潮流服饰网店平台 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybati…

记一次简单的网络通信遇到的问题点总结

目录&#xff1a; 问题1&#xff1a;服务端接收不到客户端发送的消息 问题2&#xff1a;客户端接收服务端消息不同步问题 先上完整且无误的代码&#xff1a; 服务端EchoServer&#xff1a; package part1;import java.io.*; import java.net.ServerSocket; import java.ne…

java网络故障报修系统J2EE

目 录 第一章 绪论 1 1.1 课题开发背景 1 1.2 课题研究意义 1 1.3 本课题主要工作 1 第二章 相关技术介绍 3 2.1 JSP技术 3 2.2 MySQL数据库 3 2.3 J2EE 技术 4 2.4 B/S架构 5 第三章 系统分析 8 3.1 系统可行性分析 8 3.2 系统设计目标…
最新文章