Linux高性能服务器编程 学习笔记 第十四章 进程池和线程池

news2024/11/20 4:31:30

动态创建子进程或子线程的缺点:
1.动态创建进程或线程比较耗时,这将导致较慢的客户响应。

2.动态创建的子进程或子线程通常只用来为一个客户服务(除非我们做特殊处理),这将导致系统上产生大量的进程或线程,进程或线程间的切换将消耗大量CPU时间。

3.动态创建的子进程是当前进程的完整映像,当前进程必须谨慎地管理其分配的文件描述符和堆内存等系统资源,否则子进程会复制这些资源,从而使系统的可用资源急剧下降,进而影响服务器性能。

进程池和线程池相似,我们以进程池为例进行介绍,下面对进程池的讨论也适用于线程池。

进程池是由服务器预先创建的一组子进程。线程池中的线程数量应该和CPU数量差不多,防止高负载下有CPU核心未被使用。

进程池中的所有子进程都运行着相同的代码,并具有相同的属性,如优先级、PGID等,因为进程池在服务器启动之初时就创建好了,所以每个子进程都相对干净,即它们没有打开不必要的文件描述符(从父进程继承而来),也不会错误地使用大块的堆内存(从父进程复制得到)。

当有新任务与到来时,主进程将通过某种方式选择进程池中某一子进程来为之服务,相比动态创建子进程,选择一个已经存在的子进程的代价小很多,主进程选择哪个子进程来为新任务服务主要有两种方式:
1.主进程使用某种算法主动选择子进程。最简单、最常用的算法是随机算法和Round Robin(轮流选取)算法,但更优秀、更智能的算法将使任务在各个工作进程中更均匀地分配,从而减轻服务器的整体压力。

2.主进程和所有子进程通过一个共享的工作队列来同步,子进程都睡眠在该工作队列上,当有新任务到来时,主进程将任务添加到工作队列中,这将唤醒正在等待任务的子进程,但只有一个子进程能获得新任务的接管权,它可以从工作队列中取出任务并执行之,而其他子进程将继续睡眠在工作队列上。

选好子进程后,主进程还需使用某种通知机制来告诉目标子进程有新任务需要处理,并传递必要的数据,最简单的方法是,在父进程和子进程之间先创建好一条管道,然后通过该管道来实现所有的进程间通信(当然要预先定义好一套协议来规范管道的使用)。在父线程和子线程之间传递数据就要简单得多,因为我们可以把这些数据定义为全局的,那么它们本身就是被所有线程共享的。

进程池的一般模型为:
在这里插入图片描述
使用进程池处理多客户任务时,首先要考虑的一个问题是:监听socket和连接socket是否都由主进程来统一管理。第8章中,半同步/半反应堆模式是由主进程统一管理这两种socket的,而更高效的半同步/半异步模式和领导者/追随者模式,则是由主进程管理所有监听socket,而各个子进程分别管理属于自己的连接socket的。半同步/半异步模式中,主进程接受新的连接以得到连接socket,然后它需要将该socket传递给子进程(对于线程池而言,父线程将socket传递给子线程是简单的,因为它们可以共享该socket,而对于进程池,我们需要用UNIX域套接字来传递socket);而领导者/追随者模式的灵活性更大一点,因为子进程可以自己调用accept来接受新连接,这样父进程就无须向子进程传递socket,而只需简单地向子进程通知一声:我检测到了新连接,你来接受它。

长连接只一个客户的多次请求可以复用一个TCP连接,在设计进程池时还要考虑:一个客户连接上的所有任务是否始终由一个子进程来处理,如果客户任务是无状态的,那么我们可以考虑使用不同的子进程来为该客户的不同请求服务,如下图所示:
在这里插入图片描述
但如果客户任务是存在上下文关系的,则最好一直用同一个子进程来为之服务,否则实现起来会比较麻烦,我们将不得不在各子进程之间传递上下文数据。

第八章中的半同步/半异步并发模式:
在这里插入图片描述
以下代码实现一个半同步/半异步并发模式的进程池,为了避免在父子进程间传递文件描述符,我们将接受新连接的操作放到子进程中,对于这种模式而言,一个客户连接上的所有任务始终是由一个子进程来处理的:

// filename: processpool.h
#ifndef PROCESSPOOL_H
#define PROCESSPOOL_H

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>

// 描述一个子进程的类
class process {
public:
    process() : m_pid(-1) { }

private:
    // 目标子进程的PID
    pid_t m_pid;
    // 父进程和子进程通信用的管道
    int m_pipefd[2];
};

// 进程池类,将它定义为模板类是为了代码复用,其模板参数是处理逻辑任务的类
template <typename T> class processpool {
private:
    // 私有构造函数,只能通过后面的create静态方法来创建processpool实例
    processpool(int listenfd, int process_number = 8);

public:
    // 单体模式,以保证进程最多创建一个processpool实例,这是程序正确处理信号的必要条件
    static processpool<T> *create(int listenfd, int process_number = 8) {
        // 此处有bug,默认new失败会抛异常,而非返回空指针
        if (!m_instance) {
            m_instance = new processpool<T>(listenfd, process_number);
        }
        return m_instance;
    } 
    
    ~processpool() {
        delete[] m_sub_process;
    }
    
    // 启动进程池
    void run();
    
private:
    void setup_sig_pipe();
    void run_parent();
    void run_child();

private:
    // 进程池允许的最大子进程数量
    static const int MAX_PROCESS_NUMBER = 16;
    // 每个子进程最多能处理的客户数量
    static const int USER_PER_PROCESS = 65536;
    // epoll最多能处理的事件数
    static const int MAX_EVENT_NUMBER = 10000;
    // 进程池中的进程总数
    int m_process_number;
    // 子进程在池中的序号,从0开始
    int m_idx;
    // 每个进程都有一个epoll内核事件表,用m_epollfd标识
    int m_epollfd;
    // 监听socket
    int m_listenfd;
    // 子进程通过m_stop决定是否停止运行
    int m_stop;
    // 保存所有子进程的描述信息
    process *m_sub_process;
    // 进程池静态实例
    static processpool<T> *m_instance;
};

teamplate<typename T> processpool<T> *processpool<T>::m_instance = NULL;

// 用来处理信号的管道,以实现统一事件源,后面称之为信号管道
static int sig_pipefd[2];

