【Linux】Http协议的学习

文章目录

  • 前言
  • 一、了解HTTP协议是如何规定的
  • 总结


前言

HTTP协议(超文本传输协议)和我们上一篇写的网络版计算器中自己定制的协议一样,只不过Http协议是是一个非常好用的协议,所以我们可以直接用现成的不用自己再搞一套了。


一、了解HTTP协议是如何规定的

如下图所示,HTTP协议包含以下几个部分:

现在我们随便看一下http网址,几乎都没有登录信息了,因为这个东西不再被需要了。为什么http要有文件路径呢?因为我们的网页资源实际上是从服务器的某个文件拿的,这也就解释了为什么http协议被称为超文本传输协议。

urlencodeurldecode:

/ ? : 等这样的字符 , 已经被 url 当做特殊意义理解了 . 因此这些字符不能随意出现 .
比如 , 某个参数中需要带有这些特殊字符 , 就必须先对特殊字符进行转义 .
转义的规则如下 :
将需要转码的字符转为 16 进制,然后从右到左,取 4 ( 不足 4 位直接处理 ) ,每 2 位做一位,前面加上 % ,编码成 %XY格式。
下面我们举个例子:

wd就是我们 输入的关键字,可以看到c++中的+号被转移为“%2B”,而urldecode就是urlencode的逆过程。

首先一般情况下对于以上编码解码的过程是不需要我们做这个工作的,即使需要我们做这个工作也可以直接从网络拿取写好的编码解码代码。下面我们认识一下http版本的请求和回应都有哪几部分组成:

 我们http请求包括请求行,请求报头,空行和请求正文。在请求行中,我们分为三部分,get是http获取的方法,主要有get和post方法。url就是域名部分,后面httpversion就是我们http的版本号,注意后面有\r\n:

 比如上面红色部分就是url了。了解了请求行后我们再看请求报头,每一个请求报头都由\r\n作为结尾。请求报头过来是空行,空行就是\r\n,然后是请求正文,请求正文是可以没有的这点要注意。

 在响应部分有状态行,响应报头,空行,响应正文。状态行中分为三部分,第一部分是HTTP协议版本号,第二部分是状态码,比如我们经常遇到的404就是状态码,第三部分是状态码描述,状态码描述就比如404后面跟着“访问的资源不存在”这句话。

下面我们来关注两个细节:

在http中,如何保证请求和响应应用层完整读取完毕了呢?其实很简单,我们会发现所有http请求的字段都是字符串,并且以行为单位,所以要保证完整读取就只需要用while循环按行为单位将所有请求行和请求报头读取完毕即可,注意我们第三部分就是空行,这就是while循环的判断条件,只要读到空行,说明我们将请求行和请求报头都读取完毕了,那么如何保证读取正文呢,还记得我们实现网络版计算器自定协议为正文长度+\r\n+正文+\r\n吗,没错http中报头前面也是正文的长度,只要我们读取完头就知道了正文长度,从而将正文读取完毕。

第二个细节, 请求和响应是如何做到序列化和反序列化的?这里是http自己实现的,序列化只需要将请求报头按照\r\n依次插入到请求行的后面即可,反序列化直接按照\r\n将整个字符串拆解为多个字符串即可。对于正文是不用做处理的,这是http协议规定的。

下面我们自己构建一个简单的http服务器:

首先服务器还是之前Tcp的代码,我们修改一下即可:

