【项目】实现web服务器

news2024/12/23 5:18:23

目录

1.需要实现的项目需求(web服务器的工作原理)

2.实现过程:

1.编写套接字 

2.多线程的代码和任务类

3.文件描述符的处理方法的框架

4.读取请求

4.1.读取请求行 

4.2.读取请求报头

4.3.分析请求行和报头

请求行的方法、URI、版本放到承装容器;

4.4.读取正文

5.构建响应

5.1.根据请求方法和是否带参来判断是否需要进行CGI处理:

 5.2.把URI处理合理

 6.CGI处理

7.发送响应

9.源码链接


1.需要实现的项目需求(web服务器的工作原理)

实现:从用户输入网页地址,到建立连接、获取和分析请求、对有参数的请求进行CGI机制处理(CGI机制:用户会访问web服务器的任意文件,服务器需要依靠参数对文件进行处理,不能把处理方法放在web服务器的代码下,因为用户只会访问它需要的,那么大量的处理方法是无意义的,而且内容太多了)、构建和发送响应的全过程;

下面是我画的一个实现全过程的思维导图

 

2.实现过程:

1.编写套接字 

1. 使用单例模式类封装的一个套接字 

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include <cstdlib>
#include <cstring>
#include"Log.hpp"

#define Backlog 5
class TcpSocket
{
private:
    void Socket()
    {
        _socket = socket(AF_INET, SOCK_STREAM, 0);
        if (_socket < 0)
        {
            LOG(FATAL,"socket error");
            exit(1);
        }
        // 快速重启
        int opt = 1;
        setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    }
    void Bind()
    {
        struct sockaddr_in local;
        // 把套接字置为0;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(_port);
        if (bind(_socket, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            LOG(FATAL,"bind error");
            exit(2);
        }
    }
    void Listen()
    {
        if (listen(_socket, Backlog) < 0)
        {
            LOG(FATAL,"listen error");
            exit(3);
        }
    }

public:
    void Init()
    {
        Socket();
        Bind();
        Listen();
    }
    static TcpSocket *GetInstance(int port)
    {
        static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
        if (_sigleton == nullptr)
        {
            // 防止多进程访问临界资源
            pthread_mutex_lock(&lock);
            if (_sigleton == nullptr)
            {
                _sigleton = new TcpSocket(port);
                _sigleton->Init();
            }
            pthread_mutex_unlock(&lock);
        }
        return _sigleton;
    }
    int GetSocket()
    {
        return _socket;
    }
    ~TcpSocket()
    {
        if (_socket >= 0)
            close(_socket);
    }

private:
    TcpSocket(int port) : _socket(-1), _port(port)
    {
    }
    TcpSocket(const TcpSocket &s)
    {
    }

private:
    int _socket;
    int _port;
    // 单例模式指针
    static TcpSocket *_sigleton;
};
TcpSocket *TcpSocket::_sigleton = nullptr;

2.获取请求,添加到多线程,sigpipe信号必须忽略,客服端可能随时关闭,读端关闭,写端收到sigpipe中止进程;

#include "TcpSocket.hpp"
#include "Protocol.hpp"
#include "Thread_pool.hpp"
#include<signal.h>
class HttpSocket
{
private:
    int _port;
    int _quit;

public:
    HttpSocket(int port) : _port(port), _quit(false)
    {
    }
    void InitServer()
    {
        //信号SIGPIPE需要进行忽略,如果不忽略,在写入时候,可能直接崩溃server
        //客户端随时可能关闭连接,
        signal(SIGPIPE, SIG_IGN); 
    }
    void loop()
    {   
        TcpSocket *segleton = TcpSocket::GetInstance(_port);
        int listen_socket = segleton->GetSocket();
        LOG(INFO,"build success,wait client");
        while (!_quit)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int new_socket = accept(listen_socket, (struct sockaddr *)&peer, &len);
            if(new_socket<0)
                continue;
            ThreadPool::GetInstance()->Push(new_socket);
        }
    }
};

主函数

#include "HttpServer.hpp"
#include <cstdlib>
#include <memory>

