【Linux】Http协议的学习

news2024/11/15 17:51:34

文章目录

  • 前言
  • 一、了解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的第二个参数是底层全连接长度+1
    static 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.进行bind
           struct 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绑定任意地址IP
           if (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\n
        string line = Util::getOneLine(inbuffer,sep);
        if (line.empty())
        {
            return;
        }
        //2.从请求行中提取三个字段
    }

public:
    string inbuffer;
    string method;      //请求行get方法
    string url;         //请求行url
    string 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\n
        string 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\n
        string 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.coloradmin.cn/o/798468.html

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

相关文章

LeetCode Top100 Liked 题单(序号1~17)

01Two Sum - LeetCode 我自己写的代码【193ms】 因为不知道怎么加cmp函数&#xff0c;就只能pair的first设为值了&#xff0c;但其实这也是瞎做&#xff0c;应该也是O(n&#xff09;吧 class Solution { public:vector<int> twoSum(vector<int>& nums, int …

【观察】智能运维的“下半场”,看云智慧如何“开新局”

毫无疑问&#xff0c;随着数字化转型的加速&#xff0c;越来越多的企业正在把数字化战略提升到一个全新的高度&#xff0c;转型的进程也正从“浅层次”的数字化走向“深层次”的数字化。 也正因此&#xff0c;过去传统的人工运维方式越来越“捉襟见肘”&#xff0c;谋求运维模…

飞桨paddlespeech语音唤醒推理C定点实现

前面的文章&#xff08;飞桨paddlespeech语音唤醒推理C浮点实现&#xff09;讲了飞桨paddlespeech语音唤醒推理的C浮点实现。但是嵌入式设备通常CPU频率低和memory小&#xff0c;在嵌入式设备上要想流畅的运行语音唤醒功能&#xff0c;通常用的是定点实现。于是我就在浮点实现&…

【redis】通过配置文件简述redis的rdb和aof

redis的持久化方式有2种&#xff0c;rdb&#xff0c;即通过快照的方式将全量数据以二进制记录在磁盘中&#xff0c;aof&#xff0c;仅追加文件&#xff0c;将增量的写命令追加在aof文件中。在恢复的时候&#xff0c;rdb要更快&#xff0c;但是会丢失一部分数据。aof丢失数据极少…

HTML快速学习

目录 一、网页元素属性 1.全局属性 2.标签 2.1其他标签 2.2表单标签 2.3图像标签 2.4列表标签 2.5表格标签 2.6文本标签 二、编码 1.字符的数字表示法 2.字符的实体表示法 三、实践一下 一、网页元素属性 1.全局属性 id属性是元素在网页内的唯一标识符。 class…

207. 课程表 Python

文章目录 一、题目描述示例 1示例 2 二、代码三、解题思路 一、题目描述 你这个学期必须选修 numCourses 门课程&#xff0c;记为 0 到 numCourses - 1 。 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出&#xff0c;其中 prerequisites[i] [ai, …

入门Linux基本指令(2)

这篇文章主要提供一些对文件操作的Linux基本指令&#xff0c;希望对大家有所帮助&#xff0c;三连支持&#xff01; 目录 cp指令(复制) mv指令(剪切) nano指令 cat指令(打印文件内容) > 输出重定向 >> 追加重定向 < 输入重定向 more指令 less指令(推荐) …

影刀下载,插件安装

1、下载 在影刀官网下载&#xff1a;www.yingdao.com 2、谷歌插件安装 参考&#xff1a; 影刀插件安装各种方式 浏览器安装插件说明 - 影刀帮助中心 安装说明&#xff1a;驱动外置 Chrome 需要安装插件&#xff0c;并且保证此插件处于开启状态 方式一&#xff1a;用户头…

利用sklearn 实现线性回归、非线性回归

代码&#xff1a; import pandas as pd import numpy as np import matplotlib import random from matplotlib import pyplot as plt from sklearn.preprocessing import PolynomialFeatures from sklearn.linear_model import LinearRegression# 创建虚拟数据 x np.array(r…

nacos安装与基础配置