namespace server
{enum{SOCKET_ERR = 2,USE_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport = 8080;//listen的第二个参数是底层全连接长度+1static const int gbacklog = 5;class HttpServer{public:HttpServer(func_t func,const uint16_t& port = gport):_port(port),_listensock(-1),_func(func){}void initServer(){//1.创建文件套接字对象_listensock = socket(AF_INET,SOCK_STREAM,0);if (_listensock==-1){exit(SOCKET_ERR);}//2.进行bindstruct sockaddr_in local;bzero(&local,sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY; //INADDR_ANY绑定任意地址IPif (bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0){exit(BIND_ERR);}//3.Tcp需要将套接字状态设为listen状态来一直监听(因为Tcp是面向字节流的)if (listen(_listensock,gbacklog)<0){exit(LISTEN_ERR);}}void start(){//忽略17号信号signal(SIGCHLD,SIG_IGN);for (;;){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(_listensock,(struct sockaddr*)&peer,&len);if (sock<0){continue;}cout<<"sock: "<<sock<<endl;pid_t id = fork();if (id==0){close(_listensock);close(sock);exit(0);} close(sock);}}~HttpServer(){}private: int _listensock;     //不是用来进行数据通信的,它是用来监听链接到来获取新链接的uint16_t _port;func_t _func;};
}

因为我们是没有写对客户端的请求做处理的回调函数的,所以我们先写一个回调函数:

因为回调函数是需要客户端请求处理后将结果返回到响应的,所以还需要一个类来保存请求与响应:

class HttpRequest
{
public:HttpRequest(){}~HttpRequest(){}
public:string inbuffer;
};class HttpResponse
{
public:string outbuffer;
};

 目前我们就仅在这两个类中放一个string缓冲区即可,然后我们用包装器定义一个回调函数:

 注意在服务器的私有成员变量中加一个回调函数,然后我们在启动服务器的时候处理这个请求:

 下面我们编写一下处理客户端请求的函数:

注意:我们编写网络版计算器的时候,那个时候接收客户端消息需要自己去掉报头然后反序列化,而今天的实现我们就不去做那些事情,因为我们只是演示一下http服务器的原理,对于协议就不再浪费时间了:

