【网络篇】第九篇——多线程版的TCP网络程序

news2025/4/9 5:37:26

多进程与多线程对比

多进程

多线程

多线程版的TCP网络程序


多进程与多线程对比

多进程

优点

  • 可以处理多个用户
  • 易于边写
  • 稳定,因为进程具有独立性

缺点

  • 连接来了之后才创建进程,性能太低
  • 多进程服务器特别吃资源,而且同时服务的客户有上限,上限也很容易达到
  • 进程越多,CPU在调度时选择一个进程的周期会变长,客户等待的时间就变长。也就是切换成本大,影响性能。

多线程

多线程版本的程序同样也有多进程版本的几个缺点,但是相对于进程来说,创建线程的代价要小很多,而且调度线程比调度进程的粒度要小,这样就可以降低成本,提高性能。
但是线程还有一个缺点就是线程不稳定,一个线程的退出会导致主线程直接退出。

总结一下就是,进程的开销要远远大于线程,所以,如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的CPU资源,比如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。

不论是多进程还是多线程都是来了连接请求之后才创建进程或线程,这个问题我们可以用进程池和线程池来解决。

多线程版的TCP网络程序

当服务进程调用accept函数获取到一个新连接后,就可以直接创建一个线程,让该线程为对应客户端提供服务。

当然,主线程(服务进程)创建出新线程后,也是需要等待新线程退出的,否则也会造成类似于僵尸进程这样的问题。但对于线程来说,如果不想让主线程等待新线程退出,可以让创建出来的新线程调用pthread_detach函数进行线程分离,当这个线程退出时系统会自动回收该线程所对应的资源。此时主线程(服务进程)就可以继续调用accept函数获取新连接,而让新线程去服务对应的客户端。

各个线程共享同一张文件描述符表

文件描述符表维护的是进程与文件之间的对应关系,因此一个进程对应一张文件描述符表。而主线程创建出来的新线程依旧属于这个进程,因此创建线程时并不会为该线程创建独立的文件描述符表,所有的线程看到的都是同一张文件描述符表。

因此当服务进程(主线程)调用accept函数获取到一个文件描述符后,其他创建的新线程是能够直接访问这个文件描述符的。

需要注意的是,虽然新线程能够直接访问主线程accept上来的文件描述符,但此时新线程并不知道它所服务的客户端对应的是哪一个文件描述符,因此主线程创建新线程后需要告诉新线程对应应该访问的文件描述符的值,也就是告诉每个新线程在服务客户端时,应该对哪一个套接字进行操作。

参数结构体

实际新线程在为客户端提供服务时就是调用Service函数,而调用Service函数时是需要传入三个参数的,分别是客户端对应的套接字、IP地址和端口号。因此主线程创建新线程时需要给新线程传入三个参数,而实际在调用pthread_create函数创建新线程时,只能传入一个类型为void*的参数。

这时我们可以设计一个参数结构体Param,此时这三个参数就可以放到Param结构体当中,当主线程创建新线程时就可以定义一个Param对象,将客户端对应的套接字、IP地址和端口号设计进这个Param对象当中,然后将Param对象的地址作为新线程执行例程的参数进行传入。

此时新线程在执行例程当中再将这个void*类型的参数强转为Param*类型,然后就能够拿到客户端对应的套接字,IP地址和端口号,进而调用Service函数为对应客户端提供服务。

class Param
{
public:
	Param(int sock, std::string ip, int port)
		: _sock(sock)
		, _ip(ip)
		, _port(port)
	{}
	~Param()
	{}
public:
	int _sock;
	std::string _ip;
	int _port;
};

文件描述符关闭的问题

 由于此时所有线程看到的都是同一张文件描述符表,因此当某个线程要对这张文件描述符表做某种操作时,不仅要考虑当前线程,还要考虑其他线程。

  • 对于主线程accept上来的文件描述符,主线程不能对其进行关闭操作,该文件描述符的关闭操作应该又新线程来执行。因为是新线程为客户端提供服务的,只有当新线程为客户端提供的服务结束后才能将该文件描述符关闭。
  • 对于监听套接字,虽然创建出来的新线程不必关心监听套接字,但新线程不能将监听套接字对应的文件描述符关闭,否则主线程就无法从监听套接字当中获取新连接了。

Service函数定义为静态成员函数

由于调用pthread_create函数创建线程时,新线程的执行例程是一个参数为void*,返回值为void*的函数。如果我们要将这个执行例程定义到类内,就需要将其定义为静态成员函数,否则这个执行例程的第一个参数是隐藏的this指针。

在线程的执行例程当中会调用Service函数,由于执行例程是静态成员函数,静态成员函数无法调用非静态成员函数,因此我们需要将Service函数定义为静态成员函数。恰好Service函数内部进行的操作都是与类无关的,因此我们直接在Service函数前面加上一个static即可。

class TcpServer
{
public:
	static void* HandlerRequest(void* arg)
	{
		pthread_detach(pthread_self()); //分离线程
		//int sock = *(int*)arg;
		Param* p = (Param*)arg;

		Service(p->_sock, p->_ip, p->_port); //线程为客户端提供服务

		delete p; //释放参数占用的堆空间
		return nullptr;
	}
	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;
			