源码 https://github.com/alibaba/nacos https://gitee.com/mirrors/Nacos 编译 git clone https://github.com/alibaba/nacos.git cd nacos/ mvn -Prelease-nacos -Dmaven.test.skiptrue clean install -U ls -al distribution/target/// change the $version to your ac…

24考研数据结构-队列1

目录 3.2队列&#xff08;Queue&#xff09;3.2.1队列的基本概念3.2.2队列的顺序存储结构3.2.2.1 队列存储的基本操作3.2.2.2 循环队列 基本操作和判空方式 \color{Red}{基本操作和判空方式} 基本操作和判空方式3.2.2.3 知识回顾 3.2队列&#xff08;Queue&#xff09; 3.2.1队…

解读RSAC 2021丨灵魂拷问:你的网络够“皮实”吗?

美国时间5月20日&#xff0c;RSA大会落下帷幕。大会虽已结束&#xff0c;讨论还在继续。对于大会的主题“Resilience”&#xff0c;每个厂商、每个人都有自己的解读。 山石网科新技术研究院全程关注RSA大会&#xff0c;对于“Resilience”&#xff0c;他们的解读简单易懂接地气…

Java面向对象 - 常用类——Object类

什么是Object类 Java中有一个比较特殊的类&#xff0c;就是 Object类&#xff0c;它是所有类的父类&#xff0c;如果一个类没有使用extends关键字明确标识继承另外一个类&#xff0c;那么这个类就默认继承 Object类。因此&#xff0c;Object 类是 Java 类层中的最高层类&#x…

【C语言进阶篇】指针都学完了吧!那回调函数的应用我不允许还有人不会!

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《C语言初阶篇》 《C语言进阶篇》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 &#x1f4cb; 前言&#x1f4ac; 函数指针数组&#x1f4ad; 函数指针数组的定义&#x1f4ad; 函数指针数组的…

Android性能优化之游戏引擎初始化ANR

近期&#xff0c;着手对bugly上的anr 处理&#xff0c;记录下优化的方向。 借用网上的一张图&#xff1a; 这里的anr 问题是属于主线程的call 耗时操作。需要使用trace 来获取发生anr前一些列的耗时方法调用时间&#xff0c;再次梳理业务&#xff0c;才可能解决。 问题1 ja…

Java Servlet实现下拉选择查询(双表)和单列模式

0目录 1.Servlet实现下拉选择查询&#xff08;双表&#xff09; 2.单列模式 1.Servlet实现下拉选择查询&#xff08;双表&#xff09; 新建数据库和表 实体类 接口方法 实现接方法 Servlet类 Web.xml List.jsp 页面效果 加入功能 2.单列模…

批发零售进销存哪个好?盘点5款主流批发零售进销存软件!

在我看来&#xff0c;几乎没有批发零售行业不需要做进销存管理&#xff0c;哪怕是路边一个小摊贩&#xff0c;也需要做进销存管理&#xff0c;但是传统的进销存过程中存在很多问题&#xff1a; 前后方协作困难&#xff1a;采购/销售/财务工作相互独立&#xff0c;工作入口不一…

机器学习深度学习——多层感知机

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——感知机 &#x1f4da;订阅专栏&#xff1a;机器学习&&深度学习 希望文章对你们有所帮助 上一节…

Java阶段五Day14

Java阶段五Day14 文章目录 Java阶段五Day14分布式事务整合demo案例中架构&#xff0c;代码关系发送半消息本地事务完成检查补偿购物车消费 鲁班周边环境调整前端启动介绍启动前端 直接启动的项目gateway&#xff08;网关&#xff09;login&#xff08;登录注册&#xff09;atta…

DSA之图(4):图的应用

文章目录 0 图的应用1 生成树1.1 无向图的生成树1.2 最小生成树1.2.1 构造最小生成树1.2.2 Prim算法构造最小生成树1.2.3 Kruskal算法构造最小生成树1.2.4 两种算法的比较 1.3 最短路径1.3.1 两点间最短路径1.3.2 某源点到其他各点最短路径1.3.3 Dijkstra1.3.4 Floyd 1.4 拓扑排…