HTTP服务器

news2024/11/16 21:31:00

HTTP服务器

1. 项目背景和技术特点

实现目的

从移动端到浏览器,HTTP 协议无疑是打开互联网应用窗口的重要协议,其在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。

完善对HTTP协议的理论学习,从零开始完成WEB服务器开发。

采用 CS 模型,编写支持中小型应用的HTTP服务器,井结合MySQL。

该项目可以帮助我们从技术上理解上网输入网站URL到关闭浏览器到所有操作中的技术细节。

HTTP基础知识,这里就不再赘述了

技术特点

  • Linux 网络编程 TCP/IP协议、socket流式套接、http协议。
  • 多线程技术
  • cgi 技术
  • shell 脚本
  • 线程池

开发环境

  • centos7
  • vim gcc/g++ Makefile

 

2. 代码结构和实现思路

TcpServer

先实现HTTP的底层协议TCP的代码,也就是完成基本的网络套接字代码。实现成单例模式以供上层调用。

//TcpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <fcntl.h>

#define BACK_LOG 5

class TcpServer
{
private:
    int _port;
    int _listen_sock;

    TcpServer(int port) : _port(port), _listen_sock(-1)
    {}
    TcpServer(const TcpServer& ts) = delete;
    TcpServer operator=(const TcpServer& ts) = delete;

    static TcpServer* _svr;
public:

    static TcpServer* GetInstance(int port);
    void InitServer();

    void Socket();
    void Bind();
    void Listen();
    ~TcpServer()
    {}
};
TcpServer* TcpServer::_svr = nullptr;

HttpServer

HttpServer类实现调用TcpServer单例,并进入事件循环。

#pragma once
#include <iostream>
#include <pthread.h>
#include "TcpServer.hpp"
#include "Protocol.hpp"

#define PORT 8080

class HttpServer
{
private:
    int _port;
    TcpServer* _tcp_svr;
    bool _stop;

public:
    HttpServer(int port = PORT) : _port(port), _tcp_svr(nullptr), _stop(false)
    {}

    void InitServer() {
        _tcp_svr = TcpServer::GetInstance(_port);
    }

    void Loop();
    
    ~HttpServer()
    {}
};

接下来就是,读取请求,分析请求,构建响应并返回。

Protocol

定制HTTP请求和响应的协议。接着就是EndPoint类实现HTTP读取解析响应等一系列函数。

struct HttpRequest
{
    std::string _request_line;
    std::vector<std::string> _request_headers;
    std::string _blank;
    std::string _request_body;

    std::string _method;
    std::string _uri;
    std::string _version;

    std::string _path;
    std::string _query;
    struct stat _resoucre_stat;
    bool _cgi = false;

    std::unordered_map<std::string, std::string> _header_kvs;
};

struct HttpResponse
{
    std::string _status_line;
    std::vector<std::string> _response_headers;
    std::string _blank = LINE_BREAK;
    std::string response_body;

    int _status_code = OK; 
    int _resource_fd = -1;
};
class EndPoint
{
private:
    int _sock;
    HttpRequest _http_request;
    HttpResponse _http_response;

private:
    void RecvHttpRequestLine();
    void RecvHttpRequestHeadler();

    void ParseHttpRequestLine();
    void ParseHttpRequestHeadler();
    void RecvParseHttpRequestBody();

	int ProcessCgi();
	int ProcessWebPage();

public:
    EndPoint(int sock) : _sock(sock) {}

    void RecvHttpRequest()
    {
        RecvHttpRequestLine();
        RecvHttpRequestHeadler();
    }
    void ParseHttpRequest()
    {
        ParseHttpRequestLine();
        ParseHttpRequestHeadler();
        RecvParseHttpRequestBody();
    }
	void BuildHttpResponse();
    void SendHttpResponse();

    ~EndPoint()
    {}
};

读取和解析请求

当前请求时,我们以行为单位。

从各大平台发来的数据的行分隔符各有不同,我们要做的是兼容所有情况,也就是我们要自行实现读取一行数据的接口。

