【计算机网络】UDP实战
其实经过这几天写的几种不同的UDP的简易客户端与服务端,还是很有套路的,起手式都是非常像的。
更多的难点对我来说反而是解耦,各种各样的function一用,回调函数一调,呕吼,就会懵一下。
对于这篇文章,我主要是把那些起手式,还有我觉得有点难得解耦方式稍微进行一下说明,方便我自己回顾,当然如果可以帮助更多的小伙伴那自然是更好啦。
目录
- Echo
- 服务端起手式
- 服务端LOOP
- 客户端起手
- 客户端LOOP
- 验证
- Dict
- 设计思想
- 验证
- Chat
- 服务端的修改
- 客户段的修改
- 效果展示
Echo
服务端起手式
这个echo的代码是为了熟悉起手式,因为几乎没有业务的附带,所以是很简单的。
而它的功能就是你向服务器中发送消息,你的服务端会重新发给你。
注意:日志真的很重要,可以让你知道你的程序在哪一步出错了,很快的定位。
首先大概的看一下起手式接口:
因为我们的网络要通信需要IP + 端口号才能定位到具体主机内具体进程,而IP + port就是我们说的套接字,关于套接字在UDP中需要知道2个接口,但是这两个接口中我们注意到有一个sockaddr
结构体,因此我们需要看一下结构体。
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
首先,这组接口不仅可以实现网络通信,也可以实现主机内通信。
其中sockaddr_in是网络,sockaddr_un是主机内,那么为什么bind接口内是sockaddr?
因为我们会填16位地址类型,所以当我们将对应的结构体强转为sockaddr *时,函数内部就会根据16位地址类型判断究竟是哪一种,这也就是C语言层面的多态!
首先,我们的server是面向对象的,代码中没有定义就出现变量都是私有成员。根据名字都可以知道大概意思,在最后会有完整代码。
int fd = ::socket(AF_INET, SOCK_DGRAM, 0);if (fd < 0){exit(1);}_socketfd = fd;// 将Ip Port与套接字绑定struct sockaddr_in addr;memset(&addr, 0, sizeof(addr));addr.sin_family = AF_INET;// 不要忘记转为网络序列!// addr.sin_addr.s_addr = inet_addr(_ip.c_str());addr.sin_addr.s_addr = INADDR_ANY;addr.sin_port = htons(_port);// bindint n = ::bind(_socketfd, (struct sockaddr *)&addr, sizeof(addr));
再来解释一下代码
第一个参数:
因为我们是网络通信,所以16位网络地址选择使用AF_INET
第二个参数:
我们选择的是UDP
也就是无连接,不可靠,数据报。
第三个参数:
表示希望使用的协议,我们通常设置为0,系统会根据情况自己处理。
返回值:
socket返回的是一个文件描述符
为什么返回文件描述符?
我们在此感性的理解
因为我们网络的通信是建立在网卡上的,而linux中一切皆文件,所以就相当于我们返回的是网卡的文件描述符。
于是我们的socket就创建好了,但是还要与IP与port进行绑定起来。
那么就先要创建一个sockaddr_in的结构体填参
sin我们可以理解为socket Internet。
其中我们只关心图中的框起来部分,sin_zero是作为将结构体补齐用的,新的网络编程库中甚至都见不到这个字段了。
我们逐个分析一下这个要填的3个字段
第一个参数的形式是一个宏,在预处理阶段会进行处理,进行替换得到sa_family_t sin_family
(##
在预处理阶段会将两边的字符串进行拼接)。
而这个填的就是AF_INET,与套接字对应。
第二个参数是一个无符号短整型,uint16_t的类型,我们填入自定义端口即可。
注意:由于我们要注意网络序列与主机序列的转换,自己进行判断的话过于麻烦,OS也提供了一组接口方便我们进行转换。
第三个参数是IP
注意这个IP可是有很大讲究的,
首先他是结构体内嵌套结构体,填的时候要注意
其次我们刚开始肯定觉得绑定自己的公网IP,或者局域网IP,又或者是本地环回。
但是如果填一个具体的IP,那么就意味着你以后只能从这一个向指定的IP中获取信息,但是你的主机IP有多个,反而不能全部利用,因此这里我们选择填入INADDR_ANY
(0)。
此时我们就可以接收多个IP+端口号发送来的信息了。
注意:我们一般在进行网络测试时,一般会使用本地环回IP测试,也就是127.0.0.1,当你的客服端向127.0.0.1这个IP发送时,那么就不会在网络中传输,而是在本机。
那么此时我们就完成起手式,socket的创建与绑定了。
服务端LOOP
while (true)
{char buffer[1024];sockaddr_in peer;socklen_t len = sizeof(peer);int n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&peer, &len);if (n != -1){buffer[n] = 0;int m = sendto(_socketfd, buffer, n, 0, (struct sockaddr*)&peer, sizeof(peer));if (m == -1){perror("发送错误");break;}}
}
我们的服务端肯定是要进行接收消息的,然后在做一些加工返回给客服端。
这里就不得不说两个函数了。
他们的参数都非常的类似,在接收时我们传入一段缓冲区,填入大小,即可得到客户端发来的消息了,因为我们接收后还要发送给对方,所以后边的两个参数是输入型参数,会得到对方的sockaddr信息。
对于发送时,我们也是如此。
另外:它们的 flags 参数是用来控制函数行为的标志位,允许程序员指定一些特殊的选项或操作模式。flags 参数通常是多个标志的按位或(OR)组合,但大多数情况下,这些标志并不是必需的,因此绝大多数会传递0作为默认值。
客户端起手
与服务端有很大的不同。
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1)
{exit(1);
}
先说结论:我们在服务端只需要创建socket即可,肯定需要bind,但无需显示bind,因为会在sendto中OS会自主绑定。
为什么不需要自己指定IP + 端口号?
OS肯定是知道你的IP,那么OS给你绑定也说得过去,那么端口号为什么不自主绑定?
我们举一个例子,一个主机上的端口号是有限的,如果客户端是自主定义,那么可能不同客户端会出现重复!比如抖音用端口号8888,那么快手绑定8888是势必不成功,因为IP + port标识一个唯一进程。所以这个由OS自主分配即可。
客户端LOOP
while (true)
{std::string buffer;std::cout << "Please write msg:";std::getline(std::cin, buffer);// 处理sockaddr结构体 + 发送数据到服务端struct sockaddr_in peer;peer.sin_addr.s_addr = inet_addr(ip.c_str());peer.sin_port = htons(port);peer.sin_family = AF_INET;socklen_t len = sizeof(peer);int n = sendto(fd, buffer.c_str(), buffer.size(), 0, (struct sockaddr *)&peer, len);if (n != -1){char inbuffer[1024];struct sockaddr_in temp;socklen_t len;int m = recvfrom(fd, inbuffer, n, 0, (struct sockaddr *)&temp, &len);if (m != -1){inbuffer[m] = 0;std::cout << inbuffer << std::endl;}}
}
我们在服务端绑定时不需要填真正的IP,发送和接收时也直接使用现成的,但是在客户端我们需要手动填写,但是我们现在有的是一个字符串,我们压迫将他转为4字节,还要转为网络序列,这也是很繁琐的,因此也有一批函数用来转化。
可以看到我们放入一个字符串地址即可得到网络序列4字节IP,非常的方便,但是这里可以改进(我们最后说,一般使用inet_pton)。
所以我们现在也就没啥干货了,起手式已经完成!
验证
由于上图代码都是非常简略的,并没有将如何封装写出,但是还是要验证一下的
完整代码在Gitee链接中给出。
Dict
设计思想
我们在以上的基础上增加一些业务,这里也就开始涉及一些解耦的设计了。
我们的理想效果为输入一个单词返回他的意思。
其实服务器与客户端大的逻辑仍旧是不变的,但是这里进行设计解耦的思想是很好的,要学习!
首先我们要改变的就是服务端,我们在recvfrom到字符串单词后可以使用回调函数
,将这个单词交给外部来做,返回汉语意思字符串sendto。这样就很好的完成了解耦,因为我们要用到回调函数,所以也就意味着在构造服务端对象时要传入可调用对象。
此时我们就可以利用function进行包装,包装出一个可调用对象类型。
using func_t = std::function<std::string(std::string)>;
关于这里我其实还想补充几点
我们命名时可以看到func_t中的func代表这是函数,t代表typename表示类型,这样别人一看就知道这是一个函数类型。
而命名空间时也有这样的讲究,比如我们有一个日志类,使用log_ns域封装起来,ns就是namespace的缩写,也是一目了然。
另外就是关于function的一些点了,实际上我们的function绑定时与被绑定的函数类型并不需要完全相符,就像下图这样的代码甚至可以编过,我一点都不理解…
但是我认为还是最好保证一样。
回到主线:
由于我们希望解耦,因此function中我们也就没有必要传引用,就解耦解的结结实实(但实际上传也是可以的,还避免了拷贝)。
随后我们再编写一个字典类,最终要的是要有支持翻译的功能!
我们可以选择搞一个配置文件的形式,创建对象时进行加载即可~
const std::string sep = ": ";// 单词与翻译的分隔符class Dict
{
private:void Load(){std::ifstream in(_path.c_str());if (!in.is_open()){exit(1);}std::string line;while (std::getline(in, line)){std::string key;std::string value;auto pos = line.find(sep);if (pos == -1)continue;key = line.substr(0, pos);value = line.substr(pos + sep.size());_map.insert(std::make_pair(key, value));}}public:Dict(const std::string & path): _path(path){Load();}std::string GetChinese(std::string word){auto ite = _map.find(word); if (ite != _map.end())return ite->second;elsereturn "None";}~Dict(){}
private:std::unordered_map<std::string, std::string> _map; std::string _path;
};
其中GetChinese函数就是我们未来在服务端回调的函数!
但是此时要注意,我们不能直接将这个函数传入服务器的构造函数中,因为这是一个静态成员函数!所以我们需要将这个函数bind一下,让this指针隐式写入即可!
我觉得这就是最精髓的地方了。
代码见链接。
验证
Chat
这是一个聊天室项目,我觉得是还算挑战性的,但实际上只是套的层数有点的,好多整合在一起。
听说Java那更喜欢各种封,各种套,什么结构啥的,害怕~
但是对于当前的chat聊天室来说最重要的搞清楚整体的大框架。
不仅仅是对于当前的聊天室,甚至可以说是任何比较嵌套的,只要把结构搞清楚了,那么就会轻松很多。
我们现在直接使用以上的服务端 + 客户端进行改进即可。
线程池的代码
服务端的修改
我们在字典中已经学到了在服务端使用回调进行业务处理,我们当然也可以使用回调完成转发!
我们进行转发需要3个元素。
sockfd描述符, message消息体,sockaddr结构体。
因此我们的回调设置为这样子即可。
这里的Inet就是我们带码云中封装过的转化类,
using service_t = std::function<void(int, const std::string &, const Inet &)>;
在服务器端上一个版本原本调用处理获取翻译的地方更改一下即可~
所以又到了设计转发类的时候了,我们一般喜欢在应用层中把这个工作叫做路由。
也就是设计一个路由类。
我们这样设计:当服务端回调到路由模块时,我们就得到了sockfd,message,addr,
- 首先检查当前ip + port是否在在线列表中,不在就add,在了就不管。
- 当消息为QUIT或者Q时,将在线列表中的user删除
- 转发我们只需要遍历一遍在线用户列表即可
也就是转发时使用线程池。
class Route
{
public:Route(){}void CheckOnlineUsers(const Inet &inet_addr){_online_users.insert(inet_addr)}void Offline(const Inet &inet_addr){LOG(DEBUG, "%s offline\n", inet_addr.AddrStr().c_str());_online_users.erase(inet_addr);}void ForwardHelper(int socket, const std::string &message){for (auto &user : _online_users){sockaddr_in peer = user.Sockaddr();socklen_t len = sizeof(peer);int n = ::sendto(socket, message.c_str(), message.size(), 0, (struct sockaddr *)&peer, len);}}void Forward(int socket, const std::string &message, const Inet &inet_addr){CheckOnlineUsers(inet_addr);if (message == "Q" || message == "QUIT"){Offline(inet_addr);}// 转发模块,线程池去执行std::function<void()> f = std::bind(&Route::ForwardHelper, this, socket, send_message);ThreadPool<std::function<void()>>::GetInstance()->Equeue(f);}~Route(){}private:std::set<Inet, Route_ns::comp> _online_users;pthread_mutex_t _mutex;
};
注意到我们的线程池中只需要push进去一个可调用对象即可,所以我们进行bind一下以进行适配线程池模板。
而我们在进行构造客户端时传入Route类中的Forward函数即可~
依旧和Dict服务器一样的方法套路。
这样服务端就设计好了
客户段的修改
我们当前的客户端首先是有问题的,因为我们当前只有一个线程同时进行收和发,当我们多起几个客户端时,如果客户端A进行发消息,其他的客户端其实都不会显示的,因为只有别的客户端进行sendto时才会收到消息,否则就一直阻塞在sendto中。
所以这里我们也是用多线程进行一下修改,一个线程一直读,一个一直进行发送。
main函数中我们创建2个线程,分别执行各自的读和写,这里就没什么细节了。
int ClientInit()
{int fd = socket(AF_INET, SOCK_DGRAM, 0);if (fd == -1){LOG(FATAL, "create socket err");exit(1);}return fd;
}void receiver(const std::string &name, int socketfd)
{while (true){char inbuffer[1024];struct sockaddr_in temp;socklen_t len;int n = recvfrom(socketfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&temp, &len);if (n >= 0){inbuffer[n] = 0;std::cerr << inbuffer << std::endl;}}
}void sender(const std::string &name, int socketfd, const std::string &ip, uint16_t port)
{while (true){std::string buffer;std::cout << "Please input msg:";std::getline(std::cin, buffer);// 处理sockaddr结构体 + 发送数据到服务端struct sockaddr_in peer;peer.sin_addr.s_addr = inet_addr(ip.c_str());peer.sin_port = htons(port);peer.sin_family = AF_INET;socklen_t len = sizeof(peer);int n = sendto(socketfd, buffer.c_str(), buffer.size(), 0, (struct sockaddr *)&peer, len);}
}int main(int args, char *argv[])
{// 处理命令行行参数if (args != 3){std::cerr << "Usage:" << argv[0] << " Ip Port" << std::endl;exit(1);}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);int socketfd = ClientInit();MyThread t1("thread-receiver", std::bind(receiver, std::placeholders::_1, socketfd));MyThread t2("thread-sender", std::bind(sender, std::placeholders::_1, socketfd, ip, port));t1.Start();t2.Start();t1.Join();t2.Join();return 0;
}
代码链接
效果展示
完~~
相关文章:

【计算机网络】UDP实战
其实经过这几天写的几种不同的UDP的简易客户端与服务端,还是很有套路的,起手式都是非常像的。 更多的难点对我来说反而是解耦,各种各样的function一用,回调函数一调,呕吼,就会懵一下。 对于这篇文章&#x…...

七、ESP32-S3上使用MicroPython点亮WS2812智能LED灯珠并通过web控制和JS颜色选择器改变灯珠颜色
本地代码集成离线iro.js库来添加一个颜色选择器控件,在无网络环境可以通过JavaScript将选中的颜色发送到服务器以改变LED颜色。以下是将iro.js集成到网页后的颜色图片。 Iro.js 地址API操作手册 color:change # 每当所选颜色发生变化时触发 - 无论是当用户与颜色选…...

Z 字形遍历二叉树
假设一个二叉树上各结点的权值互不相同。 我们就可以通过其后序遍历和中序遍历来确定唯一二叉树。 请你输出该二叉树的 ZZ 字形遍历序列----也就是说,从根结点开始,逐层遍历,第一层从右到左遍历,第二层从左到右遍历,…...

[Vue]Vue3从入门到精通-综合案例分析
一.Vue是什么: 概念:Vue是一个用于构建用户界面的渐进式的框架 以下的内容是自里向外的 声明式渲染(Vuejs核心包)组件系统(Vuejs核心包)客户端路由VueRouter大规模状态管理Vuex构建工具Webpack/Vite Vue的两种使用方式: Vue核心包开发-&…...

深度学习——神经网络(neural network)详解(二). 带手算步骤,步骤清晰0基础可看
深度学习——神经网络(neural network)详解(二). 手算步骤,步骤清晰0基础可看 前文如下:深度学习——神经网络(neural network)详解(一). 带手算步骤&#x…...

【扒网络架构】backbone、ccff
backbone CCFF 还不知道网络连接方式,只是知道了每一层 backbone backbone.backbone.conv1.weight torch.Size([64, 3, 7, 7])backbone.backbone.layer1.0.conv1.weight torch.Size([64, 64, 1, 1])backbone.backbone.layer1.0.conv2.weight torch.Size([64, 64,…...

linux进程
exit()函数正常结束进程 man ps aux 是在使用 ps 命令时常用的一个选项组合,用于显示系统中所有进程的详细信息。aux 不是 ps 命令的一个正式选项,而是三个选项的组合:a, u, 和 x。这三个选项分别代表不同的含义&#…...

PRVF-4037 : CRS is not installed on any of the nodes
描述:公司要求替换centos,重新安装ORACLE LINUX RAC的数据库做备库,到时候切换成主库,安装Linux7GRID 19C 11G Oracle,顺利安装grid 19c,安装11G数据库软件的时候检测报如题错误:**PRVF-4037 …...

整理 酷炫 Flutter 开源UI框架 FAB
flutter_villains 灵活且易于使用的页面转换。 项目地址:https://github.com/Norbert515/flutter_villains 项目Demo:https://download.csdn.net/download/qq_36040764/89631324...

Unity 编写自己的aar库,接收Android广播(broadcastReceiver)并传递到Unity
编写本文是因为找了很多文章,都比较片段,不容易理解,对于Android新手来说理解起来不友好。我这里写了一个针对比较小白的文章,希望有所帮助。 Android端 首先还是先来写Android端,我们新建一个Android空项目…...

Mysql cast函数、cast用法、字符串转数字、字符串转日期、数据类型转换
文章目录 一、语法二、示例2.1、复杂示例 三、cast与convert的区别 CAST 函数是 SQL 中的一种类型转换函数,它用于将一个数据类型转换为另一个数据类型,这篇文章主要介绍了Mysql中Cast()函数的用法,需要的朋友可以参考下。 Mysql提供了两种将值转换成指…...

微信小程序开发之组件复用机制
新建复用文件,另外需要注册 behavior 例如: 在behavior.js文件中写入方法,并向外暴露出去 写法一: module.exportsBehavior({data: {num: 1},lifetimes: {created() {console.log(1);}} })写法二: const behavior …...

数据结构--线性表
数据结构分类 集合 线性结构(一对一) 树形结构(一对多) 图结构(多对多) 数据结构三要素 1、逻辑结构 2、数据的运算 3、存储结构(物理结构) 线性表分类 1、顺序表 2、链表 3、栈 4、队列 5、串 线性表--顺序表 顺序表的特点 顺序表的删除和插入…...

深入探针:PHP与DTrace的动态追踪艺术
标题:深入探针:PHP与DTrace的动态追踪艺术 在高性能的PHP应用开发中,深入理解代码的执行流程和性能瓶颈是至关重要的。DTrace,作为一种强大的动态追踪工具,为开发者提供了对PHP脚本运行时行为的深入洞察。本文将详细介…...

黑龙江日报报道第5届中国计算机应用技术大赛,赛氪提供赛事支持
2024年7月17日,黑龙江日报、极光新闻对在哈尔滨市举办的第5届中国计算机应用技术大赛全国总决赛进行了深入报道。此次大赛由中国计算机学会主办,中国计算机学会计算机应用专业委员会与赛氪网共同承办,吸引了来自全国各地的顶尖技术团队和选手…...

【计算机网络】LVS四层负载均衡器
https://mobian.blog.csdn.net/article/details/141093263 https://blog.csdn.net/weixin_42175752/article/details/139966198 《高并发的哲学原理》 (基本来自本书) 《亿级流量系统架构设计与实战》 LVS 章文嵩博士创造 LVS(IPVS) 章⽂嵩发…...

Java 守护线程练习 (2024.8.12)
DaemonExercise package DaemonExercise20240812;public class DaemonExercise {public static void main(String[] args) {// 守护线程// 当普通线程执行完毕之后,守护线程没有继续执行的必要,所以说会逐步关闭(并非瞬间关闭)//…...

C#小桌面程序调试出错,如何解决??
🏆本文收录于《CSDN问答解惑-专业版》专栏,主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收…...

Seatunnel Mysql数据同步到Mysql
环境 mysql-connector-java-8.0.28.jar、connector-cdc-mysql 配置 env {# You can set SeaTunnel environment configuration hereexecution.parallelism 2job.mode "STREAMING"# 10秒检查一次,可以适当加大这个值checkpoint.interval 10000#execu…...

Java Web —— 第五天(请求响应1)
postman Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件 作用:常用于进行接口测试 简单参数 原始方式 在原始的web程序中,获取请求参数,需要通过HttpServletRequest 对象手动获 http://localhost:8080/simpleParam?nameTom&a…...

【LLMOps】手摸手教你把 Dify 接入微信生态
作者:韩方圆 "Dify on WeChat"开源项目作者 概述 微信作为最热门即时通信软件,拥有巨大的流量。 微信友好的聊天窗口是天然的AI应用LUI(Language User Interface)/CUI(Conversation User Interface)。 微信不仅有个人微信,同时提供…...

Ftrans文件摆渡方案:重塑文件传输与管控的科技先锋
一、哪些行业会用到文件摆渡相关方案 文件摆渡相关的产品和方案通常用于需要在不同的网络、安全域、网段之间传输数据的场景,主要是一些有核心数据需要保护的行业,做了网络隔离和划分。以下是一些应用比较普遍的行业: 金融行业:…...

LaTeX中的除号表示方法详解
/除号 LaTeX中的除号表示方法详解1. 使用斜杠 / 表示除号优点缺点 2. 使用 \frac{} 表示分数形式的除法优点缺点 3. 使用 \div 表示标准除号优点缺点 4. 使用 \over 表示分数形式的除法优点缺点 5. 使用 \dfrac{} 和 \tfrac{} 表示大型和小型分数优点缺点 总结 LaTeX中的除号表…...

DID、DID文档、VC、VP分别是什么 有什么关系
DID(去中心化身份) 定义:DID 是一种去中心化的唯一标识符,用于表示个体、组织或设备的身份。DID 不依赖于中央管理机构,而是由去中心化网络(如区块链)生成和管理。 用途:DID 允许用…...

网络安全应急响应
前言\n在网络安全领域,有一句广为人知的话:“没有绝对的安全”。这意味着任何系统都有可能被攻破。安全攻击的发生并不可怕,可怕的是从头到尾都毫无察觉。当系统遭遇攻击时,企业的安全人员需要立即进行应急响应,以将影…...

Qt数据和视图分离——中MCV和MVVM
智能指针 一、背景知识二、命令式编程 vs 声明式编程2.1 命令式编程(Imperative Programming)2.2 声明式编程(Declarative Programming) 三、 MVC(Model-View-Controller)3.1 模型(Model)3.2 视图ÿ…...

重定义变量类型:如#define FLOAT float和typedef float FLOAT的区别
在 C 或 C 中, #define 和 typedef 都可以用来为类型或值创建别名,但它们之间存在一些关键的区别: 预处理指令 ( #define ): #define 是预处理器指令,用于定义宏。 当编译器处理源代码时,预处理器会先运行&#…...

Qt 使用阿里矢量图标库
前言 阿里矢量图标库非常好用,里面有各种丰富的图标,完全免费,还支持自定义图标,还可以将图标打包到一个项目中,使用起来非常方便。 第一步: 打开阿里矢量图标库 第二步: 搜索图标&#x…...

仓颉语言运行时轻量化实践
杨勇勇 华为语言虚拟机实验室架构师,目前负责仓颉语言静态后端的开发工作 仓颉语言运行时轻量化实践 仓颉Native后端(CJNative)是仓颉语言的高性能、轻量化实现。这里的“轻量化”意指仓颉程序运行过程中占用系统资源(内存、CPU等…...

深入理解Python中的subprocess模块
目录 subprocess模块简介常用函数执行外部命令管道通信子进程管理错误处理实际应用示例最佳实践 subprocess模块简介 subprocess模块是Python标准库的一部分,提供了一个跨平台的方法来生成新进程、连接其输入/输出/错误管道,并获取其返回码。该模块旨…...