static int setnonblocking(int fd) {
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

static void addfd(int epollfd, int fd) {
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}

// 从epollfd参数标识的epoll内核事件表中删除fd上的所有注册事件
static void removefd(int epollfd, int fd) {
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
    close(fd);
}

static void sig_handler(int sig) {
    int save_errno = errno;
    int msg = sig;
    // 发送的sig的低位1字节,如果主机字节序是大端字节序,则发送的永远是0
    send(sig_pipefd[1], (char *)&msg, 1, 0);
    errno = save_errno;
}

static void addsig(int sig, void handler(int), bool restart = true) {
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    sa.sa_handler = handler;
    if (restart) {
        sa.sa_flags |= SA_RESTART;
    }
    sigfillset(&sa.sa_mask);
    assert(sigaction(sig, &sa, NULL) != -1);
}

// 进程池的构造函数,参数listenfd是监听socket,它必须在创建进程池前被创建
// 否则子进程无法直接引用它,参数process_number指定进程池中子进程的数量
template<typename T> processpool<T>::processpool(int listenfd, int process_number) 
: m_listenfd(listenfd), m_process_number(process_number), m_idx(-1), m_stop(false) {
    assert((process_number > 0) && (process_number <= MAX_PROCESS_NUMBER));

    // 此处有bug,默认new失败会抛异常,而非返回空指针
    m_sub_process = new process[process_number];
    assert(m_sub_process);
    
    // 创建process_number个子进程,并建立它们和父进程之间的管道
    for (int i = 0; i < process_number; ++i) {
        int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_sub_process[i].m_pipefd);
        assert(ret == 0);

        m_sub_process[i].m_pid = fork();
        assert(m_sub_process[i].m_pid >= 0);
        if (m_sub_process[i].m_pid > 0) {
            close(m_sub_process[i].m_pipefd[1]);
            continue;
        } else {
            close(m_sub_process[i].m_pipefd[0]);
            m_idx = i;
            break;
        }
    }
}

// 统一事件源
template<typename T> void processpool<T>::setup_sig_pipe() {
    // 创建epoll事件监听表
    m_epollfd = epoll_create(5);
    assert(m_epollfd != -1);
    
    // 创建信号管道
    int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, sig_pipefd);
    assert(ret != -1);
    
    setnonblocking(sig_pipefd[1]);
    addfd(m_epollfd, sig_pipefd[0]);
    
    // 设置信号处理函数
    addsig(SIGCHLD, sig_handler);
    addsig(SIGTERM, sig_handler);
    addsig(SIGINT, sig_handler);
    addsig(SIGPIPE, SIG_IGN);
}

// 父进程中m_idx值为-1,子进程中m_idx值大于等于0,我们据此判断要运行的是父进程代码还是子进程代码
template<typename T> void processpool<T>::run() {
    if (m_idx != -1) {
        run_child();
        return;
    }
    run_parent();
}

template<typename T> void processpool<T>::run_child() {
    setup_sig_pipe();
    
    // 每个子进程都通过其在进程池中的序号值m_idx找到与父进程通信的管道
    int pipefd = m_sub_process[m_idx].m_pipefd[1];
    // 子进程需要监听管道文件描述符pipefd,因为父进程将通过它通知子进程accept新连接
    addfd(m_epollfd, pipefd);
    
    epoll_event events[MAX_EVENT_NUMBER];
    // 此处有bug,默认new失败会抛异常,而非返回空指针
    T *users = new T[USER_PER_PROCESS];
    assert(users);
    int number = 0;
    int ret = -1;
    
    while (!m_stop) {
        number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
        if ((number < 0) && (errno != EINTR)) {
            printf("epoll failure\n");
            break;
        }
        
        for (int i = 0; i < number; ++i) {
            int sockfd = events[i].data.fd;
            if ((sockfd == pipefd) && (events[i].events & EPOLLIN)) {
                int client = 0;
                ret = recv(sockfd, (char *)&client, sizeof(client), 0);
                if (((ret < 0) && (errno != EAGAIN)) || ret == 0) {
                    continue;
                } else {
                    struct sockaddr_in client_address;
                    socklen_t client_addrlength = sizeof(client_address);
                    int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, 
                                        &client_addrlength);
                    if (connfd < 0) {
                        printf("errno is: %d\n", errno);
                        continue;
                    }
                    addfd(m_epollfd, connfd);
                    // 模板类T必须实现init方法,以初始化一个客户连接,我们直接使用connfd来索引逻辑处理对象(T对象)
                    // 这样效率较高,但比较占用空间(在子进程的堆内存中创建了65535个T对象)
                    users[connfd].init(m_epollfd, connfd, client_address);
                }
            // 处理子进程接收到的信号
            } else if ((sockfd == sigpipefd[0]) && (events[i].events & EPOLLIN)) {
                int sig;
                char signals[1024];
                ret = recv(sig_pipefd[0], signals, sizeof(signals), 0);
                if (ret <= 0) {
                    continue;
                } else {
                    for (int i = 0; i < ret; ++i) {
                        switch (signals[i]) {
                        case SIGCHLD:
                            pid_t pid;
                            int stat;
                            while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
                                continue;
                            }    
                            break;
                        
                        case SIGTERM:
                        case SIGINT:
                            m_stop = true;
                            break;
                        
                        default:
                            break;
                        }
                    }
                }
            // 如果是客户发来的请求,则调用逻辑处理对象的process方法处理之
            } else if (events[i].events & EPOLLIN) {
                users[sockfd].process();
            } else {
                continue;
            }
        }
    }
    
    delete[] users;
    users = NULL;
    close(pipefd);
    // 我们将关闭监听描述符的代码注释掉,以提醒读者:应由m_listenfd的创建者来关闭这个文件描述符
    // 即所谓的对象(如文件描述符或一段堆内存)应由创建函数来销毁
    // close(m_listenfd);
    close(m_epollfd);
}