void EndPoint::RecvHttpRequestLine()
{
    std::string& line = _http_request._request_line;
    Util::ReadLine(_sock, &line);
    line.resize(line.size() - 1);
}
void EndPoint::RecvHttpRequestHeadler()
{
    std::string line;
    while (true) // 读到空行,读取结束
    {
        Util::ReadLine(_sock, &line);
        if (line == "\n") {
            _http_request._blank = line; // 将空行放到blank中
            break;
        }
        line.resize(line.size() - 1);
        _http_request._request_headers.push_back(std::move(line));
        line.clear();
    }
}

void EndPoint::ParseHttpRequestLine()
{
    std::stringstream ss(_http_request._request_line);
    ss >> _http_request._method >> _http_request._uri >> _http_request._version;
}
void EndPoint::ParseHttpRequestHeadler()
{
    for (auto& header : _http_request._request_headers)
        _http_request._header_kvs.insert(Util::GetKV(header, ": "));
}
void EndPoint::RecvParseHttpRequestBody()
{
    auto& method = _http_request._method;
    auto& headers_map = _http_request._header_kvs;
    auto& body = _http_request._request_body;
    auto iter = headers_map.find("Content-Length");
    if (iter == headers_map.end())
        return;
    else
        _http_request._content_length = stoi(iter->second);
    Util::ReadLine(_sock, &body);
    body.resize(body.size() - 1);
}

构建和返回响应

如果是GET方法获取资源路径,并进行一系列的检查判断。根据请求资源的类型设置CGI处理,如果是POST方法直接设置CGI。

void EndPoint::BuildHttpResponse()
{
    auto& code = _http_response._status_code;
    auto& path = _http_request._path;
    auto& rsrc_st = _http_request._resoucre_stat;

    // 排除非法请求
    if (_http_request._method != "GET" && _http_request._method != "POST") {
        LOG(WARNING) << "bad request invaild method\n";
        code = BAD_REQUEST;
        goto END;
    }

    if (_http_request._method == "GET") {
        size_t pos = _http_request._uri.find('?');
        if (pos != std::string::npos) {
            Util::CutString(_http_request._uri, &path, &_http_request._query, pos);
            _http_request._cgi = true; // 带参一定用cgi
        }
        else 
            path = _http_request._uri;

        // 检查资源路径
        path = WEB_ROOT + path; // 拼接web根目录前缀
        if (path.back() == '/')
            path += HOME_PAGE; // 拼接默认访问资源后缀

        //判断资源路径是否合法
        if (stat(path.c_str(), &rsrc_st) == 0) {
            if (S_ISDIR(rsrc_st.st_mode)) {
                path += "/" + HOME_PAGE;
                stat(path.c_str(), &rsrc_st);
            }
            if (rsrc_st.st_mode & S_IXUSR ||
                rsrc_st.st_mode & S_IXGRP ||
                rsrc_st.st_mode & S_IXOTH)
                _http_request._cgi = true;
        }
        else {
            LOG(WARNING) << "require " << path + " resource not found\n";
            code = NOT_FOUND;
            goto END;
        }
    }
    else if (_http_request._method == "POST") {
        _http_request._cgi = true;
        path = WEB_ROOT + _http_request._uri;
    }
    else {}

    // 处理请求
    if (_http_request._cgi == true) {
        code = ProcessCgi();     // 执行cgi请求,程序运行结果放到response_body中
        if (code == OK)
            LOG(INFO) << "cgi process executed success\n";
        code = ProcessWebPage(); // 讲cgi结果构建网页返回
    }
    else {
        code = ProcessWebPage(); // 返回静态网页
        if (code == OK)
            LOG(INFO) << "send " + path + " success\n";
    }
}

发送响应就是简单的将构建好的响应返回给对端即可。

