1.服务器编程基本框架

服务器程序种类繁多,但其基本框架都是一样的,它们的不同之处在于逻辑处理。如图所示是服务器的基本框架。

该图既能用来描述一台服务器,也能用来描述一个服务器机群。如下是各个部件的含义和功能。

模块

单个服务器程序

服务器机群

IO处理单元

处理客户连接,读写网络数据

作为接入服务器,实现负载均衡

逻辑单元

业务进程或线程

逻辑服务器

网络存储单元

本地数据库、文件或缓存

数据库服务器

请求队列

各单元之间的通信方式

各服务器之间的永久TCP连接

IO处理单元是服务器管理客户连接的模块。它通常要完成以下工作:等待并接受新的客户链接,接收客户数据,将服务器响应数据返回给客户端。但是,数据的收发不一定再IO处理单元中执行,也可能再逻辑单元中执行,具体在何处执行取决于事件处理模式(Reactor模式/Proactor模式)。对于服务器机群来说,IO处理单元是一个专门的接入服务器。它实现负载均衡,从所有逻辑服务器中选取负荷最小的一台来为新客户服务。

一个逻辑单元通常是一个进程或者线程。它分析并处理客户数据,然后将结果传给IO处理单元或者直接发送给客户端(具体哪种方式取决于事件处理模式)。

网络存储单元可以是数据库、缓存和文件,甚至是一台独立的服务器。但它不是必须的,比如ssh、telnet等登陆服务就不需要这个单元。

请求队列是各单元之间的通信方式的抽象方式的抽象。IO处理单元接收到客户请求时,需要以某种方式通知一个逻辑单元来处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竟态条件。请求队列通常被实现为池的一部分。

2.两种高效的事件处理模式

服务器程序通常需要处理三类事件:IO事件、信号及定时事件。

两种高效的事件处理模式:Reactor和Proactor。

随着网络设计模式的兴起,Reactor和Proactor事件处理模式应运而生。同步IO模型通常用于实现Reactor模式,异步IO模式则用于实现Proactor模式。

1.Reactor模式