template<typename T> void processpool<T>::run_parent() {
    setup_sig_pipe();
    
    // 父进程监听m_listenfd
    addfd(m_epollfd, m_listenfd);
    
    epoll_event events[MAX_EVENT_NUMBER];
    int sub_process_counter = 0;
    int new_conn = 1;
    int number = 0;
    int ret = -1;
    
    while (!m_stop) {
        number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
        if ((number < 0) && (errno != EINTR)) {
            printf("epoll failure\n");
            break;
        }
        
        for (int i = 0; i < number; ++i) {
            int sockfd = events[i].data.fd;
            if (sockfd == m_listenfd) {
                // 如果有新连接到来,就用Round Robin方式将其分配给一个子进程处理
                int i = sub_process_counter;
                do {
                    if (m_sub_process[i].m_pid != -1) {
                        break;
                    }
                    i = (i + 1) % m_process_number;
                } while (i != sub_process_counter);
                
                if (m_sub_process[i].m_pid == -1) {
                    m_stop = true;
                    break;
                }
                sub_process_counter = (i + 1) % m_process_number;
                send(m_sub_process[i].m_pipefd[0], (char *)&new_conn, sizeof(new_conn), 0);
                printf("send request to child %d\n", i);
            } else if ((sockfd == sig_pipefd[0]) && (events[i].events & EPOLLIN)) {
                int sig;
                char signals[1024];
                ret = recv(sig_pipefd[0], signals, sizeof(signals), 0);
                if (ret <= 0) {
                    continue;
                } else {
                    for (int i = 0; i < ret; ++i) {
                        switch (signals[i]) {
                        case SIGCHLD:
                            pid_t pid;
                            int stat;
                            while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
                                for (int i = 0; i < m_process_number; ++i) {
                                    // 如果进程池中第i个进程退出
                                    if (m_sub_process[i].m_pid == pid) {
                                        printf("child %d join\n", i);
                                        // 关闭与该子进程的通信管道
                                        close(m_sub_process[i].m_pipefd[0]);
                                        // 将该子进程的m_pid设为-1,表示该子进程已退出
                                        m_sub_process[i].m_pid = -1;
                                    }
                                }
                            }
                            
                            // 如果所有子进程都已退出,则父进程也退出
                            m_stop = true;
                            for (int i = 0; i < m_process_number; ++i) {
                                if (m_sub_process[i].m_pid != -1) {
                                    m_stop = false;
                                }
                            }
                            break;
                        }
                        break;
                        
                        case SIGTERM:
                        case SIGINT:
                            // 如果父进程接收到终止信号,就杀死所有子进程,并等待它们全部结束
                            // 通知子进程结束更好的方式是向父子进程之间的通信管道发送特殊数据
                            printf("kill all the child now\n");
                            for (int i = 0; i < m_process_number; ++i) {
                                int pid = m_sub_process[i].m_pid;
                                if (pid != -1) {
                                    kill(pid, SIGTERM);
                                }
                            }
                            break;
                        
                        default:
                            break;             
                        }
                    }
                }
            } else {
                continue;
            }
        }
    }
    
    // 由创建者关闭这个文件描述符
    // close(m_listenfd);   
    close(m_epollfd); 
}

#endif

用以上进程池实现一个并发CGI服务器:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <sys/wait.h>
#include <sys/stat.h>

// 引用进程池
#include "processpool.h"

// 用于处理客户CGI请求的类,它可作为processpool类的模板参数
class cgi_conn {
public:
    cgi_conn() { }
    
    ~cgi_conn() { }
    
    // 初始化客户连接,清空读缓冲区
    void init(int epollfd, int sockfd, const sockaddr_in &client_addr) {
        m_epollfd = epollfd;
        m_sockfd = sockfd;
        m_address = client_addr;
        memset(m_buf, '\0', BUFFER_SIZE);
        m_read_idx = 0;
    }
    
    void process() {
        int idx = 0;
        int ret = -1;
        // 循环读取和分析客户数据
        while (true) {
            idx = m_read_idx;
            ret = recv(m_sockfd, m_buf + idx, BUFFER_SIZE - 1 - idx, 0);
            // 如果读操作发生错误,则关闭客户连接;如果暂时无数据可读,则退出循环
            if (ret < 0) {
                if (errno != EAGAIN) {
                    removefd(m_epollfd, m_sockfd);
                }
                break;
            // 如果对方关闭连接,则服务器也关闭连接
            } else if (ret == 0) {
                removefd(m_epollfd, m_sockfd);
                break;
            } else {
                m_read_idx += ret;
                printf("user content is: %s\n", m_buf);
                // 如果遇到字符\r\n,则开始处理客户请求
                for (; idx < m_read_idx; ++idx) {
                    if ((idx >= 1) && (m_buf[idx - 1] == '\r') && (m_buf[idx] == '\n')) {
                        break;
                    }
                }
                // 如没有遇到\r\n,则需要读取更多数据
                if (idx == m_read_idx) {
                    continue;
                }
                m_buf[idx - 1] = '\0';
                
                char *file_name = m_buf;
                // 判断客户要运行的CGI程序是否存在
                // access函数用于检测file_name参数表示的文件,F_OK表示检测文件是否存在
                if (access(file_name, F_OK) == -1) {
                    removefd(m_epollfd, m_sockfd);
                    break;
                }
                // 创建子进程执行CGI程序
                ret = fork();
                if (ret == -1) {
                    removefd(m_epollfd, m_sockfd);
                    break;
                } else if (ret > 0) {
                    // 父进程只需关闭连接
                    removefd(m_epollfd, m_sockfd);
                    break;
                } else {
                    // 子进程将标准输出重定向到m_sockfd,并执行CGI程序
                    close(STDOUT_FILENO);
                    dup(m_sockfd);
                    execl(m_buf, m_buf, 0);
                    exit(0);
                }
            }
        }
    }

private:
    static const int BUFFER_SIZE = 1024;
    static int m_epollfd;
    int m_sockfd;
    sockaddr_in m_address;
    char m_buf[BUFFER_SIZE];
    // 标记读缓冲中已经读入的客户数据的最后一个字节的下一个位置
};
int cgi::m_epollfd = -1;

int main(int argc, char *argv[]) {
    if (argc <= 2) {
        printf("usage: %s ip_address port_number\n", basename(argv[0]));
        return 1;
    }
    const char *ip = argv[1];
    int port = atoi(argv[2]);
    
    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(listenfd >= 0);
    
    int ret = 0;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);
    
    ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));
    assert(ret != -1);
    
    processpool<cgi_conn> *pool = processpool<cgi_conn>::create(listenfd);
    if (pool) {
        pool->run();
        delete pool;
    }
    close(listenfd);    // main函数创建了listenfd,就由它来关闭
    return 0;
}

第八章中的半同步/半反应堆并发模式:
在这里插入图片描述
我们接下来实现上图所示的半同步/半反应堆模式的线程池,相比以上进程池的实现,该线程池的通用性要高得多,因为它使用一个工作队列完全解除了主线程和工作线程的耦合关系:主线程往工作队列插入任务,工作线程通过竞争来取得任务并执行它。但要想将该线程池应用到实际服务器程序中,我们必须保证所有客户请求都是无状态的,因为同一个连接上的不同请求可能会由不同的线程处理。

// filename: threadpool.h
#ifndef THREADPOOL_H
#define THREADPOOL_H

#include <list>
#include <cstdio>
#include <exception>
#include <pthread.h>
// 引用第14章中的线程同步机制的包装类
#include "locker.h"