       void HanderHttp(int sock){//1.读到完整的数据请求// ..........HttpRequest req;HttpResponse resp;char buffer[4096];ssize_t n = recv(sock,buffer,sizeof(buffer)-1,0);if (n>0){buffer[n] = 0;req.inbuffer = buffer;_func(req,resp);send(sock,resp.outbuffer.c_str(),resp.outbuffer.size(),0);}}

我们今天要做的只需要构建请求与响应对象,然后定义一个缓冲区将文件描述符的数据读到缓冲区中,如果读取成功我们就将缓冲区的数据放到请求对象的缓冲区中,然后用回调函数对客户端的请求做处理,处理完发送回客户端即可。

有了hander方法我们就可以实现一下.cc文件:

void Usage(string proc)
{cout<<"Usage: \n\t"<<proc<<" port\r\n\r\n";
}
// 1.服务器和网页分离,html
// 2.url -> / :web根目录
bool Get(const HttpRequest& req,HttpResponse& resp)
{cout<<"------------------http start-----------------------"<<endl;cout<<req.inbuffer<<endl;cout<<"------------------http end-----------------------"<<endl;return true;
}
int main(int argc,char* argv[])
{if (argc!=2){Usage(argv[0]);exit(0);}uint16_t port = atoi(argv[1]);unique_ptr<HttpServer> hps(new HttpServer(Get,port));hps->initServer();hps->start();return 0;
}

对于get方法我们就先演示一下,等会再添加内容:

下面我们运行起来:

 运行起来后我们该如何访问呢,只需要在网址栏填入你的云服务器ip地址和端口号,注意ip地址和端口号以英文冒号连接:

 虽然我们的网页什么都没有,但是服务端会显示哪个浏览器访问我们服务端的记录,下面我们解释一下每一行的意思:

 第一个get表示浏览器请求的方法,默认的方法就是get方法。get后面的/就是url,因为我们登录浏览器的时候只是告诉浏览器去哪个ip和端口,并没有添加路径告诉浏览器我们要请求哪个资源,所以默认是个根目录,注意没有请求指定的资源,默认返回服务器首页,但是我们没有做处理所以是根目录。第三个HTTP1.1就是目前主流的http版本。第二行的host代表什么呢?host代表我们的请求是要发送给哪个服务端的,其实就是我们刚开始输入网址的ip和端口号,为什么会有这个呢?因为有代理服务器的存在。第三行代表支持长连接,第四行代表协议升级,也就是说http协议是可以被升级的。第五行就很有意思了,比如你用手机浏览器登录这个网址,那么上面就会显示你手机的信息,如下图:

 看到了这个我们就不难理解为什么我们用苹果手机和安卓手机搜索一个应用程序时,苹果手机浏览器会默认把ios版APP放到首页,因为当我们请求某个服务器时我们用什么设备请求的信息也会被服务器拿到。第六行代表客户端能接受什么样的格式,就比如文档格式,第七行表示客户端支持压缩。

因为上面中是没有服务器对于客户端请求的响应的,所以下面我们设计一下响应,让服务端给客户端发一个我们设定好的状态行,除了状态行还有空行和正文,对于响应报头我们就先不写。

bool Get(const HttpRequest& req,HttpResponse& resp)
{cout<<"------------------http start-----------------------"<<endl;cout<<req.inbuffer<<endl;cout<<"------------------http end-----------------------"<<endl;string respline = "HTTP/1.1 200 OK\r\n";   //响应行string respblank = "\r\n";  //空行  先暂时不写响应报头string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>my web</title><h1>hello this is test</h1></head><body><p>给你科普一下鸭子的种类、达克鸭、小黄鸭、扁嘴鸭、我想你了鸭</p></body></html>";resp.outbuffer+=respline;resp.outbuffer+=respblank;resp.outbuffer+=body;return true;
}

首先说一下这样做的原理,因为未来读到客户端的请求我们会调用get函数,而我们上面是直接将resp填充了一下,在响应中包含响应行,空行和正文,回调函数结束会将这个填充好的响应发送到客户端,下面我们运行起来看看:

 可以看到我们访问这个服务器是可以看到响应的,下面我们看看访问信息:

 可以看到是没有问题的,这里我们说一下:实际上我们的浏览器已经非常智能了,我们刚刚没有报头的信息没有正文的长度,浏览器依旧可以识别,但是今天我们只是为了做演示,如果真的要实现这些要做的工作我们还是需要做的。

下面我们就将报头加入进来:

bool Get(const HttpRequest& req,HttpResponse& resp)
{cout<<"------------------http start-----------------------"<<endl;cout<<req.inbuffer<<endl;cout<<"------------------http end-----------------------"<<endl;string respline = "HTTP/1.1 200 OK\r\n";   //响应行string respheader = "Content-type: text/html\r\n";  //响应报头string respblank = "\r\n";  //空行  string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>my web</title><h1>hello this is test</h1></head><body><p>给你科普一下鸭子的种类、达克鸭、小黄鸭、扁嘴鸭、我想你了鸭</p></body></html>";resp.outbuffer+=respline;resp.outbuffer+=respheader;resp.outbuffer+=respblank;resp.outbuffer+=body;return true;
}

content-type代表我们有效载荷的类型,通过content-type对照表我们可以告诉客户端我们返回的是什么资源,比如照片就是jpg等类型,然后我们在响应的缓冲区中加上响应报头。我们今天演示就以html为例:

 网页资源对应的就是text/html,下面我们运行起来:

 我们通过telnet工具可以看到响应的信息都有了。前面我们说了,当我们直接以ip+端口号的方式连接服务器时,默认资源路径是根目录,那么今天我们想访问其他其他资源该怎么做呢:

 我们发现当用路径去访问服务器的某个资源的时候,浏览器给服务器发送的url中自动在根目录的后面加了我们要访问资源的路径,下面我们实现一下这个操作并且解决两个问题1.服务器和网页分离2.url是一个/的时候表示web根目录,我们也实现一下这个根目录。

首先要将服务器和网页分离,那么就需要对url做切分,所以我们再创建一个新文件用来处理字符串的切分:

在处理url之前我们还要对请求行做拆分,这样才能拿到url,所以我们在请求类中在多加几个成员变量分别代表请求行,请求方法,url和httpversion.

const string sep = "\r\n";
class HttpRequest
{
public:HttpRequest(){}~HttpRequest(){}void parse(){// 1.从inbuffer中拿到第一行,分隔符\r\nstring line = Util::getOneLine(inbuffer,sep);if (line.empty()){return;}//2.从请求行中提取三个字段}public:string inbuffer;string method;      //请求行get方法string url;         //请求行urlstring httpversion; //http版本
};

 在对请求行做拆分之前还要拿到请求的第一行,所以我们需要写一个函数,这个函数就放在刚刚创建的新文件中,因为要频繁的用到分隔符,所以我们直接定义了一个。

class Util
{
public:static std::string getOneLine(std::string &buffer,const std::string& sep){auto pos = buffer.find(sep);if (pos==std::string::npos){return "";}std::string str = buffer.substr(0,pos);buffer.erase(0,str.size()+sep.size());return str;}
};

要拿到第一行还是比较简单的,我们写静态成员函数的原因是不需要this指针,如果是成员函数还会多一个this指针的参数并且我们未来可能会持续获取请求行请求报头,用静态的会更好。要获取一行首先需要一个缓冲区,然后是分隔符。这个缓冲区就是整个请求序列,有请求行请求报头什么的。我们先找第一个\r\n的位置,这样就确定了第一行,然后判断能否找到找不到就返回一个空字符串,找到了就把第一行的字符串返回并且将缓冲区第一行的字符串清空,这样我们如果要获取后面的每一行就会很方便。

 void parse(){// 1.从inbuffer中拿到第一行,分隔符\r\nstring line = Util::getOneLine(inbuffer,sep);if (line.empty()){return;}//2.从请求行中提取三个字段cout<<"line: "<<line<<endl;stringstream ss(line);ss>>method>>url>>httpversion;}

获取到第一行后我们先打印,然后用stringstream将以空格为分割的三个字段全部传入类内部的method和url和httpversion,这里的stringstream的工作实际上就是反序列化。

 在hander方法中,我们先打印获取请求行的三个字段,然后再调用回调函数处理。

bool Get(const HttpRequest& req,HttpResponse& resp)
{cout<<"------------------http start-----------------------"<<endl;cout<<req.inbuffer<<endl;cout<<"method: "<<req.method<<endl;cout<<"url: "<<req.url<<endl;cout<<"httpversion: "<<req.httpversion<<endl;cout<<"------------------http end-----------------------"<<endl;//........................
}

然后我们把请求类中三个字段在执行get函数的时候打印出来,后面的代码都一样就用...省略了,下面我们将程序运行起来:

 这样我们不就把请求行和请求行的三个字段拿出来了吗,当然我们也试试不加路径默认访问是什么样的请求:

 可以看到如果不加路径默认的请求行后面是/favicon.ico,其实这个就是我们网站的一个标签而已:

 如上图百度额logo就是/favicon.ico。

了解了上面的知识我们再来看看web根目录,我们通过上面的图片可以看到不管服务器收到什么样的请求在url中都是以/开头的,所以我们定义默认路径的时候后面不用加/:

const string default_root = "wwwroot";   //web根目录

然后我们在vscode中也创建一个这样的目录:

 接下来我们在请求行中增加一个string对象来保存路径:

void parse(){// 1.从inbuffer中拿到第一行,分隔符\r\nstring line = Util::getOneLine(inbuffer,sep);if (line.empty()){return;}//2.从请求行中提取三个字段cout<<"line: "<<line<<endl;stringstream ss(line);ss>>method>>url>>httpversion;//3.添加web默认路径path = default_root;path+=url;if (path[path.size()-1]=='/'){path+=home_page;}}

首先我们让路径默认是在wwwroot这个目录下,然后如果用户指定路径比如在/z/b中,那么路径就变成了/wwwroot/z/b,所以我们让path加等url,最后为什么要判断一下呢?这是因为我们的用户有可能只通过ip和端口号访问,他们不知道服务器的哪个路径有资源,所以我们应该设置一个判断如果用户没有输入路径,那么由url自动添加/的特性,我们直接判断路径字符串最后一个字符是否是/,如果是则说明用户没有填写路径这个时候我们就直接跳到主页即可,对于主页,我们也要设置一个默认文件:

一般主页的默认文件都是index.html, 这样的话如果用户没有指定访问路径,我们就在web根目录后面添加主页文件路径,这样的话就可以直接跳到主页了,所以我们还应该创建一个主页的文件:

然后我们就随便写一句话就好:

 然后我们在get函数中把路径也打印出来:

下面我们运行起来看看:

 当url为/的时候,路径会默认拼接变成wwwroot/index.html,当url为指定路径的时候:

通过以上的结果我们应该可以认识到web根目录是什么了,下一篇文章中我们将在web根目录中写一些具体的资源并且还会加入跳转的按钮。 


总结

这篇文章中主要讲解http协议是如何实现的,以及底层的一些原理是什么,在我们手动实现一些原理的时候我们才能对http有更加深刻的认识。

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

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

相关文章

EM算法实例

EM算法的E步骤&#xff0c; 是计算在当前的预估参数下&#xff0c;隐含变量&#xff08;是A硬币还是B硬币&#xff09;的每个值出现的概率。也就是给定和观测数据&#xff0c;计算这组数据出自A硬币的概率和这组数据出自B硬币的概率 我们根据E步中求出的A硬币、B硬币概率分布&…

在家构建您的迷你 ChatGPT

这篇文章分为三个部分&#xff1b;他们是&#xff1a; 什么是指令遵循模型&#xff1f;如何查找遵循模型的指令构建一个简单的聊天机器人废话不多说直接开始吧&#xff01;&#xff01;&#xff01; 什么是指令遵循模型&#xff1f; 语言模型是机器学习模型&#xff0c;可以根…

tp6 实现excel 导入功能

在项目根目录安装 composer require phpoffice/phpspreadsheet 我们看一下郊果图&#xff0c;如下 点击导入excel表格数据 出现弹窗选择文件&#xff0c;控制台打开输出文档内容 前端layui代码 <form id"uploadForm" class"form-horizontal" encty…

如何使用 Flatpak 在 Linux 上安装 ONLYOFFICE 桌面编辑器?

Flatpak 是一款与 Linux 发行版无关的软件实用工具&#xff0c;可用于在 Linux 上构建和分发桌面端应用。其可帮助您安装第三方 Linux 应用程序&#xff0c;无需安装库或处理依赖。 ONLYOFFICE 桌面版是什么 ONLYOFFICE 编辑器桌面版是一款全面的办公工具&#xff0c;提供了文…

(七)「消息队列」之 RabbitMQ 发布者确认(使用 .NET 客户端)

发布者确认&#xff08;Publisher Confirms&#xff09; 发布者确认是一个 RabbitMQ 扩展&#xff0c;用于实现可靠的发布。当在通道上启用发布者确认时&#xff0c;客户端发布的消息将由代理异步确认&#xff0c;这意味着它们已在服务器端得到处理。 0、引言 先决条件 本教程…

在命令行模式、eclipse console下执行Java程序输入中文的几种情况尝试

介绍 在命令行模式下执行Java程序&#xff0c;如果输入中文&#xff0c;经常会出现和代码中的解码字符集不匹配的情况&#xff0c;导致结果不正确。 在命令行模式下执行Java程序&#xff0c;输入中文&#xff0c;其实是用某种字符集编码成字节流&#xff0c;Java程序读取该字节…

Seaborn库绘制单变量分布和双变量分布

Matplotlib虽然已经是比较优秀的绘图库了&#xff0c;但是它有个今人头疼的问题&#xff0c;那就是API使用过于复杂&#xff0c;它里面有上千个函数和参数&#xff0c;属于典型的那种可以用它做任何事&#xff0c;却无从下手。 Seaborn基于 Matplotlib核心库进行了更高级的API…

python与深度学习(五):CNN和手写数字识别

目录 1. 说明2. 卷积运算3. 填充4. 池化5. 卷积神经网络实战-手写数字识别的CNN模型5.1 导入相关库5.2 加载数据5.3 数据预处理5.4 数据处理5.5 构建网络模型5.6 模型编译5.7 模型训练、保存和评价5.8 模型测试5.9 模型训练结果的可视化 6. 手写数字识别的CNN模型可视化结果图7…

LeetCode面向运气之Javascript—第2600题-K件物品的最大和-94.68%

LeetCode第2600题-K件物品的最大和 题目要求 袋子中装有一些物品&#xff0c;每个物品上都标记着数字 1 、0 或 -1 。 四个非负整数 numOnes 、numZeros 、numNegOnes 和 k 。 袋子最初包含&#xff1a; numOnes 件标记为 1 的物品。numZeroes 件标记为 0 的物品。numNegOn…

12 扩展Spring MVC

✔ 12.1 实现页面跳转功能 页面跳转功能&#xff1a;访问localhost:8081/jiang会自动跳转到另一个页面。 首先&#xff0c;在config包下创建一个名为MyMvcConfig的配置类&#xff1a; 类上加入Configuration注解&#xff0c;类实现WebMvcConfiger接口&#xff0c;实现里面的视…

【学习心得】sublime text 4 自定义编译系统

一、问题描述 在电脑中有多个版本的Python解释器&#xff0c;而sublime默认选择最新版本的解释器&#xff0c;如何指定自己想要的解释器呢&#xff1f; 二、自定义编译系统 1、选择新建编译系统&#xff08;如图&#xff09; 2、重写两个键值对&#xff08;只修改中文部分其…

【Hive 01】简介、安装部署、高级函数使用

1 Hive简介 1.1 Hive系统架构 Hive是建立在 Hadoop上的数据仓库基础构架&#xff0c;它提供了一系列的工具&#xff0c;可以进行数据提取、转化、加载&#xff08; ETL &#xff09;Hive定义了简单的类SQL查询语言&#xff0c;称为HQL&#xff0c;它允许熟悉SQL的用户直接查询…

【《机器学习和深度学习:原理、算法、实战(使用Python和TensorFlow)》——以机器学习理论为基础并包含其在工业界的实践的一本书】

机器学习和深度学习已经成为从业人员在人工智能时代必备的技术&#xff0c;被广泛应用于图像识别、自然语言理解、推荐系统、语音识别等多个领域&#xff0c;并取得了丰硕的成果。目前&#xff0c;很多高校的人工智能、软件工程、计算机应用等专业均已开设了机器学习和深度学习…

OBS 迁移--华为云

一、创建迁移i任务 1. 登录管理控制台。 2. 单击管理控制台左上角的 在下拉框中选择区域。 3. 单击“ 服务列表 ”&#xff0c;选择“ 迁移 > 对象存储迁移服务 OMS ”&#xff0c;进入“ 对象存储迁移服务 ”页面。 4. 单击页面右上角“ 创建迁移任务 ”。 5. 仔细阅读…

el-upload上传图片和视频,支持预览和删除

话不多说&#xff0c; 直接上代码&#xff1a; 视图层&#xff1a; <div class"contentDetail"><div class"contentItem"><div style"margin-top:5px;" class"label csAttachment">客服上传图片:</div><el…

iOS--KVO和KVC

KVC 简单介绍 KVC的全称是KeyValueCoding&#xff0c;俗称“键值编码”&#xff0c;可以通过一个key来访问某个属性&#xff1b; KVC提供了一种间接访问其属性方法或成员变量的机制&#xff0c;可以通过字符串来访问对应的属性方法或成员变量&#xff1b; 它是一个非正式的…

大数据课程C4——ZooKeeper结构运行机制

文章作者邮箱&#xff1a;yugongshiyesina.cn 地址&#xff1a;广东惠州 ▲ 本章节目的 ⚪ 了解Zookeeper的特点和节点信息&#xff1b; ⚪ 掌握Zookeeper的完全分布式安装 ⚪ 掌握Zookeeper的选举机制、ZAB协议、AVRO&#xff1b; 一、Zookeeper-简介 1. 特点…

vue build 打包遇到bug解决记录

文章目录 vue-cli-service servevue打包修改dist文件夹名字vue build require is not defined 和 exports is not defind 错误 vue-cli-service serve 通常vue是不能直接使用vue-cli-service命令在终端运行的&#xff0c;所以才会在package.json中配置了scripts&#xff1a; …

JDBC的的使用

首先导入jar包。 https://downloads.mysql.com/archives/c-j/ package com.test.sql;import java.sql.*;public class StudySql {public static void init() throws SQLException {Statement stmt null;Connection conn null;ResultSet res null;PreparedStatement pstm…

自动化运维工具—Ansible概述

Ansible是什么&#xff1f; Ansible是一个基于Python开发的配置管理和应用部署工具&#xff0c;现在也在自动化管理领域大放异彩。它融合了众多老牌运维工具的优点&#xff0c;Pubbet和Saltstack能实现的功能&#xff0c;Ansible基本上都可以实现。 Ansible能批量配置、部署、…