【应用层】Http协议的学习

news2024/11/14 18:30:22

文章目录

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


前言

http/https都是应用层协议,下面是应用层的作用:

应用层主要负责应用程序之间的沟通,如简单电子邮件传输(SMTP),文件传输协议(FTP),网络远程访问协议(Telnet)等

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/818922.html

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

相关文章

支付宝调试问题

网页支付返回表单不正确显示 升级前现象&#xff1a; SpringBoot 的返回给前台的<form>表单会自动提交&#xff0c;结果一直提示这个&#xff0c;而不是期望的支付宝登录页 实际得到这个&#xff1a; 期望得到这个&#xff1a; 因为沙箱账号是之前申请的&#xff0c;所…

[CrackMe]Cruehead.2.exe的逆向及注册机编写

1. 逆向工程 这个版本连一个注册界面也没有 进去一看, 他打开了一个CRACKME32.KEY, 估计里面就是放key的, 于是我随便写了一些数字进去 从CRACKME32.KEY中读取18个字节, 然后确认读取是否成功, 可见密码是18字节, 回去把密码长度改成18在重新调试 接着把key值输入CalcHash函…

AMEYA:尼得科科宝滑动型DIP开关CVS产品参数及价格​

日本电产尼得科科宝滑动型DIP开关CVS采用紧凑设计&#xff0c;3bit产品&#xff0c;旋钮把手高度为0.2mm&#xff0c;操作性良好端子为1mm间距&#xff0c;电路数丰富(2,3,4,8)端接样式为鸥翼式&#xff0c;J形引线使用树脂材料符合UL认证94V-0 符合RoHS规范。 日本电产尼得科科…

11-矩阵的运算_加减法_数乘_转置

矩阵的运算 加法&#xff0c;数乘&#xff0c;减法&#xff0c;转置 矩阵的加减 矩阵的加法就是矩阵的对应位置相加&#xff0c;减法也是一样就是对应位置相减 数乘 转置 转置的操作和向量是一样的&#xff0c;就是把 aij 变成 aji&#xff0c;把行和列互换一下 对于矩阵而…

【低代码开发】:探索应用开发的未来趋势

低代码开发&#xff1a;加速应用开发的未来趋势 引言什么是低代码以及功能特点&#xff1f;什么是低代码开发&#xff1f;低代码平台的特点和功能低代码平台的应用场景和优势低代码的优点低代码的缺点低代码平台项目开发流程选择和实施低代码平台 低代码未来的发展趋势低代码平…

vue - 【完整源码】实现评论区发表评论、回复评论、评论盖楼等功能,前端PC网站/移动端H5实现多用户评论与回复功能(详细示例源码,一键复制开箱即用)

效果图 在vue项目开发中,实现一个类似社交软件的评论区发表留言及回复等评论功能效果,可以无限回复盖楼。 一、功

视频传输网安全防护体系

在电脑、手机信息安全保护得到广泛关注和普及的今天&#xff0c;监控摄像头等设备的安全防护仍为大众所忽略&#xff0c;大量视频监控网络的前端设备和数据没有任何保护&#xff0c;完全暴露在互联网中。 前端IP接入设备与后端业务系统处于直连状态&#xff0c;一旦有攻击者或…

点播播放器如何自定义额外信息(统计信息传值)

Web播放器支持设置观众信息参数&#xff0c;设置后在播放器上报的观看日志中会附带观众信息&#xff0c;这样用户就可以通过管理后台的统计页面或服务端API来查看特定观众的视频观看情况了。 播放器设置观众信息参数的代码示例如下&#xff1a; <div id"player"…

【100天精通python】Day20:文件及目录操作_os模块和os.psth模块,文件权限修改

目录 专栏导读 1 文件的目录操作 os模块的一些操作目录函数​编辑 os.path 模块的操作目录函数 2 相对路径和绝对路径 3 路径拼接 4 判断目录是否存在 5 创建目录、删除目录、遍历目录 专栏导读 专栏订阅地址&#xff1a;https://blog.csdn.net/qq_35831906/category_12…

C++模拟操作系统睡眠机制