// 线程池类,将它定义为模板类是为了代码复用,模板参数T是任务类
template <typename T> class threadpool {
public:
    // 参数thread_number是线程池中线程的数量,max_requests参数是请求队列中最多允许的、等待处理的请求数量
    threadpool(int thread_number = 8, int max_requests = 10000);
    ~threadpool();
    // 往请求队列中添加任务
    bool append(T *append);

private:
    // 工作线程运行的函数,它不断从工作队列中取出任务并执行之
    static void *worker(void *arg);
    void run();
    
    // 线程池中线程数
    int m_thread_number;
    // 请求队列中允许的最大请求数
    int m_max_requests;
    // 描述线程池的数组,其大小为m_thread_number
    pthread_t *m_threads;
    // 请求队列
    std::list<T *> m_workqueue;
    // 保护请求队列的互斥锁
    locker m_queuelocker;
    // 是否有任务需要处理
    sem m_queuestat;
    // 是否结束线程
    bool m_stop;
};

template <typename T> threadpool<T>::threadpool(int thread_number, int max_requests)
    : m_thread_number(thread_number), m_max_requests(max_requests), m_stop(false), m_threads(NULL) {
    if ((thread_number <= 0) || (max_requests <= 0)) {
        throw std::exception();
    }
    
    // 此处有bug,默认new失败会抛异常,而非返回空指针
    m_threads = new pthread_t[m_thread_number];
    if (!m_threads) {
        throw std::exception();
    }
    
    // 创建thread_number个线程,并将它们都设为脱离线程
    for (int i = 0; i < thread_number; ++i) {
        printf("create the %dth thread\n", i);
        // 第3个参数必须指向一个静态函数,要想在静态函数中使用类的某对象中的成员,只能通过两种方式:
        // 1.通过类的静态对象来调用,如单体模式中,静态函数通过类的全局唯一实例来访问动态成员函数
        // 2.将类的对象作为参数传递给该静态函数,然后在静态函数中使用这个对象,此处就用的这种方式
        // 将线程参数设置为this指针,然后在worker函数中获取该指针
        if (pthread_create(m_threads + i, NULL, worker, this) != 0) {
            delete[] m_threads;
            throw std::exception();
        }
        if (pthread_detach(m_threads[i])) {
            delete[] m_threads;
            throw std::exception();
        }
    }
}

template<typename T> threadpool<T>::~threadpool() {
    delete[] m_threads;
    m_stop = true;
}

template <typename T> bool threadpool<T>::append(T *request) {
    // 操作工作队列前对其加锁,因为所有线程都共享它
    m_queuelocker.lock();
    if (m_workqueue.size() > m_max_requests) {
        m_queuelocker.unlock();
        return false;
    }
    m_workqueue.push_back(request);
    m_queuelocker.unlock();
    m_queuestat.post();
    return true;
}

template<typename T> void *threadpool<T>::worker(void *arg) {
    threadpool *pool = (threadpool *)arg;
    pool->run();
    return pool;
}

template<typename T> void threadpool<T>::run() {
    while (!m_stop) {
        m_queuestat.wait();
        m_queuelocker.lock();
        if (m_workqueue.empty()) {
            m_queuelocker.unlock();
            continue;
        }
        T *request = m_workqueue.front();
        m_workqueue.pop_front();
        m_queuelocker.unlock();
        if (!request) {
            continue;
        }
        request->process();
    }
}

#endif

下面使用以上线程池实现一个并发Web服务器。

首先我们需要准备线程池的模板参数类用来封装对逻辑任务的处理,这个类是http_conn,以下代码是其头文件http_conn.h:

#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H

#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>
#include "locker.h"

class http_conn {
public:
    // 文件名的最大长度
    static const int FILENAME_LEN = 200;
    // 读缓冲区的大小
    static const int READ_BUFFER_SIZE = 2048;
    // HTTP请求方法,但我们仅支持GET
    enum METHOD {GET = 0, POST, HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT, PATCH};
    // 解析客户请求时,主状态机所处的状态
    enum CHECK_STATE {CHECK_STATE_REQUESTLINE = 0, CHECK_STATE_HEADER, CHECK_STATE_CONTENT};
    // 服务器处理HTTP请求的结果:
	// NO_REQUEST:请求不完整,需要继续读取客户数据
	// GET_REQUEST:获得了一个完整的客户请求
	// BAD_REQUEST:客户请求有语法错误
	// FORBIDDEN_REQUEST:客户对资源没有足够的访问权限
	// INTERNAL_ERROR:服务器内部错误
	// CLOSED_CONNECTION:客户已经关闭连接
    enum HTTP_CODE {NO_REQUEST, GET_REQUEST, BAD_REQUEST, NO_RESOURCE, FORBIDDEN_REQUEST,
                    FILE_REQUEST, INTERNAL_ERROR, CLOSED_CONNECTION};
    // 行的读取状态
    enum LINE_STATUS {LINE_OK = 0, LINE_BAD, LINE_OPEN};
    
    http_conn();
    ~http_conn();
    
    // 初始化新接受的连接
    void init(int sockfd, const sockaddr_in &addr);
    // 关闭连接
    void close_conn(bool real_close = true);
    // 处理客户请求
    void process();
    // 非阻塞读操作
    bool read();
    // 非阻塞写操作
    bool write();
    
    // 所有socket上的事件都被注册到同一epoll内核事件表中,所以将epoll文件描述符设置为静态的
    static int m_epollfd;
    // 统计用户数量
    static int m_user_count;

private:
    // 初始化连接
    void init();
    // 解析HTTP请求
    HTTP_CODE process_read();
    // 填充HTTP应答
    bool process_write(HTTP_CODE ret);
    
    // 下面一组函数被process_read函数调用以分析HTTP请求
    HTTP_CODE parse_request_line(char *text);
    HTTP_CODE parse_headers(char *text);
    HTTP_CODE parse_content(char *text);
    HTTP_CODE do_request();
    char *get_line() {
        return m_read_buf + m_start_line;
    }
    LINE_STATUS parse_line();
    
    // 下面一组函数被process_write函数调用以填充HTTP应答
    void unmap();
    bool add_response(const char *format, ...);
    bool add_content(const char *content);
    bool add_status_line(int status, const char *title);
    bool add_headers(int content_length);
    bool add_content_length(int content_length);
    bool add_linger();
    bool add_blank_line();
    
    // 该HTTP连接的socket和对方的socket地址
    int m_sockfd;
    sockaddr_in m_address;
    
    // 读缓冲区
    char m_read_buf[READ_BUFFER_SIZE];
    // 标识读缓冲中已经读入的客户数据的最后一个字节的下一个位置
    int m_read_idx;
    // 当前正在分析的字符在读缓冲区中的位置
    int m_checked_idx;
    // 当前正在解析的行的起始位置
    int m_start_line;
    // 写缓冲区
    char m_write_buf[WRITE_BUFFER_SIZE];
    // 写缓冲区中待发送的字节数
    int m_write_idx;
    
