【Linux】HTTP协议,Linux下的HTTP协议详解

马肤

温馨提示:这篇文章已超过467天没有更新,请注意相关的内容是否还可用!

摘要:本文简要介绍了Linux系统中的HTTP协议。HTTP是一种用于传输数据的协议,广泛应用于Web浏览器和服务器之间的通信。在Linux系统中,HTTP协议扮演着关键角色,使得Web应用程序能够正常运作。通过HTTP协议,Linux系统可以实现与Web服务器的通信,获取网页内容并展示给用户。HTTP协议在Linux系统中的重要性不言而喻,是现代Web应用不可或缺的一部分。

HTTP协议

  • 1.认识URL
  • 2.urlencode和urldecode
  • 3.HTTP协议格式
  • 4.HTTP协议基本工作流程
  • 5.HTTP的方法
  • 6.HTTP的状态码
  • 7.HTTP常见Header
  • 8.长连接
  • 9.cookie&&session会话保持
  • 10.基本工具(postman,fiddler)

    【Linux】HTTP协议,Linux下的HTTP协议详解 第1张

    喜欢的点赞,收藏,关注一下把!【Linux】HTTP协议,Linux下的HTTP协议详解 第2张

    目前基本socket写完,一般服务器设计原则和方式(多进程、多线程、线程池)+常见的各种场景,自定义协议+序列化和反序列化都已经学过了。

    那有没有人已经针对常见场景,早就已经写好了常见的协议软件,供我们使用呢?

    当然了,最典型的HTTP。未来它们做的事情和我们以前做的事情是一样的!只不过HTTP是结合它的应用场景来谈的。

    虽然我们现在关于http协议不知道它是什么。但我们知道你的http协议里面必有套接字,必有序列化和反序列的机制,也必须添加报头和分离报头的过程等等。

    在说这个http协议之前,我们先做一个预备工作,在网络基础一我们知道OSI分七层前三层分别是应用层,表示层,会话层。在TCP/IP协议这三层合起来算一层应用层。

    【Linux】HTTP协议,Linux下的HTTP协议详解 第3张

    在上篇文章说过我们写的网络版计数器软件分成三层。第一层获取链接多进程或者多线程或线程池进行处理,第二层handlerEntery进行读取完整报文、提取有效载荷、序列化反序列化等一系列工作,第三层进行业务处理callback。其实我们自己写的的第一层就是会话层,第二层就是表示层,第三层callback进行对应的业务逻辑处理就是应用层。

    OSI定义成七层,原因就是后面写代码时每一层都少不了。OSI为什么没把这三层压成一层呢?在于表示层有自定义的方案、Json方案、protobuf方案、xml方案等等,如果它某种方案写到内核里就固定下来了,而实际上我们并没有固定。

    http作为应用层协议它也要解决刚才说的三个工作。

    1.认识URL

    平时我们俗称的 “网址” 其实就是说的 URL

    https://blog.csdn.net/fight_p/article/details/137103487

    https -> 协议

    blog.csdn.net -> 域名,域名等价于IP,这里会有一个域名解析的工作(把域名这个字符串结构转化成IP地址),IP标识一台网络主机(Linux系统)

    /fight_p/article/details/137103487 -> 文件路径

    URL的作用就是,浏览器通过拿着这个URL,找到这台Linux机器然后在这台机器上找对应的文件。把文件打开返回给浏览器。

    【Linux】HTTP协议,Linux下的HTTP协议详解 第4张

    实际上URL有多种格式。

    【Linux】HTTP协议,Linux下的HTTP协议详解 第5张

    为什么我们刚才URL没有端口呢?

    我们对应的协议是和端口号强相关的。服务器端口号是一个众所周知的端口号,一般不能随便改变,刚才URL没有写出来并不代表没有,因为客户端访问服务器端一定要知道服务端的是IP地址和端口号。这里没有但浏览器在真正请求时会给我们填上,浏览器结合我们用的协议就知道用的端口号是多少。默认一些协议对应的端口号:

    http:80

    https:443

    【Linux】HTTP协议,Linux下的HTTP协议详解 第6张

    这里圈起来的是什么东西呢?

    其实它并不是根目录,而是web根目录,一般而言,可以是Linux下的任意一个目录。这个任意目录必须要有对应请求的资源。(后面写代码的时候具体解释)

    http是文本传输协议。说白了http协议就是从服务器拿下来对应的 “资源”。

    什么是资源呢?

    凡是你在网络中看到的都是资源!(如:刷的短视频是视频文件、淘宝上看到的图片是图片文件,网易云听的音乐是音乐文件。。。)

    所有的资源都可以看做资源文件,在服务器中都是以文件形式存在磁盘中的文件系统中的某一个路径下,所以需要Linux系统的路径结构。当要的时候把磁盘中对应的路径所标识的资源返回给客户端。所以http协议本质上是从服务器上拿文件资源。

    因为文件资源的种类特别多,http都能搞定,所以http是 “超文本传输协议”。

    2.urlencode和urldecode

    像 / ? : 等这样的字符, 已经被url当做特殊意义理解了。因此这些字符不能随意出现。比如, 某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义。

    如果原封不动会干扰url的正确解析,所以浏览器这端必须先编码然后提给服务端。

    【Linux】HTTP协议,Linux下的HTTP协议详解 第7张

    那如何编码&&如何解码,需要自己做吗?

    编码转义的规则如下:

    将需要转码的字符转为16进制。一个字符占8个比特位,然后从右到左,取4个比特位转成16进制数(不足4位直接处理),每2位16机制数前面加上%,编码成%XY格式

    解码就是把收到的这些东西转成2进制的格式

    我们写服务端如果从0开始写这个解码工作一定要自己做,但是我们在网上搜索url decode C++源码,当一个CV工程师。

    如何验证这个过程?

    urlencode工具

    3.HTTP协议格式

    接下面我们先从宏观方面说说http请求和响应的格式

    http请求是以行为单位的一种协议。

    第一行

    第一列:请求的方法

    第二列:url

    第三列:请求的http的版本

    常见的版本:http 1.0 、1.1、 2.0版本

    行以\r\n为分隔符,或者以\n

    【Linux】HTTP协议,Linux下的HTTP协议详解 第8张

    第二大块也是以行为单位的,只不过这一大块会存在多行,多行包含http请求各种属性,属性几乎都以name:value的样子

    而我们又把第一行称为请求行,第二大快称为请求报头

    【Linux】HTTP协议,Linux下的HTTP协议详解 第9张

    第三大块,特别要强调一下是http请求的空行,相当重要

    【Linux】HTTP协议,Linux下的HTTP协议详解 第10张

    最后一块是请求正文,可以没有也可以有,如果未来想把自己要登录账号可以把账号和密码放在正文,也就是说想给服务器提上去的参数就放在正文

    【Linux】HTTP协议,Linux下的HTTP协议详解 第11张

    上面四大块就是http request请求的完整报文。它会通过tcp链接,向服务器发送过去。

    http响应格式几乎和请求格式是一样的,也分四部分。

    第一行是状态行,也有三列构成,中间由空格分开。

    第一列:是http版本

    第二列:状态码,如200、400、302、307、500、404,如404我们常见的页面不存在。状态码用来表示请求结果是否正确,就如网络版本计数器我们写的exitcode。

    第三列:状态码描述 如404 -> Not Found、200 -> OK

    【Linux】HTTP协议,Linux下的HTTP协议详解 第12张

    第二大块也是由多行构成的,叫做响应报文

    【Linux】HTTP协议,Linux下的HTTP协议详解 第13张

    第三块同样也是空行

    【Linux】HTTP协议,Linux下的HTTP协议详解 第14张

    第四大块,在响应里会高频的出现,叫做响应正文(有效载荷),通常带html/css/js/图片/视频/音频等

    【Linux】HTTP协议,Linux下的HTTP协议详解 第15张

    这四大部分构成了响应报文。在根据tcp链接socket,向客户端返回响应。未来所有http通信都采用的是这种方案进行通信的。

    【Linux】HTTP协议,Linux下的HTTP协议详解 第16张

    现在我们在谈一谈通信细节问题

    1.请求和响应怎么保证应用层完整读取完毕了呢?

    首先我们发现http请求都是字符串按行为单位,所以

    1. 我可以读取完整的一行
    2. while(读取完整一行) --> 所有的请求行+请求行报文全部读完 --> 直到空行!
    3. 我们没说正文也是按行为单位分开的没有办法保证把正文读完,但是我们能保证把报头读完,而报头里有一个Content-Length:xxx(代表正文长度)
    4. 解析出来内容长度,在根据内容长度,读取正文即可!

    2.请求和响应是怎么做到序列化和反序列化的?

    http是用的特殊字符自己实现的。http序列化什么都不做直接发就好了,反序列化 :第一行+请求/响应报头,只要按照\r\n将字符串1->n即可!不用借助任何东西如Json

    protobuf等。而正文序列化反序列也不用做直接发送就行了。如果你的正文携带结构化数据就要自己处理了。

    接下来我们写代码的方式,验证上面说的东西。

    以前写udp和tcp我们都写过服务端用过套接字,这里还是直接拿过来用。

    4.HTTP协议基本工作流程

    Protocol.hpp

    #pragma once
    #include 
    #include 
    using namespace std;
    class httpRequest
    {
    public:
        httpRequest(){};
        ~httpRequest(){};
    public:
        string inbuffer;//缓冲区
        
        //下面我们先不管,未来都可以细分,序列化反序列也都可以写到类中,我们这里写简单一点主要看一下http的细节
        // string reqline;//请求行
        // vector reqheader;//报头
        // string body;//请求正文
        
        //第一行细分
        //string method;
        //string url;
        //string httpversion;
    };
    class httpResponse
    {
    public:
        string outbuffer;//缓冲区
    };
    

    httpServer.hpp

    #pragma once
    #include "protocol.hpp"
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    enum
    {
        USAGG_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };
    const int backlog = 5;
    typedef function func_t;
    void handlerEntery(int sock,func_t callback)
    {
        // 1. 读到完整的http请求
        // 2. 反序列化
        // 3. httprequst, httpresponse, callback(req, resp)
        // 4. resp序列化
        // 5. send
    }
    class httpServer
    {
    public:
        httpServer(const uint16_t port) : _port(port), _listensock(-1)
        {
        }
        void initServer()
        {
            // 1.创建socket文件套接字对象
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock  
    

    httpServer.cc

    #include "httpServer.hpp"
    #include 
    void Usage(string proc)
    {
        cout 
    }
    // ./httpserver port
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            Usage(argv[0]);
            exit(USAGG_ERR);
        }
        uint16_t serverport = atoi(argv[1]);
        unique_ptr
        // 1. 读到完整的http请求
        // 2. 反序列化
        // 3. httprequst, httpresponse, callback(req, resp)
        // 4. resp序列化
        // 5. send
    	
    	char buffer[4096];
        httpRequest req;
        httpResponse resp;
        ssize_t n=recv(sock,buffer,sizeof(buffer)-1,0);//大概率我们直接就能读取到完整的http请求
        if(n0)
        {
            buffer[n]=0;
            req.inbuffer=buffer;
            callback(req,resp);
            send(sock,resp.outbuffer.c_str(),resp.outbuffer.size(),0);
        }
    }
    
    void Get(const httpRequest &req, httpResponse &resp)
    {
        cout 
        cout 
        cout 
    public:
        // xxx yyy zzz\r\naaa
        static string GetOneline(string &buffer, const string &sep)
        {
            auto pos = buffer.find(sep);
            if (pos == string::npos)
                return "";
            string sub = buffer.substr(0, pos);
            return sub;
        }
    };
    
    public:
        httpRequest(){};
        ~httpRequest(){};
        void parse()
        {
            // 1. 从inbuffer中拿到第一行,分隔符\r\n
            string line = Util::GetOneline(inbuffer, sep);
            if (line.empty())
                return;
                
            // 2. 从请求行中提取三个字段
            istringstream iss(line);
            iss > method >> url >> httpversion;
        }
    public:
        string inbuffer;
        // string reqline;
        // vector reqheader;
        // string body;
        string method;
        string url;
        string httpversion;
    };
    
    void handlerEntery(int sock,func_t callback)
    {
        // 1. 读到完整的http请求
        // 2. 反序列化
        // 3. httprequst, httpresponse, callback(req, resp)
        // 4. resp序列化
        // 5. send
        char buffer[4096];
        httpRequest req;
        httpResponse resp;
        ssize_t n=recv(sock,buffer,sizeof(buffer)-1,0);// 大概率我们直接就能读取到完整的http请求
        if(n>0)
        {
            buffer[n]=0;
            req.inbuffer=buffer;
            
            req.parse();
            callback(req,resp);
            send(sock,resp.outbuffer.c_str(),resp.outbuffer.size(),0);
        }
    }
    
    void Get(const httpRequest &req, httpResponse &resp)
    {
        cout 
    public:
    	//。。。
        void parse()
        {
            // 1. 从inbuffer中拿到第一行,分隔符\r\n
            string line = Util::GetOneline(inbuffer, sep);
            if (line.empty())
                return;
            // 2. 从请求行中提取三个字段
            istringstream iss(line);
            iss  method >> url >> httpversion;
            // 3. 添加web默认路径
            path = default_root;// ./wwwroot
            path += url;//  ./wwwroot/a/b/c.html 请求具体资源
            //刚才我们看到url只有/的样子,这里也要拼  ./wwwroot/
            //但是这里就遭了,还是不知道访问的那个具体的资源
            //其实对于一个服务器来说,它有自己的主页信息
            //如果访问的是根目录,就把首页给拼上
            if (path[path.size() - 1] == '/')//判断是否是根目录
                path += home_page;
                
         }
         
    public:
    	string inbuffer;
    	
    	string method;
    	string url;
    	string httpversion;
    	string path;
    };    
    

    【Linux】HTTP协议,Linux下的HTTP协议详解 第17张

    【Linux】HTTP协议,Linux下的HTTP协议详解 第18张

    在url是这样请求的,但是实际上web服务器它会自己拼前缀,带着这个路径去找对应资源文件,如果有就返回,没有就返回404。

    接下来我们做服务器和网页分离

    我们让body从文件中读取,因此添加一个readFile接口,把文件内容全部读到body里。

    class Util
    {
    public:
        // xxx yyy\r\nzzz
        static string GetOneline(string &buffer, const string &sep)
        {
            auto pos = buffer.find(sep);
            if (pos == string::npos)
                return "";
            string sub = buffer.substr(0, pos);
            return sub;
        }
        static bool readFile(const string &path, string &body)
        {
            ifstream ofs(path,ios_base::binary);//二进制方式读
            if (!ofs.is_open())
                return false;
            string line;
            while(getline(path, line))
            {
                body += line;
            }
            ofs.close();
            return true;
        }
    };
    

    【Linux】HTTP协议,Linux下的HTTP协议详解 第19张

    void Get(const httpRequest &req, httpResponse &resp)
    {
        cout 
            Util::readFile(html_404, body); // 读取失败返回404,一定能成功
        }
       
    	//序列化
         resp.outbuffer += respline;
    	 resp.outbuffer += respheader;
         resp.outbuffer += respblank;
         resp.outbuffer += body;
    }
    
    public:
        httpRequest(){};
        ~httpRequest(){};
        void parse()
        {
            // 1. 从inbuffer中拿到第一行,分隔符\r\n
            string line = Util::GetOneline(inbuffer, sep);
            if (line.empty())
                return;
            // 2. 从请求行中提取三个字段
            istringstream iss(line);
            iss  method  url  httpversion;
            // 3. 添加web默认路径
            path = default_root + url;
            if (path[path.size() - 1] == '/')
                path += home_page;
            // 4. 获取path对应的资源后缀
            // ./wwwroot/index.html
            // ./wwwroothttps://blog.csdn.net/test/a.html
            // ./wwwroothttps://blog.csdn.net/image/1.jpg
            auto pos=path.rfind(".");
            if(pos == string::npos) suffix=".html";
            suffix=path.substr(pos);
        }
    public:
        string inbuffer;
        string method;
        string url;
        string httpversion;
        string path;
        string suffix;
    };
    
        string type = "Content-Type: ";
        if (suff == ".html")
            type += "text/html";
        else if (suff == ".jpg")
            type += "application/x-jpg";
        type += "\r\n";
        return type;
    }
    void Get(const httpRequest &req, httpResponse &resp)
    {
        cout 
            Util::readFile(html_404, body); // 读取失败返回404,一定能成功
        }
       
    	//序列化
         resp.outbuffer += respline;
    	 resp.outbuffer += respheader;
         resp.outbuffer += respblank;
         resp.outbuffer += body;
    }
    
    public:
        httpRequest(){};
        ~httpRequest(){};
        void parse()
        {
            // 1. 从inbuffer中拿到第一行,分隔符\r\n
            string line = Util::GetOneline(inbuffer, sep);
            if (line.empty())
                return;
            // 2. 从请求行中提取三个字段
            istringstream iss(line);
            iss  method  url  httpversion;
            // 3. 添加web默认路径
            path = default_root + url;
            if (path[path.size() - 1] == '/')
                path += home_page;
            // 4. 获取path对应的资源后缀
            // ./wwwroot/index.html
            // ./wwwroothttps://blog.csdn.net/test/a.html
            // ./wwwroothttps://blog.csdn.net/image/1.jpg
            auto pos=path.rfind(".");
            if(pos == string::npos) suffix=".html";
            suffix=path.substr(pos);
    		// 5. 得到资源的大小
            struct stat sif;
            if(stat(path.c_str(),&sif) == 0)
                size=sif.st_size;
            else    
                size=-1;
        }
    public:
        string inbuffer;
        string method;
        string url;
        string httpversion;
        string path;
        string suffix;
        int size;
    };
    
        string type = "Content-Type: ";
        if (suff == ".html")
            type += "text/html";
        else if (suff == ".jpg")
            type += "application/x-jpg";
        type += "\r\n";
        return type;
    }
    void Get(const httpRequest &req, httpResponse &resp)
    {
        cout 
            respheader += "Content-Length: ";
            respheader += to_string(req.size);
            respheader += "\r\n";
        }
        
        string respblank = "\r\n";
        string body;
        body.resize(req.size + 1);
        if (!Util::readFile(req.path, body))
        {
            // 找不到文件,文件大小是-1,要返回404.html,因此重新计算大小
            //否则body大小是-1,404.html文件内容就读不到body里
            struct stat sif;
            if (stat(html_404.c_str(), &sif) == 0)
                body.resize(sif.st_size + 1);
            Util::readFile(html_404, body); // 一定能成功
        }
       
    	//序列化
         resp.outbuffer += respline;
    	 resp.outbuffer += respheader;
         resp.outbuffer += respblank;
         resp.outbuffer += body;
    }
    
    public:
        // xxx yyy\r\nzzz
        static string GetOneline(string &buffer, const string &sep)
        {
            auto pos = buffer.find(sep);
            if (pos == string::npos)
                return "";
            string sub = buffer.substr(0, pos);
            return sub;
        }
        static bool readFile(const string &path, string &body)
        {
            ifstream ofs(path,ios_base::binary);
            if (!ofs.is_open())
                return false;
            ofs.read((char *)body.c_str(), body.size());
            ofs.close();
            return true;
        }
    };
    
    public:
        httpRequest(){};
        ~httpRequest(){};
        void parse()
        {
            // 1. 从inbuffer中拿到第一行,分隔符\r\n
            string line = Util::GetOneline(inbuffer, sep);
            if (line.empty())
                return;
            // 2. 从请求行中提取三个字段
            istringstream iss(line);
            iss  method  url  httpversion;
    		//考虑提参的情况
            // 2.1 /search?name=zhangsan&pwd=12345
            // 通过?将左右进行分离
            // 如果是POST方法,本来就是分离的!
            // 左边PATH, 右边parm
            // 3. 添加web默认路径
            path = default_root + url;
            if (path[path.size() - 1] == '/')
                path += home_page;
            // 4. 获取path对应的资源后缀
            // ./wwwroot/index.html
            // ./wwwroothttps://blog.csdn.net/test/a.html
            // ./wwwroothttps://blog.csdn.net/image/1.jpg
            auto pos=path.rfind(".");
            if(pos == string::npos) suffix=".html";
            suffix=path.substr(pos);
            // 5. 得到资源的大小
            struct stat sif;
            if(stat(path.c_str(),&sif) == 0)
                size=sif.st_size;
            else    
                size=-1;
        }
    public:
        string inbuffer;
        string method;
        string url;
        string httpversion;
        string path;
        string suffix;
        int size;
        string parm;
    };
    
         if(req.path == "test.py")
         {
             //建立进程间通信,pipe
             //fork创建子进程,子进程执行这个脚本 execl("/bin/python", test.py)
             // 父进程,将req.parm 通过管道写给某些后端语言,py,java,php等语言
             //这也是为什么服务器是c++写的,后端是其他语言写的
         }
         if(req.path == "/search")
         {
             // req.parm
             // 使用我们自己写的C++的方法,提供服务
         }
    	//。。。
    }
    
    	//。。。
        void registerCb(string servicename, func_t cb)
        {
            funcs.insert(make_pair(servicename, cb));
        }
    	
    	void handlerEntery(int sock)
    	{
    	    // 1. 读到完整的http请求
    	    // 2. 反序列化
    	    // 3. httprequst, httpresponse, callback(req, resp)
    	    // 4. resp序列化
    	    // 5. send
    	
    	    char buffer[4096];
    	    httpRequest req;
    	    httpResponse resp;
    	    ssize_t n=recv(sock,buffer,sizeof(buffer)-1,0);// 大概率我们直接就能读取到完整的http请求
    	    if(n0)
    	    {
    	        buffer[n]=0;
    	        req.inbuffer=buffer;
    	        req.parse();
    	        funcs[req.path](req, resp);
    	        send(sock,resp.outbuffer.c_str(),resp.outbuffer.size(),0);
    	    }
    	}
    	
    private:
        uint16_t _port;
        int _listensock;
        unordered_map
        if (argc != 2)
        {
            Usage(argv[0]);
            exit(USAGG_ERR);
        }
        uint16_t serverport = atoi(argv[1]);
        unique_ptr
        cout 
            respheader += "Content-Length: ";
            respheader += to_string(req.size);
            respheader += "\r\n";
        }
    	//Location配套重定向,告诉浏览器去哪里访问
        respheader += "Location: https://www.baidu.com/\r\n";
        string respblank = "\r\n";
        string body;
        body.resize(req.size + 1);
        if (!Util::readFile(req.path, body))
        {
            // 找不到文件,文件大小是-1,要返回404.html,因此重新计算大小
            struct stat sif;
            if (stat(html_404.c_str(), &sif) == 0)
                body.resize(sif.st_size + 1);
            Util::readFile(html_404, body); // 一定能成功
        }
        resp.outbuffer += respline;
        resp.outbuffer += respheader;
        resp.outbuffer += respblank;
        
        cout 
        cout 
            respheader += "Content-Length: ";
            respheader += to_string(req.size);
            respheader += "\r\n";
        }
        respheader += "Location: https://www.baidu.com/\r\n";
        respheader+="Set-Cookie: 123456abc\r\n";//
        string respblank = "\r\n";
        // string body="