void Usage()
{
    std::cout << "usage: ./main port" << std::endl;
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage();
        return 1;
    }
    HttpSocket s(atoi(argv[1]));
    s.InitServer();
    s.loop();
    return 0;
}

2.多线程的代码和任务类

线程池工作原理:一个生产消费模型的多线程线程池,accept成功后把sock文件描述符添加到线程池,由线程池构建一个Task类对象,Task类对象包含一个函数指针,这个指针指向对sock文件描述符的处理方法(读取请求报头、分析请求报头、构建响应报头等等);

#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include "Log.hpp"
#include "Task.hpp"

#define NUM 5
class ThreadPool
{
private:
    void Lock()
    {
        pthread_mutex_lock(&_mt);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_mt);
    }
    void Wait()
    {
        pthread_cond_wait(&_cond, &_mt);
    }
    void Wakeup()
    {
        pthread_cond_signal(&_cond);
    }
    bool IfEmpty()
    {
        _q.empty();
    }
    void Pop(Task &t)
    {
        t = _q.front();
        _q.pop();
    }
    // 如果不是静态成员函数,那么参数实际有两个,一个是this指针
    static void *handler(void *arg)
    {
        Task t;
        pthread_detach(pthread_self());
        ThreadPool *tp = (ThreadPool *)arg;
        tp->Lock();
        while (tp->IfEmpty())
        {
            tp->Wait();
        }
        tp->Pop(t);
        tp->Unlock();
        t.ProcessOn();
    }

public:
    void Push(int sock)
    {
        Task ts(sock);
        _q.push(ts);
        Wakeup();
    }
    void InitThread()
    {
        pthread_t pt[_num];
        for (int i = 0; i < _num; i++)
        {
            if (pthread_create(&pt[i], nullptr, handler, this) != 0)
            {
                LOG(ERROR, "pthread_create fail");
                exit(1);
            }
        }
    }
    static ThreadPool *GetInstance()
    {
        static pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
        if (_sigleton == nullptr)
        {
            pthread_mutex_lock(&_mutex);
            //双重判断,防止上面的判断成功线程被切换,调度回来时可能已经被其他线程new
            if(_sigleton == nullptr)
            {
                _sigleton = new ThreadPool;
                _sigleton->InitThread();
                LOG(INFO,"thread init ok");
            }
            pthread_mutex_lock;
        }
        return _sigleton;
    }

private:
    ThreadPool() : _num(NUM)
    {
        pthread_mutex_init(&_mt, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    ThreadPool(const ThreadPool &tp) = delete;
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mt);
        pthread_cond_destroy(&_cond);
    }

private:
    int _num;
    std::queue<Task> _q;
    pthread_mutex_t _mt;
    pthread_cond_t _cond;
    static ThreadPool *_sigleton;
};
ThreadPool *ThreadPool::_sigleton = nullptr;

任务类:成员:1.accept返回的套接字文件描述符;2.对这个描述符的处理方法(读取请求报头、分析请求报头、构建响应报头等等);

#pragma once
#include <iostream>
#include "Protocol.hpp"
class Task
{
public:
    Task()
    {
    }
    Task(int sock) : _accept_sock(sock)
    {
    }
    ~Task()
    {
    }
    void ProcessOn()
    {
        _cb.ProcessOn(_accept_sock);
    }

private:
    int _accept_sock;
    Callback _cb;
};

3.文件描述符的处理方法的框架

框架:分别承装请求和响应报文的类,把它们当做结构体,用做读取的请求、构建的响应的存放;对sock文件描述符需要做读取和分析请求、构建和发送响应;

#pragma once
#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/sendfile.h>
#include <sys/wait.h>
#include <algorithm>
#include <sstream>
#include <vector>
#include <unordered_map>
#include "Util.hpp"
#include "Log.hpp"

#define WEB_ROOT "webroot"
#define PAGE_404 "webroot/page_404.html"
#define PAGE_400 "webroot/page_400.html"
#define PAGE_500 "webroot/page_500.html"
#define HOME_PAGE "index.html"
#define OK 202
#define CLIENT_ERR 400
#define SERVER_ERR 500
#define NOT_FOUND 404
#define END_LINE "\n"