    // 主状态机当前所处的状态
    CHECK_STATE m_check_state;
    // 请求方法
    METHOD m_method;
    
    // 客户请求的目标文件的完整路径,其内容等于doc_root+m_url,doc_root是网站根目录
    char m_real_file[FILENAME_LEN];
    // 客户请求的目标文件的文件名
    char *m_url;
    // HTTP版本号,我们仅支持HTTP/1.1
    char *m_version;
    // 主机名
    char *host;
    // HTTP请求的消息体的长度
    int m_content_length;
    // HTTP请求是否要求保持连接
    bool m_linger;
    
    // 客户请求的目标文件被mmap到内存中的起始位置
    char *m_file_address;
    // 目标文件的状态,可通过它判断文件是否存在、是否是目录、是否可读、文件大小等信息
    struct stat m_file_stat;
    // 我们将采用writev函数来执行写操作,所以定义以下成员,其中m_iv_count表示被写内存块的数量
    struct iovec m_iv[2];
    int m_iv_count;
};

#endif

以下是类http_conn的实现文件http_conn.cpp:

#include "http_conn.h"

// 定义HTTP响应的状态信息
const char *ok_200_title = "OK";
const char *error_400_title = "Bad Request";
const char *error_400_form = "Your request has bad syntax or is inherently impossible to satisfy.\n";
const char *error_403_title = "Forbidden";
const char *error_403_form = "You do not have permission to get file from this server.\n";
const char *error_404_title = "Not Found";
const char *error_404_form = "The requested file was not found on this server.\n";
const char *error_500_title = "Internal Error";
const char *error_500_form = "There was an unusual problem serving the requested file.\n";
const char *doc_root = "/var/www/html";

int setnonblocking(int fd) {
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

void addfd(int epollfd, int fd, bool one_shot) {
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    if (one_shot) {
        event.events |= EPOLLONESHOT;
    }
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}

void removefd(int epollfd, int fd) {
    // epoll_ctl函数的第4个参数是epoll_event类型指针,用于描述与文件描述符fd参数相关的事件以及关联的数据
    // 此处执行删除操作,只需要指定要删除的文件描述符
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
    close(fd);
}

void modfd(int epollfd, int fd, int ev) {
    epoll_event event;
    event.data.fd = fd;
    event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}

int http_conn::m_user_count = 0;
int http_conn::m_epollfd = -1;

void http_conn::close_conn(bool real_close) {
    // 如果real_close为true且当前连接的socket存在(该连接的socket,即m_sockfd,不为-1)
    if (read_close && (m_sockfd != -1)) {
        removefd(m_epollfd, m_sockfd);
        m_sockfd = -1;
        // 关闭一个连接时,将客户总数减1
        --m_user_count;
    }
}

void http_conn::init(int sockfd, const sockaddr_in &addr) {
    m_sockfd = sockfd;
    m_address = addr;
    // 以下两行是为了避免TIME_WAIT状态,仅用于调试,实际使用时应去掉
    int reuse = 1;
    setsockopt(m_sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    addfd(m_epollfd, sockfd, true);
    ++m_user_count;
    
    init();
}

void http_conn::init() {
    m_check_state = CHECK_STATE_REQUESTLINE;
    m_linger = false;
    
    m_method = GET;
    m_url = 0;
    m_version = 0;
    m_content_length = 0;
    m_host = 0;
    m_start_line = 0;
    m_checked_idx = 0;
    m_read_idx = 0;
    m_write_idx = 0;
    memset(m_read_buf, '\0', READ_BUFFER_SIZE);
    memset(m_write_buf, '\0', WRITE_BUFFER_SIZE);
    memset(m_real_file, '\0', FILENAME_LEN);
}

// 从状态机
http_conn::LINE_STATUS http_conn::parse_line() {
    char temp;
    for (; m_checked_idx < m_read_idx; ++m_checked_idx) {
        temp = m_read_buf[m_checked_idx];
        if (temp == '\r') {
            if ((m_checked_idx + 1) == m_read_idx) {
                return LINE_OPEN;
            } else if (m_read_buf[m_checked_idx + 1] == '\n') {
                m_read_buf[m_checked_idx++] = '\0';
                m_read_buf[m_checked_idx++] = '\0';
                return LINE_OK;
            }
            
            return LINE_BAD;
        } else if (temp == '\n') {
            if ((m_checked_idx > 1) && (m_read_buf[m_checked_idx - 1] == '\r')) {
                m_read_buf[m_checked_idx - 1] = '\0';
                m_read_buf[m_checked_idx++] = '\0';
                return LINE_OK;
            }
            return LINE_BAD;
        }
    }
    
    return LINE_OPEN;
}

bool http_conn::read() {
    if (m_read_idx >= READ_BUFFER_SIZE) {
        return false;
    }
    
    int bytes_read = 0;
    while (true) {
        bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
        if (bytes_read == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                break;
            }
            return false;
        } else if (bytes_read == 0) {
            return false;
        }
        
        m_read_idx += bytes_read;
    }
    return true;
}

// 解析HTTP请求行,获得请求方法、目标URL、HTTP版本号
// 我们预期text的格式类似GET /path/to/resource HTTP/1.1
http_conn::HTTP_CODE http_conn::parse_request_line(char *text) {
    // strpbrk函数用于在一个字符串中查找第一个包含在指定字符集合中的字符,并返回该字符在字符串中的位置
    m_url = strpbrk(text, " \t");
    if (!m_url) {
        return BAD_REQUEST;
    }
    *m_url++ = '\0';
    
    char *method = text;
    if (strcasecmp(method, "GET") == 0) {
        m_method = GET;
    } else {
        return BAD_REQUEST;
    }
    
    // strspn函数返回一个size_t类型的值,表示在第一个参数中从开头开始的连续字符数量
    // 这些字符都包含在第二个参数中的字符集合中
    m_url += strspn(m_url, " \t");
    m_version = strpbrk(m_url, " \t");
    if (!m_version) {
        return BAD_REQUEST;
    }
    *m_version++ = '\0';
    m_version += strspn(m_version, " \t");
    if (strcasecmp(m_version, "HTTP/1.1") != 0) {
        return BAD_REQUEST;
    }
    
    if (strncasecmp(m_url, "http://", 7) == 0) {
        m_url += 7;
        // strchr函数在一个字符串中查找指定字符的第一次出现的位置,并返回该位置的指针
        m_url = strchr(m_url, '/');
    }
    
    if (!m_url || m_url[0] != '/') {
        return BAD_REQUEST;
    }
    
    m_check_state = CHECK_STATE_HEADER;
    return NO_REQUEST;
}

// 解析HTTP请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text) {
    // 遇到空行,表示不再有头部字段
    if (text[0] == '\0') {
        // 如果HTTP请求有消息体,则还需读取m_content_length字节的消息体
        // 状态机转移到CHECK_STATE_CONTENT状态
        if (m_content_length != 0) {
            m_check_state = CHECK_STATE_CONTENT;
            return NO_REQUEST;
        }
        
        // 否则说明我们已经得到了一个完整HTTP请求
        return GET_REQUEST;
    } else if (strncasecmp(text, "Connection:", 11) == 0) {
            text += 11;
            text += strspn(text, " \t");
            if (strcasecmp(text, "keep-alive") == 0) {
                m_linger = true;
            }
        }
    } else if (strncasecmp(text, "Content-Length:", 15) == 0) {
        text += 15;
        text += strspn(text, " \t");
        m_content_length = atoi(text);
    } else if (strncasecmp(text, "Host:", 5) == 0) {
        text += 5;
        text += strspn(text, " \t");
        m_host = text;
    } else {
        printf("oop! unknown header %s\n", text);
    }
    
    return NO_REQUEST;
}