void EndPoint::SendHttpResponse()
{
    send(_sock, _http_response._status_line.c_str(), _http_response._status_line.size(), 0);
    for (auto& header : _http_response._response_headers) {
        header += LINE_BREAK;
        send(_sock, header.c_str(), header.size(), 0);
    }
    send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0);
    if (_http_request._cgi) {
        auto& body = _http_response.response_body;
        int total = 0;
        while (total < body.size()) {
            ssize_t s = send(_sock, body.c_str() + total, body.size() - total, 0);
            if (s == 0) break;
            total += s;
        }
    }
    else {
        sendfile(_sock, _http_response._resource_fd, 0, _http_request._resrc_stat.st_size);
        close(_http_response._resource_fd);
    }

    LOG(INFO) << "send http response success\n";
}

处理静态和非静态请求

构建普通网页响应。

int ProcessNonCgi()
{
    _http_response._resource_fd = open(_http_request._path.c_str(), O_RDONLY); 
    if (_http_response._resource_fd < 0)
        return NOT_FOUND;
    
    auto& line = _http_response._status_line;
    auto& code = _http_response._status_code;
    line = HTTP_VERSION + " " + std::to_string(code) + " " + Util::Code2Desc(code) + LINE_BREAK;
    
    auto& stat = _http_request._resoucre_stat;
    auto& path = _http_request._path;
    std::string content_type_header = "Content-Type: " + Util::Suffix2Type(GetSuffix(path));
    std::string content_length_header = "Content-Length: " + std::to_string(stat.st_size);
    std::string content_language_header = "Content-Language: zh-cn";
    
    _http_response._response_headers.push_back(content_type_header);
    _http_response._response_headers.push_back(content_length_header);
    _http_response._response_headers.push_back(content_language_header);

    return OK;
}

构建CGI响应。这是本项目的重难点。

线程首先首先创建子进程,将具体执行进程程序替换的任务交给子进程。

其次定制父子进程通信协议。

请求方法,GET方法的请求参数,报头中的正文大小几个变量都用环境变量导给子进程。POST方法的请求体使用管道导给子进程。

中个细节代码中有注释说明。

int ProcessCgi()
{
    auto& method = _http_request._method;
    auto& body = _http_request._request_body;
    auto& path = _http_request._path;
    auto& code = _http_response._status_code;

    // 构建两个管道,一个是父写子读,一个是父读子写,管道从父进程角度命名
    // parent output[1]  -->  output[0] child
    // parent  input[0]  <--  input[1]  child
    int input[2];  // 父读子写
    int output[2]; // 父写子读
    if (pipe(input) < 0 || pipe(output) < 0)
        return SVR_ERROR;

    pid_t pid = fork();
    if (pid == 0) /* child */ {
        close(input[0]);
        close(output[1]);

        int ret = setenv("METHOD", method.c_str(), 1); // 先导请求方法
        if (method == "GET") // 再导GET请求参数
            ret |= setenv("QUERY", _http_request._query.c_str(), 0); 
        else if (method == "POST") // 再导正文大小
            ret |= setenv("CONTENT_LENGTH", std::to_string(body.size()).c_str(), 0); 

        if (ret < 0)
            LOG(WARNING) << "set env failed, errno: " << errno << '\n';

        dup2(input[1], 1);
        dup2(output[0], 0);

        execl(_http_request._path.c_str(), _http_request._path.c_str(), nullptr);
        code = SVR_ERROR;
        exit(1);
    }
    else if (pid > 0) /* parent */ {
        close(input[1]);
        close(output[0]);

        if (_http_request._method == "POST") {
            auto& body = _http_request._request_body;
            int already = 0;
            while (already < body.size()) {
                ssize_t s = write(output[1], body.c_str() + already, body.size() - already);
                if (s == 0) break;
                already += s;
            }
        }

        int status = 0;
        pid_t ret = waitpid(pid, &status, 0);
        if (ret < 0)
            LOG(ERROR) << "parent process wait failed\n";
            code = SVR_ERROR;
        }
        close(input[0]);
        close(output[1]);
        return code;
    }
    else { /* pid < 0 */ 
        LOG(ERROR) << "failed to create child process\n";
        return SVR_ERROR;
    }

    return code;
}