// 做为承装请求报文的容器
class HttpRequest
{
public:
    std::string _request_line;
    std::vector<std::string> _request_header;
    std::string _blank;
    std::string _body;
};
// 做为承装构建响应报文的容器

class HttpResponse
{
public:
    std::string _status_line;
    std::vector<std::string> _status_header;
    std::string _blank;
    std::string _body;
};

class EndPoint
{
private:
    // 读取请求报头
    bool RecvRequestLine()
    {}
    bool RecvRequestHeader()
    {}
    // 分析请求报头
    void ParseRequestLine()
    {}
    bool RecvRequestBody()
    {}
private:
    void BuildStatusLine(int code)
    {}
public:
    // 读取
    void RecvRequest()
{}
    // 构建
    void BuildResponse()
    {}
    void SendResponse()
    {}
public:
    EndPoint(int sock) : _new_socket(sock), _stop(false)
    {}
    ~EndPoint()
    {
        if (_new_socket >= 0)
            close(_new_socket);
    }

private:
    HttpRequest _http_request;
    HttpResponse _http_response;

    int _new_socket;
    bool _stop;
};
class Callback
{
public:
    void ProcessOn(int new_socket)
    {
        EndPoint ep(new_socket);

        std::cout << "begin.........." << std::endl;
        ep.RecvRequest();
        if (ep.GetStop() == false)
        {
            ep.BuildResponse();
            ep.SendResponse();
        }

        std::cout << "end.........." << std::endl;

    }
};

4.读取请求

4.1.读取请求行 

读取函数:不同的环境下,http协议的结尾的区分可能不同,可能为'\n'、'\r\n'、'\r',需要统一为'\n',方便后序处理;下面使用了一个小技巧:recv的MSG_PEEK选项:窥探读,读取下一个字节,但是不从内核缓冲区拿出来,所以后序可以继续读这个字节;

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>

class Util
{
public:
    static bool RecvLine(int sock, std::string &buffer)
    {
        char ch;
        while (ch != '\n')
        {
            ssize_t s = recv(sock, &ch, 1, 0);
            if (s > 0)
            {
                if (ch == '\r')
                {
                    // 窥探下一个元素但是不拿取;
                    recv(sock, &ch, 1, MSG_PEEK);
                    if (ch == '\n') // 处理\r\n->\n
                    {
                        recv(sock, &ch, 1, 0);
                    }
                    else // 处理\r->\n
                    {
                        ch = '\n';
                    }
                }
                buffer.push_back(ch);
            }
            else
            {
                return false;
            }
        }
        return true;
    }
};

读取请求报行 ,保存在承装类中,LOG是一个日志;

    // 读取请求行
    bool RecvRequestLine()
    {
        if (Util::RecvLine(_new_socket, _http_request._request_line))
        {
            // 去除\n
            _http_request._request_line.pop_back();
            LOG(INFO, _http_request._request_line);
            return true;
        }
        else
        {
            return false;
        }
    }

4.2.读取请求报头

  • 去除\n",后序要把请求报头的每行报文的类型和数据做哈希表;
    // 读取请求报头
    bool RecvRequestHeader()
    {
        std::string tmp;
        while (tmp != "\n")
        {
            tmp.clear();
            if (Util::RecvLine(_new_socket, tmp))
            {
                if (tmp != "\n")
                {
                    // 去除\n
                    tmp.pop_back();
                    _http_request._request_header.push_back(tmp);
                    // LOG(INFO, tmp);
                }
            }
            else
            {
                return false;
            }
        }
        return true;
    }

4.3.分析请求行和报头

  • 请求行的方法、URI、版本放到承装容器;

  • 请求报文的类型和数据,做哈希表,例:“content-length: 100”,key:content-length   value:100;
    // 分析请求行
    void ParseRequestLine()
    {
        std::stringstream ss(_http_request._request_line);
        ss >> _http_request._method >> _http_request._uri >> _http_request._version;
        auto &method = _http_request._method;
        std::transform(method.begin(), method.end(), method.begin(), ::toupper); //::是全局作用域符
    }
    //分析请求报头
    void ParseRequestHeader()
    {
        for (int i = 0; i < _http_request._request_header.size(); i++)
        {
            std::string sub_out1;
            std::string sub_out2;
            if (Util::CutString(_http_request._request_header[i], ": ", sub_out1, sub_out2))
            {
                _http_request._um_header[sub_out1] = sub_out2;
            }
            else
                break;
        }
    }