			Param* p = new Param(sock, client_ip, client_port);
			pthread_t tid;
			pthread_create(&tid, nullptr, HandlerRequest, p);
		}
	}
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
};

代码测试

 此时我们再重新编译服务端代码,由于代码当中用到了多线程,因此编译时需要携带上-pthread选项。此外,由于我们现在要监测的是一个个的线程,因此在监控时使用的不再是ps -axj命令,而是ps -aL命令。

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

 运行服务端,通过监控可以看到,此时只有一个服务线程,该服务线程就是主线程,它现在在等待客户端的连接到来。

当一个客户端连接到服务端后,此时主线程就会为该客户端构建一个参数结构体,然后创建一个新线程,将该参数结构体的地址作为参数传递给这个新线程,此时该新线程就能够从这个参数结构体当中提取出对应的参数,然后调用Service函数为该客户端提供服务,因此在监控当中显示了两个线程。

当第二个客户端发来连接请求时,主线程会进行相同的操作,最终再创建出一个新线程为该客户端提供服务,此时服务端当中就有了三个线程。

 由于为这两个客户端提供服务的也是两个不同的执行流,因此这两个客户端可以同时享受服务端提供的服务,它们发送给服务端的消息也都能够在服务端进行打印,并且这两个客户端也都能够收到服务端的回显数据。

 此时无论有多少个客户端发来连接请求,在服务端都会创建出相应数量的新线程为对应客户端提供服务,而当客户端一个个退出后,为其提供服务的新线程也就会相继退出,最终就只剩下最初的主线程仍在等待新连接的到来。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/5447.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(最新版2022版)剑指offer之排序题解

&#xff08;最新版2022版&#xff09;剑指offer之排序题解JZ3数组中重复的数字JZ51 数组中的逆序对JZ40 最小的K个数JZ41 数据流中的中位数JZ3数组中重复的数字 思路&#xff1a; 既然数组长度为nnn只包含了0到n−1n-1n−1的数字&#xff0c;那么如果数字没有重复&#xff0c…

qt C++中指针自动释放内存及程序中的内存操作、管理

程序加载到内存后代码存储到代码区&#xff0c;并将全局变量、静态变量初始化到全局/静态内存区&#xff0c;然后会分配2M左右的栈内存区用于存储局部变量&#xff0c;并在运行时根据需要可以在堆内存区(空闲内存区及硬盘的虚拟内存区)申请空间。 程序可使用的内存分区↓ 各基…

C++之Hello World

概览 编程语言历史 机器语言:00110101…最初始的计算机内部语言,不同机器使用的语言甚至不同 汇编语言:利用简单符号(a DB 7H…)对机器语言进行了一定的抽象,增加了可读性,更加人性化.在一定程度上仍然依赖硬件,属于低级的语言 高级语言:使用文字通过编译器被翻译为机器语言…

Vue中 引入使用 element-resize-detector 监听 Dom 元素 宽度、高度 变化

1. 前言 很多做pc端平台的小伙伴都遇到过这样一个问题&#xff1a;在做侧边栏菜单时会有一个收缩和展开的一个功能&#xff0c;在伸缩的过程中右边的页面的宽度就会随之改变。我上网查了查 &#xff0c;也动手试了试 window.onresize ()>{}。却不尽人意&#xff0c;因为它…

SVM 超平面计算例题

SVM Summary Example Suppose the dataset contains two positive samples x(1)[1,1]Tx^{(1)}[1,1]^Tx(1)[1,1]T andx(2)[2,2]Tx^{(2)}[2,2]^Tx(2)[2,2]T, and two negative samples x(3)[0,0]Tx^{(3)}[0,0]^Tx(3)[0,0]T and x(4)[−1,0]Tx^{(4)}[-1,0]^Tx(4)[−1,0]T. Please…

MySQL纯代码复习

前言 本文章是用于总结尚硅谷MySQL教学视频的记录文章&#xff0c;主要用于复习&#xff0c;非商用 原视频连接&#xff1a;https://www.bilibili.com/video/BV1iq4y1u7vj/?p21&spm_id_frompageDriver&vd_sourcec4ecde834521bad789baa9ee29af1f6c https://www.bilib…

C#重启 --- 枚举

第一部分 --- 枚举 枚举类型的本质其实就是在给整型数据加标签&#xff0c;当编译器遇到枚举类型标签的时候&#xff0c;编译器会自动获取标签对应的整型数据&#xff08;默认从0开始由上往下递增&#xff09; 枚举类型的使用方法&#xff1a; 1.枚举类型的类型名是由我们自己…

四.STM32F030C8T6 MCU开发之利用 TIM1+ADC1+DMA1 实现5路(3路外部电压模拟信号+内部2路信号)采集

四.STM32F030C8T6 MCU开发之利用 TIM1ADC1DMA1 实现5路&#xff08;3路外部电压模拟信号内部2路信号&#xff09;采集 文章目录四.STM32F030C8T6 MCU开发之利用 TIM1ADC1DMA1 实现5路&#xff08;3路外部电压模拟信号内部2路信号&#xff09;采集0.总体功能概述ADC 简介1.ADC硬…