// 我们没有真正解析HTTP请求的消息体,只是判断它是否被完整读入了
http_conn::HTTP_CODE http_conn::parse_content(char *text) {
    if (m_read_idx >= m_content_length + m_checked_idx) {
        text[m_content_length] = '\0';
        return GET_REQUEST;
    }
    
    return NO_REQUEST;
}

// 主状态机
http_conn::HTTP_CODE http_conn::process_read() {
    LINE_STATUS line_status = LINE_OK;
    HTTP_CODE ret = NO_REQUEST;
    char *text = 0;
    
    while (((m_check_state == CHECK_STATE_CONTENT) && (line_status == LINE_OK)) 
           || ((line_status = parse_line()) == LINE_OK)) {
        text = get_line();
        m_start_line = m_checked_idx;
        printf("got 1 http line: %s\n", text);
        
        switch (m_check_state) {
        case CHECK_STATE_REQUESTLINE:
            ret = parse_request_line(text);
            if (ret == BAD_REQUEST) {
                return BAD_REQUEST;
            }
            break;
        
        case CHECK_STATE_HEADER:
            ret = parse_headers(text);
            if (ret == BAD_REQUEST) {
                return BAD_REQUEST;
            } else if (ret == GET_REQUEST) {
                return do_request();
            }
            break;
         
        default:
            return INTERNAL_ERROR;   
        }
    }
    
    return NO_REQUEST;
}

// 得到一个完整、正确的HTTP请求时,该函数分析目标文件的属性
// 如果目标文件存在、对所有用户可读、不是目录,则使用mmap函数将其映射到内存地址m_file_address处
// 并告诉调用者获取文件成功
http_conn::HTTP_CODE http_coonn::do_request() {
    strcpy(m_real_file, doc_root);
    int len = strlen(doc_root);
    strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);
    if (stat(m_real_file, &m_file_stat) < 0) {
        return NO_RESOURCE;
    }
    
    if (!(m_file_stat.st_mode & S_IROTH)) {
        return FORBIDDEN_REQUEST;
    }
    
    if (S_ISDIR(m_file_stat.st_mode)) {
        return BAD_REQUEST;
    }
    
    int fd = open(m_real_file, O_RDONLY);
    m_file_address = (char *)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);
    return FILE_REQUEST;
}

// 对内存映射区执行munmap操作
void http_conn::unmap() {
    if (m_file_address) {
        munmap(m_file_address, m_file_stat.st_size);
        m_file_address = 0;
    }
}

// 写HTTP响应
bool http_conn::write() {
    int temp = 0;
    int bytes_have_send = 0;
    int bytes_to_send = m_write_idx;
    if (bytes_to_send == 0) {
        modfd(m_epollfd, m_sockfd, EPOLLIN);
        init();
        return true;
    }
    
    while (1) {
        temp = writev(m_sockfd, m_iv, m_iv_count);
        if (temp <= -1) {
            // 如果TCP写缓冲区没有空间,则等待下一轮EPOLLOUT事件
            // 虽然在此期间,服务器无法立即收到同一客户的下一请求,但这可保证同一连接的完整性
            if (errno == EAGAIN) {
                modfd(m_epollfd, m_sockfd, EPOLLOUT);
                return true;
            }
            unmap();
            return false;
        }
        
        bytes_to_send -= temp;
        bytes_have_send += temp;
        if (bytes_to_send <= bytes_have_send) {
            // 发送HTTP响应成功
            unmap();
            if (m_linger) {
                // 此处处理完一个请求后,直接调用init清空了读缓冲区
                // 如果客户连续发送多个请求,读缓冲区中可能有多于一个请求的数据
                // 会丢失请求,如果读缓冲中某个请求只读了一半,则接下来的读操作会读入另一半
                // 然后由于只有一半请求而被认为是请求语法有问题
                init();
                modfd(m_epollfd, m_sockfd, EPOLLIN);
                return true;
            } else {
                modfd(m_epollfd, m_sockfd, EPOLLIN);
                return false;
            }
        }
    }
}

bool http_conn::add_response(const char *format, ...) {
    if (m_write_idx >= WRITE_BUFFER_SIZE) {
        return false;
    }
    va_list arg_list;
    va_start(arg_list, format);
    // vsnprintf函数会根据format字符串中的格式控制码,将可变参数列表中的值格式化后写入str所指向的缓存区
    // 该函数返回写入缓冲区的字节数,包含结尾的\0
    // 如果缓冲区太小,则该函数返回要写入的数据的字节数(此时不包含结尾的\0)
    // 因此,如果该函数返回值大于等于第二个参数的大小,说明缓冲区太小
    int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
    if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx)) {
        return false;
    }
    m_write_idx += len;
    // va_start函数和va_end函数必须成对出现,va_end函数用于清理可变参数列表,用于避免潜在的内存泄漏或数据损坏
    va_end(arg_list);
    return true;
}

bool http_conn::add_status_line(int status, const char *title) {
    return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);
}

bool http::conn::add_headers(int content_len) {
    add_content_length(content_len);
    add_linger();
    add_blank_line();
}

bool http_conn::add_content_length(int content_len) {
    return add_response("Content-Length: %d\r\n", content_len);
}

bool http_conn::add_linger() {
    return add_response("Connection: %s\r\n", (m_linger == true) ? "keep-alive" : "close");
}

bool http_conn::add_blank_line() {
    return add_response("%s", "\r\n");
}

bool http_conn::add_content(const char *content) {
    return add_response("%s", content);
}

