义乌网站建设费用多少,c语言做网站后台服务,郴州今天几例,Vs做的网站调试时如何适应网页动态创建子进程或子线程的缺点#xff1a; 1.动态创建进程或线程比较耗时#xff0c;这将导致较慢的客户响应。
2.动态创建的子进程或子线程通常只用来为一个客户服务#xff08;除非我们做特殊处理#xff09;#xff0c;这将导致系统上产生大量的进程或线程#xff0c…动态创建子进程或子线程的缺点 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:// 目标子进程的PIDpid_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 processpoolT *create(int listenfd, int process_number 8) {// 此处有bug默认new失败会抛异常而非返回空指针if (!m_instance) {m_instance new processpoolT(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;// 监听socketint m_listenfd;// 子进程通过m_stop决定是否停止运行int m_stop;// 保存所有子进程的描述信息process *m_sub_process;// 进程池静态实例static processpoolT *m_instance;
};teamplatetypename T processpoolT *processpoolT::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字节如果主机字节序是大端字节序则发送的永远是0send(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指定进程池中子进程的数量
templatetypename T processpoolT::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;}}
}// 统一事件源
templatetypename T void processpoolT::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我们据此判断要运行的是父进程代码还是子进程代码
templatetypename T void processpoolT::run() {if (m_idx ! -1) {run_child();return;}run_parent();
}templatetypename T void processpoolT::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);
}templatetypename T void processpoolT::run_parent() {setup_sig_pipe();// 父进程监听m_listenfdaddfd(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);processpoolcgi_conn *pool processpoolcgi_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_numberpthread_t *m_threads;// 请求队列std::listT * m_workqueue;// 保护请求队列的互斥锁locker m_queuelocker;// 是否有任务需要处理sem m_queuestat;// 是否结束线程bool m_stop;
};template typename T threadpoolT::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();}}
}templatetypename T threadpoolT::~threadpool() {delete[] m_threads;m_stop true;
}template typename T bool threadpoolT::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;
}templatetypename T void *threadpoolT::worker(void *arg) {threadpool *pool (threadpool *)arg;pool-run();return pool;
}templatetypename T void threadpoolT::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.hclass http_conn {
public:// 文件名的最大长度static const int FILENAME_LEN 200;// 读缓冲区的大小static const int READ_BUFFER_SIZE 2048;// HTTP请求方法但我们仅支持GETenum 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_rootm_urldoc_root是网站根目录char m_real_file[FILENAME_LEN];// 客户请求的目标文件的文件名char *m_url;// HTTP版本号我们仅支持HTTP/1.1char *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不为-1if (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 htmlbody/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 10000extern 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);// 创建线程池threadpoolhttp_conn *pool NULL;try {pool new threadpoolhttp_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);// 关闭连接时直接给对面发送RSTstruct 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;
}