4.4.读取正文

  • 根据请求报头是否有content-length来判断是否有正文;
    bool IsNeedRecvBody()
    {
        if (_http_request._method == "POST")
        {
            std::unordered_map<std::string, std::string>::iterator it = _http_request._um_header.find("Content-Length");
            _http_request._content_length = atoi(it->second.c_str());
            if (_http_request._content_length > 0)
            {
                return true;
            }
        }
        return false;
    }
    bool RecvRequestBody()
    {
        if (IsNeedRecvBody())
        {
            char ch;
            int num = _http_request._content_length;
            for (int i = 0; i < num; i++)
            {
                if (recv(_new_socket, &ch, 1, 0) > 0)
                {
                    _http_request._body.push_back(ch);
                    // LOG(INFO, _http_request._body);
                }
                else
                    return false;
            }
        }
        return true;
    }

5.构建响应

5.1.根据请求方法和是否带参来判断是否需要进行CGI处理:

  1. 如果不是GET和POST直接构建400(CLIENT_ERROR)响应;
  2. GET如果不带参就返回静态网页(读取对应文件的内容,再把内容发送给客户端);
  3. GET如果带参就需要进行CGI处理(GET使用URI传参,只能传递一些比较小的参数);
  4. POST需要进行CGI处理(传递任意大小的参数);
 void BuildResponse()
    {
        auto &method = _http_request._method;
        if (method != "GET" && method != "POST")
        {
            _http_response._stat_code = CLIENT_ERR;
            LOG(RISK, "method is not right");
            goto END;
        }
        if (method == "GET")
        {
            auto &uri = _http_request._uri;
            if (uri.find('?') != std::string::npos)
            {
                _http_response._cgi = true;
                Util::CutString(uri, "?", _http_request._url, _http_request._uri_argments);
            }
            else
                _http_request._url = uri;
        }
        else if (method == "POST")
        {
            _http_request._url = _http_request._uri;
            _http_response._cgi = true;
        }
        else
        {
            // do nothing
        }

 5.2.把URI处理合理

  • 访问的一定的是服务器想让你访问的资源,服务器会建立webroot目录(就是一个不同目录,webroot就是它的名字),存在这些可以访问的资源;
  • 让webroot+=请求路径,才是合理的路径;
  • 加后webroot/请求路径/,像这样格式就访问这个目录下的index.html(首页),webroot/请求路径/index.html
  • 使用stat来判断文件是否存在,顺便使用se.st_size来获取文件大小保存在承装类中
        // 不一定是根目录,把url处理为符合服务器的url
        _http_request._request_path = WEB_ROOT;
        _http_request._request_path += _http_request._url;
        if (_http_request._request_path[_http_request._request_path.size() - 1] == '/')
        {
            // 把"/"或者"a/b/c/"这种目录都处理为,访问目录下的index.html
            _http_request._request_path += HOME_PAGE;
        }
        // 判断路径是否存在
        struct stat st;
        // 等于0代表执行函数成功,也等于有这个文件
        if (stat(_http_request._request_path.c_str(), &st) == 0)
        {
            if (S_ISDIR(st.st_mode))
            {
                // 处理最后不是'/'的路径,例:/a/b/c
                _http_request._request_path += "/";
                _http_request._request_path += HOME_PAGE;
                stat(_http_request._request_path.c_str(), &st);
            }
            if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH))
            {
                // 特殊处理
                _http_request._cgi = true;
            }
            _http_request._size = st.st_size;
        }
        else
        {
            // 文件不存在
            LOG(RISK, _http_request._request_path);
            _http_response._stat_code = CLIENT_ERR;
            goto END;
        }
        // CGI处理
        if (_http_response._cgi)
        {
            _http_response._stat_code = ProcessCgi();
        }
        else
        {
            // 不需要CGI处理,返回静态网页
            _http_response._stat_code = ProcessNonCgi();
        }
    END:
        auto &code = _http_response._stat_code;
        // 构建状态行
        BuildStatusLine(code);
        // 构建响应报文
        switch (code)
        {
        case OK:
            HandlerOK();
            break;
        case NOT_FOUND:
            HandlerError(code);
            break;
        case CLIENT_ERR:
            HandlerError(code);
            break;
        case SERVER_ERR:
            HandlerError(code);
            break;
        default:
            break;
        }
        return;
    }