// 根据服务器处理HTTP请求的结果,决定返回给客户端的内容
bool http_conn::process_write(HTTP_CODE ret) {
    switch (ret) {
    case INTERNAL_ERROR:
        add_status_line(500, error_500_title);
        add_headers(strlen(error_500_form));
        if (!add_content(error_500_form)) {
            return false;
        }
        break;
    
    case BAD_REQUEST:
        add_status_line(400, error_400_title);
        add_headers(strlen(error_400_form));
        if (!add_content(error_400_form)) {
            return false;
        }
        break;
    
    case NO_RESOURCE:
        add_status_line(400, error_400_title);
        add_headers(strlen(error_404_form));
        if (!add_content(error_404_form)) {
            return false;
        }
        break;
       
    case FORBIDDEN_REQUEST:
        add_status_line(403, error_403_title);
        add_headers(strlen(error_403_form));
        if (!add_content(error_403_form)) {
            return false;
        }    
        break;

    case FILE_REQUEST:
        add_status_line(200, ok_200_title);
        if (m_file_stat.st_size != 0) {
            add_headers(m_file_stat.st_size);
            m_iv[0].iov_base = m_write_buf;
            m_iv[0].iov_len = m_write_idx;
            m_iv[1].iov_base = m_file_address;
            m_iv[1].iov_len = m_file_stat.st_size;
            m_iv_count = 2;
            return true;
        } else {
            const char *ok_string = "<html><body></body></html>";
            add_headers(strlen(ok_string));
            if (!add_content(ok_string)) {
                return false;
            }
        }
        break;
    
    default:
        return false;    
    }
    
    m_iv[0].iov_base = m_write_buf;
    m_iv[0].iov_len = m_write_idx;
    m_iv_count = 1;
    return true;
}

// 由线程池中的工作线程调用,这是处理HTTP请求的入口
void http_conn::process() {
    HTTP_CODE read_ret = process_read();
    if (read_ret == NO_REQUEST) {
        modfd(m_epollfd, m_sockfd, EPOLLIN);
        return;
    }
    
    bool write_ret = process_write(read_ret);
    if (!write_ret) {
        close_conn();
    }
    modfd(m_epollfd, m_sockfd, EPOLLOUT);
}

定义好任务类后,main函数只需负责IO读写即可:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <cssert>
#include <sys/epoll.h>
#include <libgen.h>

#include "locker.h"
#include "threadpool.h"
#include "http_conn.h"

#define MAX_FD 65536
#define MAX_EVENT_NUMBER 10000

extern int addfd(int epollfd, int fd, bool one_shot);
extern int removefd(int epollfd, int fd);

void addsig(int sig, void handler(int), bool restart = true) {
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    sa.sa_handler = handler;
    if (restart) {
        sa.sa_flags |= SA_RESTART;
    }
    sigfillset(&sa.sa_mask);
    assert(sigaction(sig, &sa, NULL) != -1);
}

void show_error(int connfd, const char *info) {
    printf("%s", info);
    send(connfd, info, strlen(info), 0);
    close(connfd);
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("usage: %s ip_address port_number\n", basename(argv[0]));
        return 1;
    }
    const char *ip = argv[1];
    int port = atoi(argv[2]);
    
    // 忽略SIGPIPE信号
    addsig(SIGPIPE, SIG_IGN);
    
    // 创建线程池
    threadpool<http_conn> *pool = NULL;
    try {
        pool = new threadpool<http_conn>;
    // 捕获所有异常
    } catch ( ... ) {
        return 1;
    }
    
    // 预先为每个可能的客户连接分配一个http_conn对象
    // 此处有bug,默认new失败会抛异常,而非返回空指针
    http_conn *users = new http_conn[MAX_FD];
    assert(users);
    int user_count = 0;
    
    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(listenfd >= 0);
    // 关闭连接时,直接给对面发送RST
    struct linger tmp = {1, 0};
    setsockopt(listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    
    int ret = 0;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);
    
    ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));
    assert(ret >= 0);
    
    ret = listen(listenfd, 5);
    assert(ret >= 0);
    
    epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);
    assert(epollfd != -1);
    addfd(epollfd, listenfd, false);
    http_conn:m_epollfd = epollfd;
    
    while (true) {
        int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
        if (number < 0 && errno != EINTR) {
            printf("epoll failure\n");
            break;
        }
        
        for (int i = 0; i < number; ++i) {
            int sockfd = events[i].data.fd;
            if (sockfd == listenfd) {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof(client_address);
                int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
                if (connfd < 0) {
                    printf("errno is: %d\n", errno);
                    continue;
                }
                if (http_conn::m_user_count >= MAX_FD) {
                    show_error(connfd, "Internal server busy");
                    continue;
                }
                // 初始化客户连接
                users[connfd].init(connfd, client_address);
            }
	        } else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {
	            // 如果有异常,直接关闭客户连接
	            users[sockfd].close_conn();
	        } else if (events[i].events & EPOLLIN) {
	            // 根据读的结果,决定是将任务添加到线程池,还是关闭连接
	            if (users[sockfd].read()) {
	                pool->append(users + sockfd);
	            } else {
	                users[sockfd].close_conn();
	            }
	        } else if (events[i].events & EPOLLOUT) {
	            // 根据写的结果,决定是否关闭连接
	            if (!users[sockfd].write()) {
	                users[sockfd].close_conn();
	            }
	        } 
        }
    }
    
    close(epollfd);
    close(listenfd);
    delete[] users;
    delete pool;
    return 0;
}

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

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

相关文章

基于yolov2深度学习网络的猫脸检测识别matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 load yolov2.mat% 加载训练好的目标检测器 img_size [224,224]; imgPath test/; % 图…

哈佛教授因果推断力作:《Causal Inference: What If 》pdf下载

因果推断是一项复杂的科学任务&#xff0c;它依赖于多个来源的三角互证和各种方法论方法的应用&#xff0c;是用于解释分析的强大建模工具&#xff0c;同时也是机器学习领域的热门研究方向之一。 今天我要给大家推荐的这本书&#xff0c;正是因果推断领域必读的入门秘籍&#…

《WebGIS快速开发教程第四版》重磅更新

随着笔者夜以继日的不断忙碌&#xff0c;丰富和完善心血之作《WebGIS快速开发教程》&#xff0c;第四版也终于发布了&#xff0c;第四版相比于前三个版本可以用四个字概括那就是“重磅更新”&#xff0c;重磅两个字该如何理解呢&#xff1f; 首先我们来看看更新了哪些内容&…

【MySQL】如何在Linux上安装MySQL

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaEE 操作系统 Redis 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 MySQL 一、准备Linux服务器二、下载Linux版…