Reactor是这样一种模式,它要求主线程(IO处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知给工作线程(逻辑单元)。除此之外,主线程不做任何其他性质的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。

使用同步IO模型(以epoll为例)实现的Reactor模式的工作流程如下:

1.主线程往epoll内核事件表中注册socket上的读就绪事件。

2.主线程调用epoll_wait等待socket有数据可读。

3.当socket上有数据可读时,epoll_wait通知主线程。主线程则将socket的可读事件放入请求队列。

4.睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件中注册该socket上的写就绪事件。

5.当主线程调用epol_wait等待socket可写。

6.当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。

7.睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。

Reactor模式的工作流程:

工作线程从请求队列中取出事件后,将根据事件的类型来决定如何处理它:对于可读事件,执行读数据和处理请求的操作;对于可写事件,执行写数据操作。因此,如图示的Reactor模式中,没有必要区分读工作线程和写工作线程。

2.Proactor模式

与Reactor模式不同,Proactor模式将所有的IO操作都交给主线程和内核来处理,工作线程仅仅负责逻辑业务。因此,Proactor模式更符合上面所提到的服务器编程框架。

现在我们使用同步IO来模拟Proactor模式。其原理是:主线程执行读写操作,读写完成之后,主线程向工作线程通知这一完成事件。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读写结果进行逻辑处理。

使用同步IO模型(epoll_wait为例)模拟Proactor模式的工作流程:

1.主线程往epoll内核事件表中注册socket上的读就绪事件。

2.主线程调用epoll_wait等待socket上有数据可读。

3.当socket上有数据可读时,epoll_wait通知主线程。主线程从socket循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。

4.睡眠在请求队列上的某个工作线程被唤醒,它将获得请求对象并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪事件。

5.主线程调用epoll_wait等待socket可写。

6.当socket可写时,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果。

同步IO模拟Proactor模式的工作流程:

3.线程同步机制包装类

锁机制的功能

实现多线程同步,通过锁机制,确保任一时刻只能有一个线程能进入关键代码段.

#include <exception>
#include <pthread.h>
#include <semaphore.h>//封装信号量的类
class sem
{
public://创建并初始化信号量sem(){if (sem_init(&m_sem, 0, 0) != 0){//构造函数没有返回值,可以通过抛出异常来报告错误throw std::exception();}}//销毁信号量~sem(){sem_destroy(&sm_em);}//等待信号量bool wait(){return sem_wait(&m_sem) == 0;}//增加信号量bool post(){return sem_post(&m_sem) == 0;}private:sem_t m_sem;
};//封装互斥锁的类
class locker
{//创建并初始化互斥锁
public:locker(){if (pthread_mutex_init(&m_mutex, NULL) != 0){throw std::exception();}}//销毁互斥锁~locker(){pthread_mutex_destroy(&m_mutex);}//获取互斥锁bool lock(){return pthread_mutex_lock(&m_mutex) == 0;}//释放互斥锁bool unlock(){return pthread_mutex_unlock(&m_mutex)==0;}
private:pthread_mutex_t m_mutex;
};//封装条件变量的类
class cond {
public://创建并初始化条件变量cond(){if (pthread_mutex_init(&m_mutex, NULL) != 0){throw std::exception();}if (pthread_mutex_init(&m_cond, NULL) != 0){//构造函数中一旦出现问题,就应该立即释放已经成功分配了的资源pthread_mutex_destroy(&m_mutex);throw std::exception();}}//销毁条件变量~cond(){pthread_mutex_destroy(&m_mutex);pthread_cond_destroy(&m_cond);}//等待条件变量bool wait(){int ret = 0;pthread_mutex_lock(&m_mutex);ret = pthread_cond_wait(&m_cond,&m_mutex);pthread_mutex_unlock(&m_mutex);return ret == 0;}//唤醒等待条件变量的线程bool signal(){return pthread_cond_signal(&m_cond)==0;}
private:pthread_mutex_t m_mutex;pthread_cond_t m_cond;};

4.线程池

采用动态创建进程或线程的方法来实现并发服务器存在如下缺点:

1.动态创建进程或线程是比较耗费时间的,这将导致较慢的客户响应。

2.动态创建子进程或子线程通常只用来为一个客户服务,这将导致系统上产生大量的细微进程或线程。进程或线程之间的切换将消耗大量的CPU时间。

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

线程池是由服务器预先创建一组子线程。线程池中所有子线程都运行着相同的代码,并具有相同的属性。因为线程池在服务器启动之处就创建好了,所以每个子线程相对都比较“干净”,即它们没有打开不必要的文件描述符,也不会错误地使用大块的堆内存。

当有新的任务来到时,主线程将通过某种方式选择线程池中的某一个线程来为止服务。相比于动态创建子进程来说,选择一个已经存在的线程的代价显然要小得多。

主线程使用某种算法来主动选择子线程。最简单、最常用的算法是随机算法和Round Robin(轮流选取)算法。

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

1.空间换时间,浪费服务器的硬件资源,换取运行效率。

2.池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并且初始化,这称为静态资源。

3.当服务器进入正式运行阶段,开始处理客户请求的时候,如果它需要相关的资源,可以直接从池中获取,无需动态分配。

4.当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用释放资源。

半同步/半反应堆线程池实现:该线程池通用性要高得多,因为它使用一个工作队列完全解除了主线程和工作线程的耦合关系:主线程往工作队列中插入任务,工作线程通过竞争来取得任务并执行它。

#include <list>
#include <cstdio>
#include <exception>
#include <pthread.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd>
#include<fcntl.h>
#include "locker.h"//互斥锁类/*线程池类,将它定义为模板类是为了代码复用,模板参数T是人物类*/template<typename T>
class threadpool
{
public:/*参数thread_number是线程池中线程的数量,max_requests是请求队列中最多允许的、等待处理的请求的数量*/threadpool(int thread_number = 0, int max_requests = 10000);~threadpool();//往请求队列中添加任务bool append(T* request);private://工作线程运行的函数,它不断从工作队列中取出任务并执行static void* worker(void* arg);void run();private:int m_thread_number;//线程池中的线程数int m_max_requests;//请求队列中允许的最大请求数pthread_t* m_threads;//描述线程池的数组,其大小为m_thread_numberstd::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();}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);if (pthread_create(m_thread + i, NULL, worker, this) != 0){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.unlocker();//添加一个任务,信号量加一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.unlocker();continue;}T* request = m_workqueue.front();m_workqueue.pop_front();m_queuelocker.unlocker();if (!request){continue;}request->process();//执行具体业务的函数}}

3.有限状态机

逻辑单元内部的一种高效编程方法:有限状态机。

有的应用层协议头部包含数据包类型字段,每种类型可以映射为逻辑单元的一种执行状态,服务器可以根据它来编写相应的处理逻辑。

//状态独立的有限状态机
STATE_MACHINE(Package _pack)
{PackageType _type = _pack.GetType();switch (_type){case type_A:process_package_A(_pack);break;case type_B:process_package_B(_pack);break;}}

这就是一个简单的有限状态机,只不过该状态机的每个状态都是相互独立的,即状态之间没有相互转移。状态之间的转移是需要状态机内部驱动的。

//带状态转移的有限状态机STATE_MACHINE(Package _pack)
{State cur_State=type_A;While( cur_State!=type_c){switch (cur_State){case type_A:process_package_A(_pack);cur_State=type_B;break;case type_B:process_package_B(_pack);cur_State=type_C;break;}}}

HTTP请求的读取和分析。很多网络协议,包括TCP协议和IP协议,都在其头部中提供头部长度字段。程序根据该字段的值就可以知道是否接收到一个完整的协议头部。但HTTP协议并未提供这样的头部长度字段,并且其头部长度变化也很大,可以只有十几字节,也可以有上百字节。根据协议规定,我们判断HTTP头部结束的依据是遇到一个空行,该空行仅包括一对回车换行符(<CR><LF>)。如果一次读操作没有读入HTTP请求的整个头部,即没有遇到空行,那么我们必须等待客户继续写数据并再次读入。因此,我们每完成一次读操作,就要分析新读入的数据中是否有空行。不过在寻找空行的过程中,我们可以同时完成对整个HTTP请求头部的分析,以提高解析HTTP请求的效率。

/*
    解析客户端请求时,主状态机的状态:
    主状态机的三种可能状态,分别表示:
    当前正在分析请求行:CHECK_STATE_REQUESTLINE
    当前正在分析头部字段:CHECK_STATE_HEADER
    当前正在解析请求体:CHECK_STATE_CONTENT
*/
enum CHECK_STATE
{
    CHECK_STATE_REQUESTLINE = 0,
    CHECK_STATE_HEADER,
    CHECK_STATE_CONTENT
};

/*
    从状态机的三种可能状态,即行的读取状态,分别表示:
    读取到一个完整的行:LINE_OK
    行出错:LINE_BAD
    行数据尚且不完整:LINE_OPEN
*/
enum LINE_STATUS {
    LINE_OK = 0,
    LINE_BAD,
    LINE_OPEN
};

/*
    服务器处理HTTP请求的可能结果,报文解析的结果:
    表示请求不完整,需要继续读取客户数据:NO_REQUEST
    表示获得了一个完整的客户请求:GET_REQUEST
    表示客户请求有语法错误:BAD_REQUEST
    表示客户对资源没有足够的访问权限:FORBINNEN_REQUEST
    表示服务器内部错误:INTERNAL_ERROR
    表示客户端已经关闭连接了:CLOSED_CONNECTION
*/
enum HTTP_CODE {
    NO_REQUEST=0,
    GET_REQUEST,
    BAD_REQUEST,
    FORBINNEN_REQUEST,
    INTERNAL_ERROR,
    CLOSED_CONNECTION
};

4.HTTP请求报文格式

GET / HTTP/1.1
Host: www.baidu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: BAIDUID=6729CB682DADC2CF738F533E35162D98:FG=1;
BIDUPSID=6729CB682DADC2CFE015A8099199557E; PSTM=1614320692; BD_UPN=13314752;
BDORZ=FFFB88E999055A3F8A630C64834BD6D0;
__yjs_duid=1_d05d52b14af4a339210722080a668ec21614320694782; BD_HOME=1;
H_PS_PSSID=33514_33257_33273_31660_33570_26350;
BA_HECTOR=8h2001alag0lag85nk1g3hcm60q
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0

HTTP请求由 请求行(request line)、请求头(header)、空行和请求数据四个部分组成。

请求行:用来说明请求类型,要访问的资源以及所使用的HTTP版本。

请求方法:

GET:向指定资源发出“显示”请求。使用GET方法应该只用在读取数据,而不应该被用于产生“副作用”的操作中。

POST:向指定资源提交数据,请求服务器处理(例如提交表单或者上传文件)。数据被包含在请求文本中。这个请求可能会创建新的资源或者修改现有资源,或两者皆有。

请求头部:紧接着请求行(第一行)之后的部分,用来说明服务器要使用的附加信息。

HOST:给出请求资源所在服务器的域名。

User-Agent:HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。

Accept:说明用户代理可处理的媒体类型。

Accept-Encoding:说明用户代理支持的内容编码。

Accept-Language:说明用户代理能够处理的自然语言集。

Content-Type:说明实现主体的媒体类型。

Content-Length:说明实现主体的大小。

Connection:连接管理,可以是Keep-Alive或close。

空行:请求头部后面的空行是必须的,即使第四部分的请求数据为空 ,也必须有空行。

请求数据:也叫主体,可以添加任意的其他数据。

HTTP状态码

所有HTTP响应的第一行都是状态行,依次是当前HTTP版本号,3位数字组成的状态代码,以及描述状态 的短语,彼此由空格分隔。 状态代码的第一个数字代表当前响应的类型:

1XX : 信息类状态码(表示接收请求状态处理)

2XX : 成功状态码(表示请求正常处理完毕)

3XX : 重定向(表示需要进行附加操作,已完成请求)

4XX : 客户端错误(表示服务器无法处理请求)

5XX : 服务器错误状态码(表示服务器处理请求的时候出错

5.HTTP类

浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应的buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。

工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行解析。

解析完成之后,跳转do_request函数生成响应报文,通过process_write写入buffer,返回给浏览器。

class http_conn
{
public://文件名的最大长度static const int FILENAME_LEN = 200;//读缓冲区的大小static const int READ_BUFFER_SIZE = 2048;//写缓冲区的大小static const int WRITE_BUFFER_SIZE = 1024;//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_CONNTENT,};//服务器处理HTTP请求的可能结果enum HTTP_CODE {NO_REQUEST,GET_REQUEST,BAD_REQUEST,NO_RESQUACE,FORBIDDEN_REQUEST,FILE_REQUEST,INTERNAL_ERROR,CLOSED_CONNECTION,   };//行的读取状态enum LINE_STATUS {LINE_OK=0,LINE_BAD,LINE_OPEN};public:http_conn() {};~http_conn() {};public://初始化新接受的连接void init(int sockfd, const sockaddr_in&addr);//关闭连接void close_conn(bool real_close=true);//处理客户请求void process();//非阻塞读操作bool read();//非阻塞写操作bool write();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();public://所有socket上的事件都被注册到同一个epoll内核时间表中,所以将epoll文件描述符设置为静态的static int m_epollfd;//统计用户数量static int m_user_count;private://该HTTP连接的socket和对方的socket地址int m_sockfd;sockaddr_in m_address;//读缓冲区char m_read_buf[READ_BUFFER_SIZE];//标识读缓冲区中已经读入的客户数据的最后一个字节的下一个位置int m_read_idx;//当前正在分析的字符再读缓冲区中的位置int m_check_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.1char* m_version;//主机名char* m_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;};

6.EPOLL函数

IO多路复用函数有select、poll和epoll。这里我们主要使用多路IO复用的epoll函数来实现服务器的并发处理。

#include <sys/epoll.h>

epoll_create函数

int epoll_create(int size)

// 创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检 测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向 链表)。

int epoll_create(int size);

- 参数:

size : 目前没有意义了。随便写一个数,必须大于0

- 返回值:

-1 : 失败

> 0 : 文件描述符,操作epoll实例的

epoll_ctl函数

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

- 参数:

- epfd : epoll实例对应的文件描述符

- op : 要进行什么操作

EPOLL_CTL_ADD: 添加

EPOLL_CTL_MOD: 修改

EPOLL_CTL_DEL: 删除

- fd : 要检测的文件描述符

- event : 检测文件描述符什么事情

上述event是epoll_event结构体指针类型,表示内核所监听的事件,具体定义如下:struct epoll_event {

__uint32_t events; /* Epoll events */

epoll_data_t data; /* User data variable */

}

epoll_data_t是一个联合类型,定义如下:

typefd union epoll_data

{

void *ptr;

int fd;

uint32_t u32;

uint64_t u64;

}epoll_data_t;

event描述事件:

EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
        EPOLLOUT:表示对应的文件描述符可以写;
        EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
        EPOLLERR:表示对应的文件描述符发生错误;
        EPOLLHUP:表示对应的文件描述符被挂断;
        EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
        EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

EPOLLRDHUP:对端描述符产生一个挂断事件

epoll_wait函数

// 检测函数

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

- 参数: - epfd : epoll实例对应的文件描述符

- events : 传出参数,保存了发送了变化的文件描述符的信息

- maxevents : 第二个参数结构体数组的大小

- timeout : 阻塞时间 - 0 : 不阻塞 - -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞 - > 0 : 阻塞的时长(毫秒)

- 返回值:

- 成功,返回发送变化的文件描述符的个数 > 0

- 失败 -1

设置非阻塞模式:


//对文件描述符设置非阻塞
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;
}

条件触发(LT)和边沿触发(ET):

ET:当epoll_wait检测到fd上有事件发生并将此事件通知给应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。

LT:与ET相反,会不断通知,直到处理此事件。

EPOLLONESHOT事件:

一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket。

我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件。

6.实现HTTP类

在实现HTTP类的过程中,我们还需要调用一些函数和结构体去完成这个工作。

stat函数

stat函数用于取得指定文件的文件属性,并将文件属性存储在结构体stat里,这里仅对其中用到的成员进行介绍。

#include <sys/stat.h>

//获取文件属性,存储在statbuf中

int stat(const char *pathname, struct stat *statbuf);

struct stat

{

mode_t    st_mode;        /* 文件类型和权限 */

off_t     st_size;        /* 文件大小,字节数*/

}

writev函数

writev函数用于在一次函数调用中写多个非连续缓冲区,有时也将这该函数称为聚集写。

#include <sys/uio.h>

ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);

filedes表示文件描述符

iov为前述io向量机制结构体iovec

iovcnt为结构体的个数

这个函数一般需要和下面的结构一起使用。

iovec

定义了一个向量元素,通常,这个结构用作一个多元素的数组。

struct iovec {

void      *iov_base;      /* starting address of buffer */

size_t    iov_len;        /* size of buffer */

};

iov_base指向数据的地址

iov_len表示数据的长度

这个结构体通常用来当作缓冲区用来写入、读出数据。

va_list函数

VA_LIST 是在C语言中解决变参问题的一组宏,变参问题是指参数的个数不定,可以是传入一个参数也可以是多个;可变参数中的每个参数的类型可以不同,也可以相同;可变参数的每个参数并没有实际的名称与之相对应,用起来是很灵活。

VA_LIST的用法:      
       (1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针;
       (2)然后用VA_START宏初始化变量刚定义的VA_LIST变量;
       (3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数);
       (4)最后用VA_END宏结束可变参数的获取。

vsnprintf函数

vsnprintf用来将可变参数格式化输出到一个字符数组,常和va_start和va_end一起使用。

函数功能:将可变参数格式化输出到一个字符数组。

头文件:#include <stdarg.h>

函数声明:int vsnprintf(char *str, size_t size, const char *format, va_list ap);

参数:

str

保存输出字符数组的存储区。

size

存储区的大小。

format

包含格式字符串的C字符串,其格式字符串与printf中的格式相同

arg

变量参数列表,用va_list 定义。

#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 = "Your request have permission to get file from this server.\n";
const char* error_404_title = "Not Found";
const char* error_404_form = "The request file was not found on this server.\n";
const char* error_500_title = "Internal Error";
const char* error_500_form = "There was unusual problem serving the request 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;
}// 向epoll中添加需要监听的文件描述符,并判断是否触发EPOLL的EPOLLONESHOT事件
void addfd(int epollfd, int fd, bool one_shot)
{epoll_event event;event.data.fd = fd;event.event = EPOLLIN | EPOLLET | EPOLLRDHUP;if (one_shot){event.events |= EPOLLONESHOT;}epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}// 从epoll中移除监听的文件描述符
void removefd(int epollfd, int fd)
{epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);close(fd);
}// 修改文件描述符,重置socket上的EPOLLONESHOT事件,以确保下一次可读时,EPOLLIN事件能被触发
void modfd(int epollfd, int fd, int ev)
{epoll_event event;event.data.fd = fd;event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;epoll(epollfd, EPOLL_CTL_MOD, fd, &event);
}// 所有的客户数
int http_conn::m_user_count = 0;// 所有socket上的事件都被注册到同一个epoll内核事件中,所以设置成静态的
int http_conn::m_epollfd = -1;// 关闭连接
void http_conn::close_conn(bool real_close)
{if (real_close && (m_sockfd != -1)){removefd(m_epollfd, m_sockfd);m_sockfd = -1;m_user_count--;//关闭一个连接时,将客户数量-1}
}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));m_user_count++;init();
}void http_conn::init()
{//主状态机当前正在分析请求行m_check = CHECK_STATE_REQUEST;//HTTP请求是否要求保持连接,默认不保持链接m_linger = false;// HTTP请求方式,默认请求方式为GETm_method = GET;//客户请求的目标文件的文件名,默认为0m_url = 0;//HTTP协议版本号,默认为0m_version = 0;//HTTP请求的消息体长度,默认为0m_content_length = 0;//主机名,默认为0m_host = 0;//当前正在解析的行的起始位置,默认为0m_start_line = 0;//当前正在分析的字符在读缓冲区的位置,默认为0m_check_idx = 0;//标识读缓冲区中已经读入的客户数据的最后一个字节的下一个位置,默认为0m_read_idx = 0;//写缓冲区中待发送的字节数,默认为0m_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);}//从状态机,解析一行,判断依据\r\n
http_conn::LINE_STATUS http_conn::parse_line()
{char temp;/*当前正在分析的字符在读缓冲区的位置,默认为0m_check_idx = 0;标识读缓冲区中已经读入的客户数据的最后一个字节的下一个位置,默认为0m_read_idx = 0*/for (; m_check_idx < m_read_idx; ++m_checked_idx){temp = m_read_buf[m_check_idx];if (temp == '\r'){//下一个字符达到了buffer结尾,则接收不完整,需要继续接收if ((m_check_idx + 1) == m_read_idx){return LINE_OPEN;//行数据不完整,因为m_read_idx表示客户数据的最后一个字节的下一个位置}//下一个字符是\n,将\r\n改为\0\0else if (m_read_buf[m_check_idx + 1] == '\n'){m_read_buf[m_checked_idx++] = '\0';m_read_buf[m_checked_idx++] = '\0';return LINE_OK;//读取到一个完整的行,并将这一行的\r\n变为\0,即结束符,结束这一行}return LINE_BAD;//都不符合,行出错}//如果当前字符是\n,也有可能读取到完整行//一般是上次读取到\r就到buffer末尾了,没有接收完整,再次接收时会出现这种情况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_check_idx++] = '\0';}return LINE_BAD;}}//并没有找到\r\n,需要继续接收    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版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char* text)
{   //例如GET /index.html HTTP/1.1//请求行中最先含有空格和\t任一字符的位置并返回m_url = strpbrk(text, "\t");if (!m_url){return BAD_REQUEST;}//例如GET\0/index.html HTTP/1.1*m_url++ = '\0';char* method = text;if (strcasecmp(method, "GET") == 0){m_method = GET;}else{return BAD_REQUEST;}//m_url此时跳过了第一个空格或\t字符,但不知道之后是否还有//将m_url向后偏移,通过查找,继续跳过空格和\t字符,指向请求资源的第一个字符m_url += strspn(m_url, "\t");//使用与判断请求方式的相同逻辑,判断HTTP版本号// /index.html HTTP/1.1m_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;}// 目前m_url为/index.html\0HTTP/1.1if (strncasecmp(m_url, "http://", 7) == 0){//http://192.168.110.129:10000/index.html这种情况下m_url += 7;m_url = strchr(m_url, '/');//找/第一次出现的位置,现在m_url为/index.html}if (!m_url || m_url[0] != '/'){return BAD_REQUEST;}//请求行处理完毕,将主状态机转移处理请求头m_check_state = CHECK_STATE_HEADER;return NO_REQUEST;//请求不完整,需要继续读取客户数据
}/*GET / HTTP/1.1Host: www.baidu.comUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding: gzip, deflate, brConnection: keep-aliveCookie: BAIDUID=6729CB682DADC2CF738F533E35162D98:FG=1;BIDUPSID=6729CB682DADC2CFE015A8099199557E; PSTM=1614320692; BD_UPN=13314752;BDORZ=FFFB88E999055A3F8A630C64834BD6D0;__yjs_duid=1_d05d52b14af4a339210722080a668ec21614320694782; BD_HOME=1;H_PS_PSSID=33514_33257_33273_31660_33570_26350;BA_HECTOR=8h2001alag0lag85nk1g3hcm60qUpgrade-Insecure-Requests: 1Cache-Control: max-age=0*///解析HTTP请求的头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char* text)
{//遇到空行,表示头部字段解析完毕if (text[0] == '\0'){//如果HTTP请求有消息体,则还需要读取m_content_length字节的消息体,状态机转移到CHECK_STATE_CONTENT状态//HTTP请求的消息体长度,默认为0//m_content_length = 0;if (m_content_length != 0){m_check_state = CHECK_STATE_CONTENT;return NO_REQUEST;}//否则说明我们已经得到了一个完整的HTTP请求return GET_REQUEST;}//处理Connection头部字段//Connection: keep-aliveelse if (strncasecmp(text, "Connection:", 11) == 0){text += 11;text += strspn(text, " \t");if (strcasecmp(text, "keep-alive") == 0){//如果是长连接,则将linger标志设置为truem_linger = true;}}//处理Content-Length头部字段(解析请求头部内容长度字段)else if (strncasecmp(text, "Content-Length", 15) == 0){text += 15;text += strspn(text, "\t");m_content_lengtg = atol(text); //把字符串转换成长整型数}//解析Host头部字段else if (strncasecmp(text, "Host", 5) == 0){text += 5;text += strspn(text, "\t");m_host = text;//主机名}else{printf("oop!unknow headr:%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;
}//m_start_line当前正在解析的行的起始位置,将该位置后面的数据赋给text
//此时从状态机已提前将一行的末尾字符\r\n变为\0\0,所以text可以直接取出完整的行进行解析
char* get_line()
{return m_read_buf + m_start_line;
}//主状态机,解析请求
http_conn::HTTP_CODE http_conn::process_read()
{//初始化从状态机状态、HTTP请求解析结果LINE_STATUS line_status = LINE_OK;HTTP_CODE ret = NO_REQUEST;char* text = 0;//parse_line为从状态机的具体实现while (((m_check_state == CHECK_STATE_CONTENT) && (line_status == LINE_OK)) || ((line_status == parse_line()) == LINE_OK)){text = get_line();//m_start_line是每一个数据行在m_read_buf中的起始位置// m_checked_idx表示从状态机在m_read_buf中读取的位置m_start_line = m_checked_idx;printf("got 1 http line:%s\n", text);//主状态机的三种状态转移逻辑switch (m_check_idx){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;}case:CHECK_STATE_CONTENT:{//解析消息体ret = parse_content(text);//完整解析POST请求后,跳转到报文响应函数if (ret == GET_REQUEST){return do_request();}//解析完消息体即完成报文解析,避免再次进入循环,更新line_statusline_status = LINE_OPEN;break;}default:{return INTERNAL_ERROR;}}}return NO_REQUSET;
}/*当得到一个完整、正确的HTTP请求时,我们就分析目标文件的属性。如果
目标文件存在,对所有用户可读,且不是目录,则使用mmap将其映射到内存地
址m_file_address处,并告诉调用者获取文件成功
*///doc_root = "/var/www/html";
//struct stat m_file_stat。目标文件的状态。通过它我们可以判断文件是否存在、是否为目录、是否可读,并获取文件大小等信息
http_conn::HTTP_CODE http_conn::do_request()
{//初始化的m_real_file赋值为网站根目录strcpy(m_real_file, doc_root);int len = strlen(doc_root);strncp(m_real_file + len, m_url, FILENAME_LEN - len - 1);// 获取m_real_file文件的相关的状态信息,-1失败,0成功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);// 客户请求的目标文件被mmap到内存中的起始位置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;//将要发送的字节(m_write_idx)写缓冲区中待发送的字节数int bytes_to_send = m_write_idx;if (bytes_to_send == 0){//将要发送的字节为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响应成功,根据HTTP请求中的Connection字段决定是否立即关闭连接unmap();if (m_linger){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;//将变量arg_list初始化为传入参数va_start(arg_list, format);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_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();
}//添加Content_Length,表示响应报文长度
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");
}//添加文本content
bool http_conn::add_content(const char* content)
{return add_response("%s", content);
}//添加文本类型,这里是html
bool http_conn::add_content_type() {return add_response("Content-Type:%s\r\n", "text/html");
}//根据服务器处理HTTP请求的结果,决定返回给客户端的内容
bool http_conn::process_write(HTTPCODE ret)
{switch (ret){//内部错误,500case 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:{//报文语法有误,404add_status_line(404, error_404_title);add_headers(strlen(error_404_form));if (!add_content(error_404_form))return false;break;}case NO_REQUEST:{//请求报文中存在语法错误add_status_line(400,error_400_title);add_headers(strlen(error_400_form));if (!add_content(error_400_form))return false;break}case FORBIDDEN_REQUEST:{//资源没有访问权限,403add_status_line(403, error_403_title);add_headers(strlen(error_403_form));if (!add_content(error_403_form))return false;break;}case FILE_REQUEST:{// 我们将采用writev来执行写操作,所以定义下面两个成员,其中m_iv_count表示被写内存块的数量//文件存在,200add_status_line(200, ok_200_title);//如果请求的资源存在if (m_file_stat.st.size1 = 0){add_headers(m_file_stat.st_size);//第一个iovec指针应当指向响应报文缓冲区,长度指向m_write_idx;m_iv[0].iov_base = m_write_buf;m_iv[0].iov_len = m_write_idx;//第二个iovec指针应当指向mmap返回的文件指针,长度指向文件大小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{//如果请求的资源大小为0,则返回空白的html文件const char* ok_string = "<html><body></body></html>";add_headers(strlen(ok_string));if (!add_content(ok_string)){return false;}}  }default:{return false;}}//除FILE_REQUEST状态外,其余状态只申请一个iovec,指向响应报文缓冲区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);//根据服务器处理HTTP请求的结果,决定返回给客户端的内容if (!write_ret){close_conn();}modfd(m_epollfd,m_sockfd,EPOLLOUT);
}

        从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。

这就是实现http_conn类的代码。通过代码我们可以发现,我们从客户端接收到请求,然后解析客户端的请求,解析请求主要是通过主从状态机来进行推动的。我们在主状态机内部调用从状态机,一步一步的了解客户端的需求。

7.main函数

#include <iostream>#include"locker"
#include"threadpool.h"
using namespace std;//最大文件描述符个数
#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;sigfillset(&sa.sa_mask);assert(sigaction(sig,&sa,NULL)!=-1);}void sho_error(int connfd,const char* info)
{printf("%s",info);send(connfd,info,strlen(info),0);close(connfd);
}int main(int argc,char* argv[])
{if (argv <= 2){printf("usage:%s ip_address port_number\n",basename(argv[0]));return 1;}//获得ip地址和port端口号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对象http_conn* users = new http_conn[MAX_FD];int listenfd = socket(PF_INET,SOCK_STREAM,0);assert(listenfd>=0);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);//创建epoll对象和事件数组epoll_event events[MAX_EVENT_NUMBER];int epollfd = epoll_event(5);//添加到epoll对象中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,&cilent_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()){uses[sockfd].close_conn();}}else{ }}}close(epollfd);close(listenfd);delete[]users;delete pool;return 0;
}

C++实现高并发Web服务器相关推荐

  1. nginx简介(轻量级开源高并发web服务器:大陆使用者百度、京东、新浪、网易、腾讯、淘宝等)(并发量5w)(一般网站apache够用了,而且稳定)...

    nginx简介(轻量级开源高并发web服务器:大陆使用者百度.京东.新浪.网易.腾讯.淘宝等)(并发量5w)(一般网站apache够用了,而且稳定) 一.总结 1.在连接高并发的情况下,Nginx是A ...

  2. 高并发WEB服务器设计

    目标:考虑设计一个高并发WEB服务器框架,有哪些惯用的手法?希望在本篇文章给出一个标准答案. 关键词: 迭代模型,并发模型,I/O多路复用,线程池,协程 参考:Apache/MySQL,Nginx, ...

  3. 高并发 WEB 服务器 nginx 源码通读中文分析注释,带详细函数注释及函数调用注释,附 github 地址,后期持续维护更新...

    github地址:https://github.com/y123456yz/reading-code-of-nginx-1.9.2 对源码通读注释,函数进行了详细注释.同时对相关功能进行了优化,日志功 ...

  4. aiohttp 高并发web服务_UE4 unity3d连接高并发http服务器

    1.HTTP服务器使用c++语言开发,vs2019编辑器.支持windows和linux.服务器主要用于游戏服务器后台,以及APP 应用后台增.删.查.改数据. 2.服务器使用多线程技术,以及使用线程 ...

  5. linux线程池实现多线程并发,基于Linux的多线程池并发Web服务器设计-电子设计工程.PDF...

    基于Linux的多线程池并发Web服务器设计-电子设计工程.PDF 第 卷 第 期 电子设计工程 年 月 基于 的多线程池并发 服务器设计 陈 涛 任海兰 武汉邮电科学研究院 湖北 武汉 摘要 时至今 ...

  6. aiohttp 高并发web服务_【Python入门】50.异步IO之 asyncio实现异步操作

    摘要:如何通过asyncio实现异步IO:用aiohttp模块编写支持多用户高并发的服务器. *写在前面:为了更好的学习python,博主记录下自己的学习路程.本学习笔记基于廖雪峰的Python教程, ...

  7. PHP_MVC框架开发,nosql学习,mysql优化,以及高并发web架构处理

    文章目录 定义入口文件 完成自动加载 路由类 .htaccess explode trim array_slice 实现 控制器 加载控制器 数据库 初始化连接 数据查询函数 视图 extract 其 ...

  8. 搞懂分布式技术30:高并发解决方案——提升高并发量服务器性能解决思路

    高并发解决方案--提升高并发量服务器性能解决思路 一个小型的网站,可以使用最简单的html静态页面就实现了,配合一些图片达到美化效果,所有的页面均存放在一个目录下,这样的网站对系统架构.性能的要求都很 ...

  9. 用IIS建立高安全性Web服务器

    用IIS建立高安全性Web服务器 因为IIS(即Internet Information Server)的方便性和易用性,使它成为最受欢迎的Web服务器软件之一.但是,IIS的安全性却一直令人担忧.如 ...

最新文章

  1. vbs之CurrentDirectory
  2. 转载:Linux下查看/修改系统时区、时间
  3. HDU 2050 折线分割平面
  4. 21个令程序员泪流满面的瞬间【第二弹】
  5. python函数定义和调用_函数定义和调用
  6. codeforces C. Bits(数学题+或运算)
  7. css 浮动效果 0302
  8. 【英语学习】【Daily English】U06 Shopping L01 We are out of pasta.
  9. python利用微信的方法_作为程序员,如何利用Python玩转微信,让你的微信与众不同...
  10. spring框架总结
  11. Jquery 中each循环嵌套的使用示例教程
  12. matlab fisher z变换,科学网—胜过 Fisher z 变换!(2) - 杨正瓴的博文
  13. 零点存在定理与介值定理
  14. 基于h5的航空订票系统的设计与实现
  15. mysql 字符串截取,拼接
  16. Day03-JavaScript01
  17. rsync+sersync实时同步数据
  18. 添加编译警告的另一种方式:开发 clang
  19. 1024短信盲盒 | 暖心短信陪你过节,还有更多好礼
  20. 中国移动应停止新增WiFi热点并实施全免费策略

热门文章

  1. 采用蒙特卡罗方法生成zipf分布随机数据
  2. 串级PID 位置环+速度环
  3. java xmap_转:使用XMAP完成JavaBean和XML之间转换
  4. java与软件测试哪个好?
  5. SPI通信协议详解,一篇就够!
  6. oracle rac mpp,DB2 purescale vs Oracle RAC
  7. 小程序重构 [cnode社区]:mpvue + 开源api,现已上线!
  8. C语言,一个分号引发的问题:Run-Time Check Failure #2 - Stack around the variable ‘class1‘ was corrupted.
  9. 面试中常见的50个面试题目及答案
  10. 市场调研报告-全球与中国燃油泄漏检测设备市场现状及未来发展趋势