6.CGI处理

  1. CGI机制的原因:CGI机制:用户会访问web服务器的任意文件,服务器需要依靠参数对文件进行处理;不能把处理方法放在web服务器的代码下,因为用户只会访问它需要的,那么大量的处理方法是无意义的,而且内容太多了;
  2. CGI机制的本质:使用子进程进程替换,使用环境变量传递参数大小,匿名管道传递参数和执行结果;
  3. 匿名管道需要两个,因为匿名管道是单信道通信;
int ProcessCgi() //(重点)
    {
        int code = SERVER_ERR;
        auto &path = _http_request._request_path;
        auto &method = _http_request._method;
        auto &get_arguments = _http_request._uri_argments;
        auto &post_arguments = _http_request._body;
        auto &body = _http_response._body;

        // 把数据大小添加到环境变量内,返回被替换的子进程拿取
        std::string arguments_size;
        arguments_size += "ARGUMENTS_SIZE=";
        if (method == "GET")
            arguments_size += std::to_string(get_arguments.size());
        else if (method == "POST")
            arguments_size += std::to_string(post_arguments.size());
        putenv((char *)arguments_size.c_str());

        // 匿名管道是单信道通信,站在父进程的视角
        int output[2];
        int input[2];

        if (pipe(output) == -1)
        {
            LOG(ERROR, "pipe output[2] create fail");
            return SERVER_ERR;
        }
        if (pipe(input) == -1)
        {
            LOG(ERROR, "pipe input[2] create fail");
            return SERVER_ERR;
        }

        // 创建子进程后续用来进程替换
        pid_t pid = fork();
        if (pid == 0)
        {
            close(output[1]);
            close(input[0]);
            // 站在子进程的视角,进程替换数据被替换,提前使用dup2把:
            // output[0]->标准输入
            // input[1]->标准输出
            dup2(output[0], 0);
            dup2(input[1], 1);
            if (execl(path.c_str(), path.c_str(), nullptr) == -1)
            {
                std::cout << 2 << std::endl;
            }
            exit(0);
        }
        else if (pid > 0)
        {
            close(output[0]);
            close(input[1]);
            if (method == "GET")
            {
                write(output[1], get_arguments.c_str(), get_arguments.size());
            }
            if (method == "POST")
            {
                // post参数的大小可能很多,一次可能写不完
                int psize = post_arguments.size();
                int total = 0;
                ssize_t s = 0;
                while (total < psize && (s = write(output[1], post_arguments.c_str() + total, psize - total)) > 0)
                {
                    if (s <= 0)
                    {
                        LOG(ERROR, "write error");
                        break;
                    }
                    total += s;
                    // std::cout << psize << ":" << s << ":" << total << std::endl;
                }
            }
            // 读取cgi结果
            char ch;
            while (read(input[0], &ch, 1) > 0)
            {
                body.push_back(ch);
            }
            // std::cout<<body<<std::endl;
            int status;
            if (waitpid(pid, &status, 0) == pid)
            {
                if (WIFEXITED(status))
                {
                    if (WEXITSTATUS(status) == 0)
                        code = OK;
                    else
                        LOG(error, "error code discontinue");
                }
                else
                    LOG(error, "signal discontinue");
            }
            else
            {
                LOG(ERROR, "waitpid fail");
                return SERVER_ERR;
            }
            close(output[1]);
            close(input[0]);
        }
        else
        {
            // fork错误
            LOG(ERROR, "fork fail");
            return SERVER_ERR;
        }
        return code;
    }