开源欧拉 openEuler 23.09 创新版本发布

导读近日&#xff0c;openEuler 23.09 创新版本正式发布&#xff0c;是社区最新发布的创新版&#xff0c;使用 EulerMaker 构建该版本的的服务器、云计算、边缘计算镜像&#xff0c;版本代码总计 9.1 亿行&#xff0c;相比 openEuler 23.03&#xff0c;新增代码 8900 万行。 新…

Flutter笔记:发布一个电商中文货币显示插件Money Display

Flutter笔记 电商中文货币显示插件 Money Display 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/1338…

功率放大器在超声导波中的应用有哪些

超声导波技术是一种基于声波传播原理的非破坏性检测技术。它通过向被测物体中注入超声波&#xff0c;并接收反射回来的信号&#xff0c;来分析被测物体的内部结构和缺陷情况。在超声导波技术中&#xff0c;功率放大器作为信号源和信号放大器&#xff0c;发挥着重要的作用。下面…

pdf文件大小超过限制怎么办?一招教你压缩pdf文件

我们在制作pdf文档的时候&#xff0c;会加入许多内容&#xff0c;文字、图片等等&#xff0c;素材添加的过多之后就会导致pdf文档特别大&#xff0c;在上传或者储存时&#xff0c;就会特别不方便&#xff0c;所以今天就告诉大家一个pdf压缩&#xff08;https://www.yasuotu.com…

2023年中国氯丁橡胶产量、需求量及进出口现状分析[图]

氯丁橡胶是以2-氯-1,3-丁二烯为主要单体&#xff0c;通过自由基乳液聚合制得的极性合成橡胶。氯丁橡胶具有优异的阻燃性、耐热性、耐候性及耐化学品性&#xff0c;在工业制品、汽车配件、电线电缆护套及粘合剂等领域具有广泛的应用。 2022年&#xff0c;国内氯丁橡胶装置存在2-…

[正式学习java①]——java项目结构,定义类和创建对象,一个标准javabean的书写

目录 一、创建第一个java文件 二、 初始类和对象 三、符合javabean规范的类 一、创建第一个java文件 要想写代码&#xff0c;你得有文件啊 以前的创建方式&#xff1a; 右键新建文本文档&#xff0c;开始写代码&#xff0c;写完改后缀名&#xff0c;保存……这样文件一旦多了…

c语言从入门到实战——C语言数据类型和变量

C语言数据类型和变量 前言1. 数据类型介绍1.1 字符型1.2 整型1.3 浮点型1.4 布尔类型1.5 各种数据类型的长度1.5.1 sizeof操作符1.5.2 数据类型长度1.5.3 sizeof中表达式不计算 2. signed 和 unsigned3. 数据类型的取值范围4. 变量4.1 变量的创建4.2 变量的分类 5. 算术操作符&…

竞赛 深度学习OCR中文识别 - opencv python

文章目录 0 前言1 课题背景2 实现效果3 文本区域检测网络-CTPN4 文本识别网络-CRNN5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习OCR中文识别系统 ** 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;…

如何管理嵌入式开发中产生的数字资产?ACT汽车电子与软件技术周演讲回顾

2023 ATC汽车电子与软件技术周已于8月18日在中国上海落下帷幕。展会现场&#xff0c;龙智技术支持部负责人、Atlassian认证专家叶燕秀与龙智技术工程师邱洁玉共同为观众带来了主题为“更好、更快、更安全&#xff1a;嵌入式开发中的最佳实践与工具链构建”的演讲&#xff0c;分…

UE5射击游戏案例蓝图篇(一)

一、使用到的资源 1.小白人动画包 2.基础武器包 3.虚幻商城免费的模型包 二、角色创建 1.以Character为基类创建出需要的角色&#xff0c;双击打开之后并在已有组件的基础上&#xff0c;添加摄像机臂和摄像机两个组件。添加完成之后可以根据自己的需要调整摄像机臂的位置&…

4. qgis c++二次开发 map canvas介绍

文章目录 前言Map canvasQGis软件中的Map canvas代码添加Map canvasMap Canvas创建和显示 QGis中的QGraphicsItem二次开发中的Item Layer TreeQGis软件中的Layer Tree代码实现layer tree QgsProject(项目管理)QGis软件中的项目管理代码实现 总结 前言 前几篇文章分别介绍了qgi…

软件测试担心失业,如何找一份稳定的技术性工作?没有35岁中年危机!

工作难找&#xff0c;大龄程序员屡次碰壁&#xff0c;感慨并担忧自己的未来没出路&#xff01; 经常有网友发帖留言&#xff1a; 今年1月4号被裁员&#xff0c;至今未找到工作&#xff0c;之前做的是软件测试&#xff0c;boss上沟通了3000多次&#xff0c;投简历200多次&#…

JimuReport 积木报表 v1.6.4 稳定版本正式发布 — 开源免费的低代码报表

项目介绍 一款免费的数据可视化报表&#xff0c;含报表和大屏设计&#xff0c;像搭建积木一样在线设计报表&#xff01;功能涵盖&#xff0c;数据报表、打印设计、图表报表、大屏设计等&#xff01; Web 版报表设计器&#xff0c;类似于excel操作风格&#xff0c;通过拖拽完成报…

高通新骁龙处理器将于明年上半年发布,携四大品牌厂商首发 | 百能云芯

高通&#xff08;Qualcomm&#xff09;即将于10月下旬正式亮相首款以Oryon架构打造的 PC CPU「Snapdragon X系列」&#xff0c;据悉&#xff0c;四大品牌联想、惠普&#xff08;HP&#xff09;、戴尔&#xff08;DELL&#xff09;及宏碁将是首波推出相关PC的品牌厂&#xff0c;…

【JAVA-Day45】Java常用类StringBuffer解析

Java常用类StringBuffer解析 Java常用类StringBuffer解析一、什么是StringBuffer类二、StringBuffer类的方法2.1 append方法2.2 insert方法2.3 delete方法2.4 replace方法2.5 reverse方法2.6 toString方法2.7 capacity方法2.8 length方法 三、StringBuffer类的应用场景深入了解…

AP5216 dc-dc平均电流型 LED降压恒流驱动器 全亮半亮9W车灯驱动IC​

1&#xff0c;​​产品描述 AP5216 是一款 PWM工作模式, 高效率、外 围简单、内置功率管&#xff0c;适用于5V&#xff5e;100V输入的高 精度降压 LED 恒流驱动芯片。输出功率可达 9W&#xff0c;电流 1.0A。 AP5216 可实现全亮/半亮功能切换&#xff0c;通过 MODE 切换&#x…