数据结构《LinkeList 双向链表》

LinkeList LinkeList 的低层是由双向链表结构组成的&#xff0c;所有元素都是存放到单独的节点当中&#xff0c;通过地址引用将节点串联起来 因此在任意位置插入或删除元素时&#xff0c;都不在需要移动元素&#xff0c;效率较高 下面是双向链表的结构图&#xff1a; 在集合框…

【从零开始游戏开发】静态资源优化 | 全面总结 |建议收藏

你知道的越多&#xff0c;你不知道的越多 &#x1f1e8;&#x1f1f3;&#x1f1e8;&#x1f1f3;&#x1f1e8;&#x1f1f3; 点赞再看&#xff0c;养成习惯&#xff0c;别忘了一键三连哦 &#x1f44d;&#x1f44d;&#x1f44d; 文章持续更新中 &#x1f4dd;&#x1f4dd;…

C++智能指针

文章目录一、智能指针的目的和基本原理二、不带引用计数的智能指针2.1 auto_ptr2.2 scoped_ptr2.3 unique_ptr三、带引用计数的智能指针3.1 shared_ptr3.2 weak_ptr一、智能指针的目的和基本原理 一般new出来的对象会用普通指针引用&#xff0c;此时申请的堆上的资源需要我们手…

乐趣国学—品读《弟子规》中的“泛爱众”之道(上篇)

前言 “泛爱众”就是以广泛的爱心对待社会大众。人类生活是以爱心为纽带&#xff0c;没有爱心&#xff0c;人类生活就太痛苦不堪了。这个爱心从哪里来的&#xff1f;这个爱心就是孝心。孝道&#xff0c;正是培养爱心的第一步&#xff0c;一个连父母都不爱的人决不会真心爱他人&…

19.Feign 的工程化实例:eureka,ribbon,feign,hystrix(springcloud)

项目模型 项目结构 本实例创建model都是通过maven手动创建&#xff0c;依赖进行手动导入&#xff0c;好处是比使用springboot模板创建更加灵活&#xff0c;更方便的进行父子模块的管理。 1.创建父项目feign-project 2.对父项目feign-project的pom.xml&#xff0c;进行手动导入依…

Linux基础内容(10)—— 进程概念

目录 1.冯诺依曼体系结构 ​编辑1.冯诺依曼体系特点 2.cpu运算原理 3.数据传输 2.操作系统 1.操作系统管理的真相 2.操作系统与硬件的交互方式 3.操作系统与用户的交互方式 1.系统调用接口 2.用户对系统调用的使用 3.进程 1.进程的概念 2.Linux中的进程 3.与进程…

基于DJYOS的SPI驱动编写指导手册

1.贡献者列表 深圳市秦简计算机系统有限公司DJYOS驱动开发团队。 2.概述 DJYOS的DjyBus总线模型为IIC、SPI之类的器件提供统一的访问接口&#xff0c;SPIBUS模块是DjyBus模块的一个子模块&#xff0c;为SPI器件提供统一的编程接口&#xff0c;实现通信协议层与器件层的分离。…

Python 考试练习题 2

一、选择题 1、下列是 python 合法标识符的是&#xff08; B&#xff09;。 A. 2variable B. variable2 C. $anothervar D. if 2、在 python 中字符串的表示方式是&#xff08;D &#xff09;。 A.采用单引号包裹 B.采用双引号包裹 C.采用三重单引号包裹 D.ABC 都是 3、设有…

【浅学Linux】动态库与静态库的封装与使用

朋友们好&#xff0c;这里简单介绍一下LINUX学习中关于动态库与静态库的理解&#xff0c;以及站在封装和使用的角度去介绍是如何封装的&#xff1f;如何使用的&#xff1f; 文章目录一&#xff1a;动态库与静态库的理解二&#xff1a;静态库2.1&#xff1a;静态库的使用2.2&…

Sprite Editor

1、SpriteEditor SpriteEditor是精灵图片编辑器 它主要用于编辑2D游戏开发中使用的Sprite精灵图片 它可以用于编辑 图集中提取元素&#xff0c;设置精灵边框&#xff0c;设置九宫格&#xff0c;设置轴心&#xff08;中心&#xff09;点等等功能 2、Single图片编辑 Sprite Ed…

Docker原生网络、自定义网络、Docker容器通信、跨主机容器网络

Docker原生网络、自定义网络、Docker容器通信、跨主机容器网络Docker原生网络bridgeHostnoneDocker自定义网络自定义bridgeoverlaymacviandocker network所有基本命令Docker容器通信双冗余机制跨主机容器网络一些遗留错误解决错误1错误2错误3错误4Docker原生网络 docker安装时…

如何搭建node_exporter

如何搭建node_exporter 1.观看条件 1.假设你已经看过上一篇文章 《如何搭建普罗米修斯 Prometheus》 2.假设你已经会搭建普罗米修斯&#xff08;promethus&#xff09; 3.上面两个假设&#xff0c;只要满足一个。那你看这篇文章就没什么压力了 2.node_exporter是啥 node_…