0
收藏0
文章版权声明:除非注明,否则均为VPS857原创文章,转载或复制请以超链接形式并注明出处。

相关阅读

  • 【研发日记】Matlab/Simulink自动生成代码(二)——五种选择结构实现方法,Matlab/Simulink自动生成代码的五种选择结构实现方法(二),Matlab/Simulink自动生成代码的五种选择结构实现方法详解(二)
  • 超级好用的C++实用库之跨平台实用方法,跨平台实用方法的C++实用库超好用指南,C++跨平台实用库使用指南,超好用实用方法集合,C++跨平台实用库超好用指南,方法与技巧集合
  • 【动态规划】斐波那契数列模型(C++),斐波那契数列模型(C++实现与动态规划解析),斐波那契数列模型解析与C++实现(动态规划)
  • 【C++】,string类底层的模拟实现,C++中string类的模拟底层实现探究
  • uniapp 小程序实现微信授权登录(前端和后端),Uniapp小程序实现微信授权登录全流程(前端后端全攻略),Uniapp小程序微信授权登录全流程攻略,前端后端全指南
  • Vue脚手架的安装(保姆级教程),Vue脚手架保姆级安装教程,Vue脚手架保姆级安装指南,Vue脚手架保姆级安装指南,从零开始教你如何安装Vue脚手架
  • 如何在树莓派 Raspberry Pi中本地部署一个web站点并实现无公网IP远程访问,树莓派上本地部署Web站点及无公网IP远程访问指南,树莓派部署Web站点及无公网IP远程访问指南,本地部署与远程访问实践,树莓派部署Web站点及无公网IP远程访问实践指南,树莓派部署Web站点及无公网IP远程访问实践指南,本地部署与远程访问详解,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南。
  • vue2技术栈实现AI问答机器人功能(流式与非流式两种接口方法),Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法探究,Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法详解
  • 发表评论

    快捷回复:表情:
    评论列表 (暂无评论,0人围观)

    还没有评论,来说两句吧...

    目录[+]

    取消
    微信二维码
    微信二维码
    支付宝二维码