在系统中定义一个变量bHiberable&#xff0c;如果是3分钟内休眠&#xff0c;那么每隔3分钟检测一次这个变量&#xff0c;如果为真&#xff0c;则进入睡眠&#xff0c;如果是假&#xff0c;就把这个标志设置为真。继续等待和检测。 程序阻止操作系统休眠的办法&#xff1a;操作…

Git 一篇文章搞懂git (万字长文)

索引 一. Git初识1.提出问题2.什么是版本控制器3.git安装 二. git本地仓库基本操作1.Git本地仓库相关命令2.认识工作区&#xff0c;暂存区&#xff0c;版本库3.第一次Git追踪管理文件4.**有关于打印提交日志的命令**5.验证.git文件的指针指向6.Git管理的再理解——修改7.版本回…

shell 脚本 if 判断使用方法

例如 1&#xff1a; shell if判断条件使用-n参数的使用 if [ -n $1 ] 当str非空的时候&#xff0c;为true if [[ -n $1 ]];then app_version$1 fi $1 不管我们是否传入参数&#xff0c;都是输出app_version$1 这行&#xff0c;也就是结果一直为true 例如 2&#xff1a; net…

CASS7.0裁剪面域图形

1、打开CASS7.0&#xff0c;绘制一个线状闭合图形&#xff0c;如下&#xff1a; 2、然后填充该线状地物&#xff0c;并删除线状地物&#xff0c;仅留下填充好的面域图形。如下&#xff1a; 3、恢复面域图形的边界线&#xff08;选中面域后&#xff0c;鼠标左键双击&#xff0c;…

Pytorch基础

文章目录 一、Pytorch简介二、安装2.1 安装GPU环境2.2 安装Pytorch2.3 测试 三、Tensor3.1 Tensor创建3.1.1 torch.tensor() && torch.tensor([])3.1.2 torch.randn && torch.randperm3.1.3 torch.range(begin,end,step)3.1.4 指定numpy 3.2 Tensor运算3.2.1 A…

mysql 非definer用户如何查看存储过程定义

当我们创建存储过程时&#xff0c;如果没有显示指定definer&#xff0c;则会默认当前用户为该sp的definer&#xff0c;如果没有相关授权&#xff0c;则其他用户是看不了这个sp的。 比如用户zhenxi1拥有如下权限&#xff1a; 它拥有对dev_nacos库的查询权限&#xff0c;这个时候…

流程节点图形变化

一、背景 &#xff08;1&#xff09;流程节点为矩形&#xff0c;只有上下左右四个连接点。 &#xff08;2&#xff09;支持移动&#xff0c;放大缩小&#xff0c;连接线。 二、需求 &#xff08;1&#xff09;流程节点支持图形变化。 &#xff08;2&#xff09;支持节点边框…

第7期ThreadX视频教程:如何实现RTOS高效的任务管理,抢占式调度,时间片调度和零中断延迟(2023-07-31)

视频教程汇总帖&#xff1a;https://www.armbbs.cn/forum.php?modviewthread&tid110519 本期视频为大家分享高效的RTOS任务管理设计&#xff0c;通过这个点来引出抢占式调度&#xff0c;时间片调度&#xff0c;任务优先级设置和零中断延迟。 RTOS任务高效管理是我们使用R…

吃透《西瓜书》第四章 决策树定义与构造、ID3决策树、C4.5决策树、CART决策树

目录 一、基本概念 1.1 什么是信息熵&#xff1f; 1.2 决策树的定义与构造 二、决策树算法 2.1 ID3 决策树 2.2 C4.5 决策树 2.3 CART 决策树 一、基本概念 1.1 什么是信息熵&#xff1f; 信息熵: 熵是度量样本集合纯度最常用的一种指标&#xff0c;代表一个系统中蕴…

Python小红书旋转验证码识别

本周免费接了一个用户的需求&#xff0c;研究了一下小红书旋转验证码。刚开始小瞧了它&#xff0c;觉得它应该没有百度旋转验证码那么难&#xff0c;毕竟图像没有干扰&#xff0c;需要的训练样本就可以很少。然而事情并没有这么简单&#xff0c;所以记录一下。 首先看一下最终…