基于Proactor的c++Web服务器项目
- WebServer项目(四)->(基于Proactor的c++)Web服务器简介及简单实现
- 1.Web Server(网页服务器)
- 2.HTTP协议(应用层的协议)
- 3.HTTP 请求报文格式
- 4.HTTP响应报文格式
- 5.HTTP请求方法
- 6.HTTP状态码
- 7.服务器编程基本框架
- 8.两种高效的事件处理模式
- 同步 I/O 方式如何模拟出 Proactor 模式?
- 9.线程池
- 10.有限状态机
- 11.EPOLLONESHOT事件
- 12.服务器压力测试
WebServer项目(四)->(基于Proactor的c++)Web服务器简介及简单实现
1.Web Server(网页服务器)
Web Server(网页服务器)是一种软件程序,用于接收和处理来自客户端浏览器的HTTP请求,并向客户端浏览器提供HTML文档、图像、CSS和JavaScript等Web资源。
Web Server通常运行在服务器操作系统上,监听指定的端口,等待客户端浏览器发起HTTP请求。当收到HTTP请求后,Web Server会根据请求的URL路径找到对应的资源,然后将该资源发送给客户端浏览器。Web Server还可以处理一些特殊的HTTP请求,比如CGI请求、ASP.NET请求等。
Web Server的主要功能包括:
- 监听网络端口,等待客户端浏览器的连接请求。
- 解析HTTP请求,找到请求的资源,并返回HTTP响应。
- 处理CGI请求或其他特殊的HTTP请求。
- 支持文件上传和下载。
- 支持SSL加密,保护Web应用程序的安全性。
常见的Web Server包括Apache、Nginx、Microsoft IIS等。这些Web Server都提供了丰富的功能和插件,可以满足不同的Web应用程序的需求。
2.HTTP协议(应用层的协议)
HTTP协议(超文本传输协议,Hypertext Transfer Protocol)是一种应用层协议,用于在Web浏览器和Web服务器之间进行通信。HTTP协议定义了Web浏览器和Web服务器之间传输数据的格式和规则。
HTTP协议的主要特点包括:
- 简单性:HTTP协议的基本规则和格式非常简单,易于学习和实现。
- 无状态性:HTTP协议是一种无状态协议,每个请求和响应都是独立的,服务器不会保存客户端请求的任何信息。
- 可扩展性:HTTP协议可以通过添加新的头部字段来扩展其功能。
- 支持多媒体:HTTP协议支持传输各种类型的数据,包括文本、图像、音频、视频等多媒体数据。
HTTP协议的基本工作流程如下:
- 客户端浏览器向Web服务器发送HTTP请求。
- Web服务器接收到HTTP请求,解析请求的URL,找到对应的资源,并生成HTTP响应。
- Web服务器将HTTP响应发送回客户端浏览器。
- 客户端浏览器接收到HTTP响应,解析响应数据,并根据响应数据更新页面内容。
HTTP协议的版本包括HTTP/1.0、HTTP/1.1、HTTP/2等版本。HTTP/1.1是目前最广泛使用的版本,HTTP/2则是一种新的协议,旨在提高Web性能和安全性。
3.HTTP 请求报文格式
HTTP 请求报文是由客户端浏览器发送给Web服务器的数据,其格式如下:
HTTP Method URL HTTP Version
Header1: Value1
Header2: Value2
...
HeaderN: ValueN
Request Body
其中,各个部分的含义如下:
- HTTP Method:HTTP请求方法,如GET、POST等。
- URL:请求的资源的URL地址。
- HTTP Version:HTTP协议的版本,如HTTP/1.1。
- Header:HTTP请求头部,包括一些键值对,用于描述请求的一些详细信息,如User-Agent、Accept等。
- Request Body:HTTP请求体,用于传输请求参数等数据。
下面是一个示例HTTP请求报文:
POST /login HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
username=admin&password=123456
在上述示例中,HTTP方法为POST,请求的URL为/login,HTTP版本为HTTP/1.1。请求头部包括Host、User-Agent、Accept、Content-Type和Content-Length等字段。请求体为username=admin&password=123456,表示向服务器提交了用户名和密码参数。
需要注意的是,HTTP请求报文中的换行符必须是“\r\n”,即回车符和换行符的组合。此外,请求头部和请求体之间必须有一个空行。
4.HTTP响应报文格式
HTTP响应报文是由Web服务器发送给客户端浏览器的数据,其格式如下:
HTTP Version Status Code Reason Phrase
Header1: Value1
Header2: Value2
...
HeaderN: ValueN
Response Body
其中,各个部分的含义如下:
-
HTTP Version:HTTP协议的版本,如HTTP/1.1。
-
Status Code:HTTP响应状态码,用于表示服务器处理请求的结果,如200表示成功,404表示资源未找到,500表示服务器内部错误等。
-
Reason Phrase:HTTP响应状态码的描述,用于描述Status Code的含义,例如200的Reason Phrase为OK。
-
Header:HTTP响应头部,包括一些键值对,用于描述响应的一些详细信息,如Content-Type、Content-Length等。
-
Response Body:HTTP响应体,用于传输响应的数据,如HTML文档、图片等。
下面是一个示例HTTP响应报文:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1274
<!DOCTYPE html>
<html>
<head>
<title>Example Website</title>
<meta charset="utf-8">
...
</head>
<body>
...
</body>
</html>
在上述示例中,HTTP版本为HTTP/1.1,状态码为200,Reason Phrase为OK。响应头部包括Content-Type和Content-Length等字段。响应体为一个HTML文档。
需要注意的是,HTTP响应报文中的换行符必须是“\r\n”,即回车符和换行符的组合。响应头部和响应体之间必须有一个空行。
5.HTTP请求方法
HTTP请求方法是指客户端浏览器向Web服务器发送请求时所使用的HTTP协议定义的方法。HTTP协议定义了多种请求方法,常用的有以下几种:
-
GET:用于请求指定资源,通常用于获取Web页面、图片、文本等静态内容。
-
POST:用于向指定资源提交数据,通常用于提交表单数据、上传文件等。
-
PUT:用于向指定资源位置上传新的内容。
-
DELETE:用于请求服务器删除指定的资源。
-
HEAD:与GET方法类似,但是只返回响应头部,不返回响应体。
-
OPTIONS:请求Web服务器告知可用的请求方法和其他一些选项信息。
-
TRACE:请求Web服务器返回自己收到的请求信息,用于测试或诊断。
-
CONNECT:HTTP/1.1协议中定义的方法,用于建立与目标资源的双向通信隧道。
其中,GET和POST方法是最常用的两种请求方法。GET方法用于获取资源,只能传递少量的数据,而POST方法用于提交数据,可以传递更多的数据。
需要注意的是,不同的HTTP请求方法对应不同的语义和用途,使用不当可能会导致安全问题或其他错误。因此,在使用HTTP请求方法时需要仔细考虑其用途和限制条件。
6.HTTP状态码
HTTP状态码是Web服务器向客户端浏览器返回的HTTP响应中的一个三位数的数字代码,用于表示服务器处理请求的结果。
HTTP状态码的分类如下:
-
1xx(信息性状态码):表示服务器已经接收到请求,但是尚未处理。
-
2xx(成功状态码):表示服务器已经成功处理了请求。
- 200 OK:请求成功,服务器返回的响应信息包含在响应体中。
- 204 No Content:请求成功,但是服务器没有返回任何响应体。
- 206 Partial Content:请求成功,但是只返回了部分响应体。
-
3xx(重定向状态码):表示需要客户端浏览器进行额外的操作才能完成请求。
- 301 Moved Permanently:请求的URL已经被永久移动到了新的位置,浏览器应该使用新的URL进行访问。
- 302 Found:请求的URL已经被临时移动到了新的位置,浏览器应该使用新的URL进行访问。
- 304 Not Modified:客户端浏览器的缓存版本与服务器上的版本相同,不需要重新传输请求内容。
-
4xx(客户端错误状态码):表示客户端浏览器发送的请求存在错误或无法完成请求。
- 400 Bad Request:请求存在语法错误或无法被服务器理解。
- 401 Unauthorized:请求需要身份验证,但是客户端没有提供合法的身份凭证。
- 403 Forbidden:请求被服务器拒绝,通常是因为客户端没有访问该资源的权限。
- 404 Not Found:请求的资源在服务器上不存在。
-
5xx(服务器错误状态码):表示服务器无法完成请求。
- 500 Internal Server Error:服务器内部错误,无法完成请求。
- 502 Bad Gateway:服务器作为网关或代理,从上游服务器接收到无效的响应。
- 503 Service Unavailable:服务器过载或正在维护,无法处理请求。
HTTP状态码是HTTP协议的重要组成部分,可以帮助客户端浏览器和开发人员诊断和解决问题。在开发Web应用程序时,需要了解不同的HTTP状态码及其含义,以便更好地处理和响应HTTP请求和响应。
7.服务器编程基本框架
服务器编程的基本框架通常包含以下几个部分:
-
网络通信模块:用于处理来自客户端的网络请求和向客户端发送网络响应。通常使用Socket API或者HTTP框架来实现。
-
请求解析和处理模块:用于解析客户端发送的请求,并根据请求的内容进行相应的处理。例如,对于HTTP请求,可以解析请求头部和请求体,并根据请求的内容来生成响应。
-
数据存储和管理模块:用于管理服务器上的数据,例如用户信息、网页内容等。通常使用数据库或者缓存来实现。
-
业务逻辑处理模块:用于实现服务器的业务逻辑,例如用户身份验证、数据处理、业务规则等。
-
Web框架和模板引擎:用于简化服务器编程的复杂度,提高开发效率。通常使用现有的Web框架和模板引擎,例如Django、Flask等。
-
安全和性能优化模块:用于保障服务器的安全性和性能。例如,可以使用SSL/TLS协议来加密客户端和服务器之间的通信,使用CDN来加速静态资源的访问等。
8.两种高效的事件处理模式
在服务器编程中,常用的高效的事件处理模式包括 Reactor 模式和 Proactor 模式。
-
Reactor 模式
Reactor 模式是一种基于事件驱动的设计模式,它通过一个中心事件循环来接收和分发事件。当一个请求到达时,事件循环会将其分发给相应的处理器进行处理。处理器通过异步 I/O 操作来处理请求,避免了阻塞等待 I/O 完成的情况。Reactor 模式的优点在于它的高吞吐量和低延迟,适用于高并发的网络应用场景。 -
Proactor 模式
Proactor 模式是一种基于事件驱动的设计模式,它通过一个中心事件循环来接收和处理请求。
与 Reactor 模式不同的是:
Proactor 模式中的请求处理是由系统自动完成的,而不需要应用程序进行手动处理。当一个请求到达时,事件循环会将其分发给相应的处理器进行处理,处理器通过异步 I/O 操作来处理请求,并在 I/O 操作完成后向事件循环发送完成事件。Proactor 模式的优点在于它的高可靠性和高并发性,适用于需要处理大量 I/O 请求的应用场景,例如数据库访问和文件传输等。
总的来说,Reactor 模式和 Proactor 模式都是基于事件驱动的设计模式,它们通过异步 I/O 操作来避免阻塞等待 I/O 完成的情况,提高了服务器的性能和可靠性。选择哪种模式取决于具体的应用场景和需求。
同步 I/O 方式如何模拟出 Proactor 模式?
在同步 I/O 环境下,可以通过多线程来模拟异步 I/O 操作,从而实现 Proactor 模式。具体实现步骤如下:
-
创建一个线程池,用于处理请求。可以使用线程池模型来管理线程池,以便于控制线程的数量和复用线程资源。
-
当一个请求到达时,主线程将其加入请求队列中。
-
线程池中的线程从请求队列中获取请求,并使用同步 I/O 操作来处理请求。由于同步 I/O 操作会阻塞线程,因此线程池需要足够的线程数量来处理并发请求。
-
当 I/O 操作完成后,线程将请求的处理结果加入结果队列中,并向主线程发送处理完成事件。
-
主线程从结果队列中获取处理结果,并将其发送给客户端。
通过以上步骤,可以模拟出 Proactor 模式的效果,实现高并发的请求处理。但是需要注意的是,在同步 I/O 环境下,线程池的数量和线程的复用需要进行合理的规划,以避免线程过多或者线程资源浪费的情况。同时,还需要考虑线程安全和数据同步等问题,以保证程序的正确性和可靠性。
9.线程池
线程池是一种常用的并发编程技术,它通过预先创建一组线程来处理任务,从而避免了线程创建和销毁的开销,提高了程序的性能和响应速度。
线程池通常包含以下几个组成部分:
-
任务队列:用于存储待处理的任务,线程池中的线程从任务队列中获取任务并进行处理。
-
线程池管理器:用于管理线程池中的线程,包括线程创建、销毁、线程数量控制等。
-
工作线程:线程池中的线程,用于处理任务。
线程池的使用流程如下:
-
创建一个线程池,设置线程池的大小和任务队列的大小。
-
将任务加入任务队列中。
-
线程池中的线程从任务队列中获取任务,并进行处理。
-
处理完成后,线程将处理结果返回给主线程。
-
如果任务队列为空,线程会等待新的任务加入。
线程池的优点在于它可以避免线程的创建和销毁的开销,提高了程序的性能和响应速度。同时,线程池还可以控制线程的数量,避免线程数量过多导致的资源浪费和线程调度开销。线程池的缺点在于它需要占用一定的内存资源,而且线程池的设计和实现比较复杂,需要考虑线程安全和任务调度等问题。
// threadpool.h
// 写一个线程池,声明和定义就都写在这个头文件中了
#ifndef THREAD_POOL_H
#define THREAD_POOL_H
#include <pthread.h>
#include <list>
#include "locer.h"
#include <exception>
#include <cstdio>
// 线程池主要还是找一个线程去处理这个任务
// 为了更加通用,我们用模板表示任务类,为了让代码的复用
template <class T> // T就是任务类
class threadpool
{
public:
threadpool(int thread_number = 8, int max_requests = 1000);
~threadpool();
bool append(T *request); // 添加任务
private:
static void *worker(void *arg);
void run();
private:
// 线程的数量
int m_thread_number;
// 线程池数组,大小为m_thread_number
pthread_t *m_threads;
// 请求队列中最多允许的,等待处理的请求数量
int m_max_requests;
// 请求队列,list中装的是任务类list,使用list,模拟队列
std::list<T *> m_workqueue;
// 互斥锁
locker m_queuelocker;
// 信号量 用来判断是否有任务需要处理
sem m_queuestat;
// 是否结束线程
bool m_stop;
};
// inline可写可不写
template <class T>
inline 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 < m_thread_number; i++)
{
printf("create thread %d\n", i); // 正在创建第几个线程
int ret = pthread_create(&m_threads[i],
NULL,
worker, // 线程执行的函数,这个函数在c++中必须是静态的函数
(void *)this); // 给这个work传参
if (ret) // 线程创建失败
{
delete[] m_threads;
throw std::exception();
}
// 说明线程创建成功,设置线程分离
if (pthread_detach(m_thread[i]))
{
// 如果出错
delete[] m_threads; // 释放数组
throw std::exception(); // 抛异常
}
}
}
template <class T>
inline threadpool<T>::~threadpool()
{
delete[] m_threads;
m_stop = true;
}
template <class T>
inline bool threadpool<T>::append(T *request) // 添加任务T
{
// 保证线程同步
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 <class T>
inline void *threadpool<T>::worker(void *arg)
{
threadpool *pool = (threadpool *)arg; // 创建线程池
pool->run();
return pool;
}
template <class T>
inline 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 // THREAD_POOL_H
// locker.h
// 线程同步封装类
#ifndef LOCKER_H
#define LOCKER_H
#include <phread.h>
#include <exception>
#include <semaphore.h>
// 互斥锁类
class locker
{
public:
locker()
{
if (pthread_mutex_init(&m_mutex, NULL))
{
// perror("pthread_mutex_init");
throw std::exception();
}
}
~locker()
{
if (pthread_mutex_destroy(&m_mutex))
{
// perror("pthread_mutex_destroy");
throw std::exception();
}
}
// 上锁
bool lock()
{
return pthread_mutex_lock(&m_mutex) == 0;
}
// 解锁
bool unlock()
{
return pthread_mutex_unlock(&m_mutex) == 0;
}
// 获取成员
pthread_mutex_t *get()
{
return &m_mutex;
}
private:
phread_mutex_t m_mutex;
};
// 条件变量类
class cond
{
public:
cond()
{
if (pthread_cond_init(&m_cond, NULL))
{
// perror("pthread_cond_init");
throw std::exception();
}
}
~cond()
{
if (phread_cond_destroy(&m_cond))
{
// perror("pthread_cond_destroy");
throw std::exception();
}
}
bool wait(pthread_mutex_t *mutex)
{
return pthread_cond_wait(&m_cond, mutex) == 0; // 条件变量,互斥锁
}
bool timewait(pthread_mutex_t *mutex, struct timespec t)
{
return pthread_cond_timedwait(&m_cond, mutex, &t) == 0; // 条件变量,互斥
}
// 唤醒一个或多个
bool signal()
{
return pthread_cond_signal(&m_cond) == 0;
}
// 唤醒所有线程
bool broadcast()
{
return pthread_cond_broadcast(&m_cond) == 0;
}
private:
phread_cond_t m_cond;
};
// 信号量类
class sem
{
public:
sem()
{
if (sem_init(&m_sem, 0, 0))
{
// perror("sem_init");
throw std::exception();
}
}
sem(int num)
{
if (sem_init(&m_sem, 0, num))
{
// perror("sem_init");
throw std::exception();
}
}
~sem()
{
if (sem_destroy(&m_sem))
{
// perror("sem_destroy");
throw std::exception();
}
}
// 等待信号量
bool wait()
{
return sem_wait(&m_sem) == 0;
}
// 增加信号量
bool post()
{
return sem_post(&m_sem) == 0;
}
private:
sem_t m_sem;
};
#endif
10.有限状态机
有限状态机(Finite State Machine,FSM)是一种用于描述系统行为的数学模型。它由一组状态、一组输入和一组输出组成,其中状态表示系统的内部状态,输入表示系统的外部条件,输出表示系统对外部条件的响应。
在有限状态机中,系统的状态转移是通过输入事件触发的。当系统处于某一状态时,如果发生了输入事件,系统就会根据当前状态和输入事件进行状态转移,并产生相应的输出。这个过程可以用状态转移图或状态转移表来表示。
有限状态机广泛应用于计算机科学和工程领域,例如编译器、网络协议、自动控制系统等。在软件开发中,有限状态机可以帮助开发人员更好地理解和设计系统行为,从而提高系统的可靠性和性能。
有限状态机可以分为两种类型:确定性有限状态机(DFA)和非确定性有限状态机(NFA)。DFA中每个状态只有一条出边与一个输入相关联,而NFA中每个状态可以有多条出边与一个输入相关联。DFA具有更好的可预测性和可维护性,而NFA则具有更高的表达能力和灵活性。
11.EPOLLONESHOT事件
EPOLLONESHOT 是 Linux 下 epoll I/O 多路复用机制中的一种事件类型。当一个文件描述符上注册了 EPOLLONESHOT 事件后,它在被触发后只会被触发一次,除非重新设置 EPOLLONESHOT 事件。EPOLLONESHOT 事件的主要作用是确保一个文件描述符在任意时刻只被一个线程处理。
在使用 EPOLLONESHOT 事件时,需要注意以下几点:
-
需要使用 epoll_ctl 函数将文件描述符注册到 epoll 实例中,并设置 EPOLLONESHOT 事件。
-
当文件描述符上的 EPOLLONESHOT 事件被触发时,需要使用 epoll_ctl 函数重新设置 EPOLLONESHOT 事件,以确保下一次事件触发。
-
需要使用线程安全的方式处理文件描述符上的事件,以避免多个线程同时处理同一个文件描述符的事件。
在实际应用中,EPOLLONESHOT 事件通常用于多线程网络编程中,以确保每个文件描述符只被一个线程处理,避免多个线程同时处理同一个文件描述符的事件,提高程序的性能和稳定性。
12.服务器压力测试
Webbench 是一款基于 HTTP 协议的压力测试工具,可以模拟多个并发用户访问 Web 服务器,测试服务器的性能和稳定性。以下是使用 Webbench 进行服务器压力测试的示例:
- 安装 Webbench:
在 Ubuntu 系统中,可以使用以下命令安装 Webbench:
sudo apt-get install webbench
- 运行 Webbench:
运行 Webbench 的命令格式如下:
webbench -c 并发用户数 -t 测试时间 URL
其中,-c 选项表示并发用户数,-t 选项表示测试时间,URL 是要测试的 URL。
例如,要模拟 100 个并发用户访问 http://example.com/,测试时间为 10 秒,可以使用以下命令:
webbench -c 100 -t 10 http://example.com/
- 分析测试结果:
Webbench 执行完测试后,会输出测试结果,包括每个连接的响应时间、平均响应时间、吞吐量等指标。可以根据测试结果分析服务器的性能和稳定性,排查潜在的瓶颈和问题。
需要注意的是,Webbench 是一款比较古老的压力测试工具,不支持 HTTPS 协议,且测试结果可能不够准确,建议结合其他压力测试工具使用。
其他压力测试工具:
在 Linux 中,可以使用多种工具进行服务器压力测试,包括 Apache JMeter、ab、siege、wrk 等。