我执行的文件是一个简单的加法; 

#include <iostream>
#include <unistd.h>
#include <string>
#include<cstdlib>
#include<cstdlib>

bool GetArguments(std::string &arguments)
{
    char ch;
    int size = atoi(getenv("ARGUMENTS_SIZE"));
    for (int i = 0; i < size; i++)
    {
        read(0, &ch, 1);
        arguments.push_back(ch);
    }
    if (arguments.size() == size)
        return true;
    else
        return false;
}
void CutArguments(const std::string &in, const std::string &op, std::string& arg1, std::string& arg2)
{
    int pos = in.find(op);
    arg1 = in.substr(0, pos);
    arg2 = in.substr(pos + op.size());
}
void CutArguments(const std::string &in, const std::string &op, std::string &name, int &value)
{
    int pos = in.find(op);
    name = in.substr(0, pos);
    value = atoi(in.substr(pos + op.size()).c_str());
}
bool PutCGIResult(int arg1, int arg2)
{
    std::string s;
    s+=std::to_string(arg1);
    s+="+";
    s+=std::to_string(arg2);
    s+="=";
    s+=std::to_string(arg1+arg2);
    if (write(1, s.c_str(), s.size()) < 0)
        return false;
    // if (write(1, arg2.c_str(), arg2.size()) < 0)
    //     return false;
    return true;
}
int main()
{
    std::cerr<<12<<std::endl;
    std::string arguments;
    if (GetArguments(arguments) == false)
        return 1;
    std::cerr<<arguments<<std::endl;
    // 处理参数
    std::string arg1, arg2;
    CutArguments(arguments, "&", arg1, arg2);

    std::string name1, name2;
    int value1, value2;
    CutArguments(arg1, "=", name1, value1);
    CutArguments(arg2, "=", name2, value2);

    // 返回结果
    if (PutCGIResult(value1, value2) == false)
        return 2;
    return 0;
}

7.发送响应

    void SendResponse()
    {
        if (send(_new_socket, _http_response._status_line.c_str(), _http_response._status_line.size(), 0) <= 0)
            return;
        std::cout << _http_response._status_line;
        for (auto &at : _http_response._status_header)
        {
            if (send(_new_socket, at.c_str(), at.size(), 0) <= 0)
                return;
            std::cout << at;
        }
        send(_new_socket, _http_response._blank.c_str(), _http_response._blank.size(), 0);
        std::cout << _http_response._blank;
        if (_http_response._cgi)
        {
            int size = _http_response._body.size();
            int total = 0;
            ssize_t s;
            std::cout << _http_response._body << std::endl;
            while (total < size && (s = send(_new_socket, _http_response._body.c_str() + total, size - total, 0)) > 0)
            {
                if (s <= 0)
                {
                    LOG(ERROR, "write error");
                    break;
                }
                total += s;
                // std::cout << size << ":" << s << ":" << total << std::endl;
            }
        }
        else
        {
            int fd = open(_http_request._request_path.c_str(), O_RDONLY);
            if (fd >= 0)
                sendfile(_new_socket, fd, 0, _http_request._size);
        }
    }

8.执行结果

这个项目注重理解的是 建立连接、获取和分析请求、对有参数的请求进行CGI机制处理(CGI机制:用户会访问web服务器的任意文件,服务器需要依靠参数对文件进行处理,不能把处理方法放在web服务器的代码下,因为用户只会访问它需要的,那么大量的处理方法是无意义的,而且内容太多了)、构建和发送响应的全过程;

9.源码链接

Linux/web_server at main · lijinggai/Linux · GitHub

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

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

相关文章

桌面图标删不掉?试试这几个解决办法!

案例&#xff1a;我想对电脑桌面上的应用进行删除&#xff0c;但是我怎么删也删不掉应用的图标&#xff1f;有人知道这是怎么回事吗&#xff1f;怎样才能成功删除桌面图标&#xff1f;求一个解决办法&#xff01; 有时候我们可能会遇到桌面图标无法删除的困扰&#xff0c;桌面…