差错处理

在读取请求构建响应发送响应的过程中,都穿插着错误判断,并以HTTP响应状态码作为返回值。

在适当的地方goto END;,直接进行错误处理。错误处理利用得到的状态码构建错误响应,也就是返回错误网页,如404页面。

void BuildHttpResponse()
{
    // 排除非法请求
    if (_http_request._method != "GET" && _http_request._method != "POST")
    {
        LOG(WARNING) << "bad request invaild method\n";
        code = BAD_REQUEST;
        goto END;
    }
    
	//...
    
    // 差错处理
    END:
    if (code != OK)
    {
        LOG(INFO) << "headler error begin, code: " << code << '\n';
        ErrorHelper(); // 构建错误响应
    }
}

private:
void ErrorHelper()
{
    _http_request._cgi = false; // 错误处理,返回静态网页
    auto& code = _http_response._status_code;

    switch (code)
    {
        case BAD_REQUEST:
            HeadlerWrong(PAGE_404);
            break;
        case NOT_FOUND:
            HeadlerWrong(PAGE_404); // 单独构建404页面
            break;
        case SVR_ERROR:
            HeadlerWrong(PAGE_404);
            break;
        case SVR_UNAVL:
            HeadlerWrong(PAGE_404);
            break;
        default:
            LOG(WARNING) << "unkown error code" << std::endl;
            break;
    }
}

void HeadlerWrong(const std::string& wrong_page)
{
    _http_request._path = WEB_ROOT + '/' + wrong_page;
    stat(_http_request._path.c_str(), &_http_request._resoucre_stat);
    ProcessWebPage(); // 返回404页面
}

int ProcessCgi()
{
 	if (pid == 0) {
        //...
    }
    else if (pid > 0) { /* parent */
        // 获取子进程退出结果
        int status = 0;
        pid_t ret = waitpid(pid, &status, 0);
        if (ret == pid)
        {
            // 管道读取子进程输出
            char ch = 0;
            while (read(input[0], &ch, 1))
                _http_response.response_body.push_back(ch); // 子进程输出放到响应体中

            //判断进程是否正常终止
            if (WIFEXITED(status)) {
                LOG(INFO) << "subprocess exited exit code: " << WEXITSTATUS(status) << '\n';
                if (WEXITSTATUS(status) != 0)
                    code = BAD_REQUEST;
            } else {
                LOG(INFO) << "subprocess exited by signal: " << WIFEXITED(status) << '\n';
                code = BAD_REQUEST;
            }
        }
        else {
            LOG(ERROR) << "parent process wait failed\n";
            code = SVR_ERROR;
        }
        //...

        return code;
    }
    else /* pid < 0 */ {
        LOG(ERROR) << "failed to create child process\n";
        return SVR_ERROR;
    }

    return code;
}

引入线程池

任务队列中的任务类,设置回调方法,使任务体能够自行调用处理任务的函数。

class Task
{
private:
    int _sock;
    CallBack HandlerTask; // 设置回调 当队列中有任务时,调用回调让后端处理任务

public:
    Task() {}
    Task(int sock) : _sock(sock) {}
    ~Task() {}

    void ProcessTask() {
        HandlerTask(_sock);
    }
};

HeaderRequest方法,构建成回调CallBack仿函数。

class CallBack
{
public:
    CallBack() {}
    ~CallBack() {}

public:
    void operator()(int sock) {
        EndPoint* ep = new EndPoint(sock);
        if (ep->RecvHttpRequest() && ep->ParseHttpRequest()) {
            ep->BuildHttpResponse();
            ep->SendHttpResponse();
        }
        else 
            LOG(WARNING) << "recv http request failed\n";

        delete ep;
    }
};

设置线程池,并配备任务队列。

交给外部HTTPServer类向任务队列中添加accept接受到的任务。

自身设置TASK_NUM数量的线程来同步互斥地获取任务队列中的任务。

class ThreadPool
{
private:
    std::queue<Task> _task_queue;
    int _task_num;
    bool _stop;