【已解决】Macbook pro/Macbook air 电脑过热问题(附软件下载地址)

问题&#xff1a; 今天早上一上班打开我的macbook air&#xff0c;刚开机了十来分钟&#xff0c;就觉得左上角位置特别的热&#xff0c;耳朵凑近风扇处&#xff0c;基本听不到风扇的声音&#xff0c;风扇的转速太慢&#xff0c;导致cpu温度堆积造成温度升高。 解决办法&#…

【JS】1705- 重学 JavaScript API - Fullscreen API

❝ 前期回顾&#xff1a; 1. Page Visibility API 2. Broadcast Channel API 3. Beacon API 4. Resize Observer API 5. Clipboard API 6. Fetch API 7. Performance API 8. WebStorage API 9. WebSockets API ❞ 本文中&#xff0c;我们将探索 Fullscreen API 的概念、使用方法…

SpringBoot+Vue 的简历招聘系统

文章目录 1、效果演示2、 前言介绍3、主要技术4 **系统设计**4.1 系统体系结构4.2开发流程设计4.3 数据库设计原则4.4 数据表 5 **系统详细设计**5.1管理员功能模块5.2用户功能模块5.3前台首页功能模块 6、源码获取 1、效果演示 2、 前言介绍 随着科学技术的飞速发展&#xff…

加速5G部署,到底该怎么做?

今天&#xff0c;第31届中国国际信息通信展&#xff08;PT展&#xff09;在北京国家会议中心圆满落幕。 这次通信展&#xff0c;在举办日期上有着特殊的意义。因为&#xff0c;今年的6月6日&#xff0c;正好是国内5G牌照正式发放的四周年纪念日。而且&#xff0c;去年大概这个时…

DETR模型转RKNN

目录 1.前言 2.准备工作 3.开始转模型 4.测试代码 5.不想转&#xff0c;直接用也可以&#xff0c;转好的给你&#xff0c;请关注评论一下 1.前言 RKNN出最新版本了&#xff0c;测试了一下&#xff0c;rk在transformer方面做了很多的工作&#xff0c;至少之前不能转的模型&am…

ReadProcessMemory可不是一个进程间通信的好方法

有时候我看到有人会使用 ReadProcessMemory 这个 API 来实现进程间通信&#xff0c;老实说吧&#xff0c;我觉得这不是一个明智的选择&#xff0c;原因有如下几条。 首先&#xff0c;你不能使用 ReadProcessMemory 来跨越安全上下文 (Security Contexts)&#xff0c;至少你需要…

SpringCloud入门实战(八)- Gateway服务网关集成

&#x1f4dd; 学技术、更要掌握学习的方法&#xff0c;一起学习&#xff0c;让进步发生 &#x1f469;&#x1f3fb; 作者&#xff1a;一只IT攻城狮 &#xff0c;关注我&#xff0c;不迷路 。 &#x1f490;学习建议&#xff1a;1、养成习惯&#xff0c;学习java的任何一个技术…

macOS Sonoma 14.0 (23A5257q) Beta1 带 OC 引导双分区黑苹果镜像

6月6日&#xff0c;在WWDC2023开发者大会上&#xff0c;苹果带来了全新Mac系统&#xff0c;命名为macOS Sonoma。该系统最大的亮点是带来了小组件&#xff0c;macOS Sonoma可以添加手机上的所有小组件&#xff0c;包括车辆小组件。 镜像下载&#xff1a; 微信公众号&#xff1…

聚观早报 | 苹果发XR头显Vision Pro;英特尔将出售部分Mobileye股票

今日要闻&#xff1a;苹果发XR头显Vision Pro&#xff1b;英特尔将出售部分Mobileye股票&#xff1b;华为已申请注册两枚NETGPT&#xff1b;瑞幸咖啡全国门店数量突破1万家&#xff1b;iPhone15系列本月将在郑州富士康量产 苹果发XR头显Vision Pro 6 月 6 日&#xff0c;苹果 …

从零手写操作系统之RVOS环境搭建-01