    pthread_mutex_t _mtx;
    pthread_cond_t _cond;
    static ThreadPool* _thread_pool;

private:
    ThreadPool() = default;
    ThreadPool(int num = TASK_NUM) : _task_num(num), _stop(false) {
        pthread_mutex_init(&_mtx, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    ThreadPool(const ThreadPool&) = delete;

public:
    static ThreadPool* GetInstance() {
        pthread_mutex_t lck = PTHREAD_MUTEX_INITIALIZER;
        if (_thread_pool == nullptr) {
            pthread_mutex_lock(&lck);
            if (_thread_pool == nullptr) {
                _thread_pool = new ThreadPool(TASK_NUM);
                _thread_pool->InitThreadPool();
            }
            pthread_mutex_unlock(&lck);
        }
        return _thread_pool;
    }

    void InitThreadPool() {
        for (int i = 0; i < _task_num; ++i) {
            pthread_t tid;
            if (pthread_create(&tid, nullptr, ThreadRoutine, (void*)_thread_pool) != 0) {
                LOG(FATAL) << "create pthread failed\n";
                exit(1);
            }
        }
        LOG(INFO) << "thread pool init success\n";
    }

    bool isEmpty() {
        return _task_queue.empty();
    }
    void Lock() {
        pthread_mutex_lock(&_mtx);
    }
    void Unlock() {
        pthread_mutex_unlock(&_mtx);
    }
    void ThreadWait() {
        pthread_cond_wait(&_cond, &_mtx);
    }
    void ThreadWakeup() {
        pthread_cond_signal(&_cond);
    }

    static void* ThreadRoutine(void* args) {
        ThreadPool* tp = (ThreadPool*)args;
        while (!tp->_stop) {
            tp->Lock();

            while (tp->isEmpty())
                tp->ThreadWait();

            Task task;
            tp->PopTask(&task);

            tp->Unlock();
            task.ProcessTask();
        }
    }

    void PushTask(const Task& task) {
        Lock();

        _task_queue.push(task);

        Unlock();
        ThreadWakeup();
    }

    void PopTask(Task* task) {
        *task = _task_queue.front();
        _task_queue.pop();
    }

    ~ThreadPool() {
        pthread_mutex_destroy(&_mtx);
        pthread_cond_destroy(&_cond);
    }
};
ThreadPool* ThreadPool::_thread_pool = nullptr;
class HttpServer
{
private:
    int _port;
    TcpServer* _tcp_svr;
    ThreadPool* _thread_pool;
    bool _stop;

public:
    HttpServer(int port = PORT) : _port(port), _tcp_svr(nullptr), _stop(false) {}
    ~HttpServer() {}
    
    void InitServer() {
        signal(SIGPIPE, SIG_IGN); // 忽略SIGPIPE信号
        _tcp_svr = TcpServer::GetInstance(_port);
        _thread_pool = ThreadPool::GetInstance();
    }

    void Loop() {
        while (!_stop) {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sock = accept(_tcp_svr->GetListenSock(), (struct sockaddr*)&peer, &len);
            if (sock < 0) continue;

            Task task(sock);
            _thread_pool->PushTask(task);
        }
    }
};

调用逻辑

httpserver调用逻辑

 

3. 难点总结和项目扩展

对于CGI机制的理解和实现。

HTTP服务流程

整个HTTP服务就是CGI程序和客户端沟通的桥梁,因为CGI程序与外界的输入输出都由HTTP服务器代理和转发。

通过子进程进程程序替换的方式,能够调用任意程序,且可以获得程序的运行结果和控制其输入输出。

项目扩展

  • URL encode decode
  • 数据库增删查改
  • HTTP其他方法
  • 配置文件化
  • 301302转发

业务层面

  • 实现在线计算器(日期转换等)
  • 实现在线简历
  • 实现博客系统

技术层面

  • 支持HTTP1.1长连接(链接管理)
  • 提高并发量和执行效率
  • 支持redis
  • 支持多机器业务转发负载均衡的代理功能

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

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

相关文章

万字string类总结

目录 一、string类的介绍 二、string类的常用接口 1、构造函数 2. string类对象的容量操作 3. string类对象的访问及遍历操作 4. string类对象的修改操作 &#xff08;重点&#xff09; 5. string类非成员函数 6. vs和g下string结构的说明 三、string类的模拟 1. 浅拷…

c++智能指针(raii)

目录 1.智能指针的作用 2.智能指针带来的问题与挑战 3.三种不同的智能指针 4.auto_ptr 5.unique_ptr 6.shared_ptr 7.weak_ptr&#xff1b;相互引用 8.总结 1.智能指针的作用 以c的异常处理为例看看throw catch用法。有时&#xff0c;一个用new开出的空间用完还没delete…

[附源码]java毕业设计壹家吃货店网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【C语言】学数据结构前必学的结构体struct详细

佛祖说&#xff0c;他可以满足程序猿一个愿望。程序猿许愿有生之年写出一个没有bug的程序&#xff0c;然后他得到了永生。 目录 1、结构体的声明与定义 1.1结构体是什么&#xff1f; 1.2为什么要有结构&#xff1f; 1.3结构体的声明 1.4结构体成员类型 1.5结构体变量定义…

由CPU高负载引发内核探索之旅

导语&#xff1a;STGW&#xff08;腾讯云CLB&#xff09;在腾讯云和自研业务中承担多种网络协议接入、请求加速、流量转发等功能&#xff0c;有着业务数量庞大、接入形式多样、流量规模巨大的特点&#xff0c;给产研团队带来了各种挑战&#xff0c;经常要深入剖析各种疑难杂症。…

Win7纯净版系统镜像64位介绍

Win7系统是一款非常经典的系统&#xff0c;这里想为大家介绍的是Win7纯净版系统镜像64位&#xff0c;主要特点就是非常稳定&#xff0c;运行流畅、占用CPU和内存都非常少。系统非常纯净&#xff0c;使用此系统&#xff0c;可让你的电脑焕然一新&#xff01; 一、系统稳定 1、采…

科普读书会丨《被讨厌的勇气》:愤怒不是目的,是一种工具

Hello&#xff0c; 这里是壹脑云读书圈&#xff0c;我是领读人小美~ 《被讨厌的勇气》读书会目前已经进行了两期&#xff0c;成员们也共同探讨了其中第一夜和第二夜的内容。每个人都有被情绪困扰的时候&#xff0c;而阿德勒心理学告诉我们&#xff0c;即使是负面情绪也不可怕…

WebRTC 服务器搭建篇

First off All 服务器环境&#xff1a;采用的阿里云国内服务器&#xff0c;系统&#xff1a; Ubuntu 16.04 64位 。 各个服务所需要的编译环境图&#xff1a; 各个服务器对应所需编译平台 1.第一步&#xff0c;先更新下命令行工具&#xff0c;工欲善其身必先利其器&#xff…

推荐一款图表功能强大的可视化报表工具

企业信息化建设&#xff0c;大量的数据需要经过分析才能挖掘价值。因此数据的价值越来越受到大家的重视&#xff0c;大数据分析工具逐渐成为企业运营必不可少的辅助工具。俗话说工人要想做好事&#xff0c;首先要磨利工具&#xff0c;拥有一个好用的大数据分析工具尤为重要&…

numpy生成0和1数组方法、从已有数组生成新数组方法、生成固定范围内数组、生成随机数组、绘制指定均值和标准差正态分布图、均匀分布图绘制

一、生成0和1数组 np.ones(shape, dtype)&#xff1a;shape为要生成的数组的维度&#xff0c;dtype为数组内元素类型np.ones_like(a, dtype)&#xff1a;生成与a同维度的数组np.zeros(shape, dtype)np.zeros_like(a, dtype) 代码如下 one np.ones([3,4]) one --------------…

机器学习——支持向量机与集成学习

支持向量机与集成学习 文章目录支持向量机与集成学习支持向量机的基本原理线性可分支持向量常用核函数集成学习概述集成学习的两种方式集成学习的基本类型弱学习其合成方式AdaBoost算法训练过程简例一类按监督学习方式对数据进行二元分类的广义线性分类器 文章目录支持向量机与…

免费不限时长的语音转文字软件——Word365

适用场景 想将语音转化成文字。 这里的语音可以是实时输入&#xff0c;也可以是已有音、视频转换成文字。 后者的操作比前者多一步操作。 1.实时语音转文字 可以直接打开Word365&#xff0c;【开始】选项卡中的【听写】功能。 打开前修改一下设置&#xff0c;语言可以根据需…

nmap之nse脚本简单学习

nmap之nse脚本简单学习 环境&#xff1a;centos8 nmap安装 yum -y install nmap -- 版本 [rootqingchen /]# nmap -version Nmap version 7.70 ( https://nmap.org )脚本入门学习 cd /usr/share/nmap [rootqingchen nmap]# ls nmap.dtd nmap-mac-prefixes nmap-os-db …

300dpi等于多少分辨率?如何给图片修改分辨率大小?

​图片是我们在生活中经常需要接触使用到的东西&#xff0c;无论是工作中还是生活中都离不开图片&#xff0c;在使用图片时我们会接触到“图片分辨率”、“dpi”这个概念&#xff0c;那么到底什么是图片分辨率&#xff1f;300DPI等于多少分辨率&#xff1f;如何给图片修改分辨率…

Lidar和IMU(INS)外参标定----常用开源项目总结

写在前面&#xff1a;博主主要关注的是自动驾驶中Lidar和RTK组合导航设备的标定&#xff0c;大部分的开源项目都把其转化为Lidar和IMU的标定过程。 1. ETH的lidar_align (Github)A simple method for finding the extrinsic calibration between a 3D lidar and a 6-dof pose …

推特精准客户开发手册

你要在巷子里营造出热闹的气氛&#xff0c;人为把热度炒起来&#xff0c;虚假的繁荣是做给别人看的&#xff0c;是用来吸引别人而不是说你自己沉迷于此&#xff0c;而“虚假的繁荣”又是个怎么的虚法呢&#xff1f;它需要外界看起来是真的。 可是问题来了&#xff0c;我们都知…

NTP时钟系统为制造业信息化产业提供守时保障

随着科学技术的发展&#xff0c;工业信息化高速迈进&#xff0c;高精度的同步时钟系统显得尤为重要。利用网络同步时钟系统技术对各个设备之间进行时间统一&#xff0c;对制造业和信息化产业提高产能&#xff0c;让生产力更高效提供守时保障。NTP时钟系统是基于网络时间协议而衍…

你问我答 | 解决关于入托的8个疑问

很多新手家长对于送孩子入托有很多顾虑&#xff0c;这次我们通过“你问我答”让家长更了解托班的意义。 Q&#xff1a;不好好吃饭的小宝宝&#xff0c;适合入托吗&#xff1f; A&#xff1a;适合。吃饭是孩子生活能力培养的重要部分&#xff0c;大部分孩子在入托前&#xff0c…

C. Binary String(思维+贪心)

Problem - 1680C - Codeforces 给你一个由字符0和/或1组成的字符串s。 你必须从字符串的开头去除几个&#xff08;可能是零&#xff09;字符&#xff0c;然后从字符串的结尾去除几个&#xff08;可能是零&#xff09;字符。移除后&#xff0c;字符串可能会变成空的。删除的代价…

【跟学C++】C++STL标准模板库——算法详细整理(中)(Study18)

文章目录1、简介2、STL算法分类及常用函数2.1、变序算法(一)2.2.1 初始化算法(2个)2.2.2 修改算法(2个)2.2.3 复制算法(6个)2.2.4 删除算法(6个)3、总结 【说明】 大家好&#xff0c;本专栏主要是跟学C内容&#xff0c;自己学习了这位博主【 AI菌】的【C21天养成计划】&#x…