从零手写操作系统之RVOS环境搭建-01 背景介绍操作系统的定义操作系统的分类典型的 RTOS 介绍课程系统RVOS简介 Hello WorldQEMU介绍QEMU-virt 地址映射 系统引导引导程序要做哪些事情如何判断当前hart是不是第一个hart?如何初始化栈? 如何在屏幕输出Hello World通过串口输出U…

基于java SpringBoot框架和Vue的智能停车场管理系统

近年来&#xff0c;中国不仅综合国力大幅提升&#xff0c;国民经济也快速增长&#xff0c;推动了中国汽车工业的发展。技术的飞速发展逐渐降低了汽车的制造成本&#xff0c;越来越受欢迎。今天&#xff0c;大多数家庭都有能力购买汽车&#xff0c;因此&#xff0c;中国城市的汽…

R语言手动绘制连续线条的校准曲线(Calibration curve)(4)

校准曲线图表示的是预测值和实际值的差距&#xff0c;作为预测模型的重要部分&#xff0c;目前很多函数能绘制校准曲线。 一般分为两种&#xff0c;一种是通过Hosmer-Lemeshow检验&#xff0c;把P值分为10等分&#xff0c;求出每等分的预测值和实际值的差距。 我们既往已经通…

基于组件化开发思想的微信小程序开发框架

跨端框架的出现为小程序应用的开发带来了巨大的便利性和灵活性。它们提供了统一的开发方式、代码复用的能力&#xff0c;并且与小程序容器技术紧密结合&#xff0c;实现了一次编码、多端运行的目标。开发者可以根据项目需求和团队技术栈选择合适的跨端框架&#xff0c;从而在不…

【大数据工具】Spark 伪分布式、分布式集群搭建

Spark 集群搭建 Spark 安装包下载地址&#xff1a;https://archive.apache.org/dist/spark/ 1. Spark 伪分布式安装 安装前提&#xff1a;安装 Spark 前需要先安装好 JDK 1. 上传并解压 Spark 安装包 使用 fileZilla 或其他文件传输工具上传 Spark 安装包&#xff1a;spar…

简单易懂的 nvm 和 Node.js 版本控制指南

NVM是Node.js的版本管理工具&#xff0c;可以方便地在不同版本的Node.js之间切换。它可以通过命令行或者脚本来管理Node.js的版本&#xff0c;支持在同一台机器上安装多个版本的Node.js&#xff0c;并能够方便地切换它们。 NVM的主要功能包括&#xff1a; 安装和卸载Node.js的不…

2022年国赛高教杯数学建模A题波浪能最大输出功率设计解题全过程文档及程序

2022年国赛高教杯数学建模 A题 波浪能最大输出功率设计 原题再现 随着经济和社会的发展&#xff0c;人类面临能源需求和环境污染的双重挑战&#xff0c;发展可再生能源产业已成为世界各国的共识。波浪能作为一种重要的海洋可再生能源&#xff0c;分布广泛&#xff0c;储量丰富…

DevExpress WinForms v23.1新功能抢先看——支持系统强调色更改

DevExpress WinForm 下一个主要版本&#xff08;v23.1&#xff09;将在6月份左右发布&#xff0c;本文将为大家介绍在早期访问预览版&#xff08;EAP&#xff09;中包含的新功能。 PS&#xff1a;DevExpress WinForm拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具…

5月琐碎但值得的事情

转眼间时间就来到了6月份&#xff0c;又该写5月的思考总结了&#xff0c;依然记录一些5月份发生的小事或者收获&#xff0c; 这些内容本意给我记录生活的&#xff0c;如果对你有一些帮助就更好了。 往期&#xff1a; 1月的碎碎念&#xff0c;但是很有必要 二月的一些琐事&#…

chatgpt赋能python:Python如何阻止弹窗

Python如何阻止弹窗 Python是一种高级编程语言&#xff0c;它具有广泛的应用和丰富的库。它还可以被用于开发自动化程序&#xff0c;包括阻止弹窗。在本文中&#xff0c;我们将介绍如何使用Python阻止弹出窗口&#xff0c;并探讨防止弹窗的原因。 为什么要防止弹窗&#xff1…