pr# Webserver组成部分
这个项目,粗略的看可以分为下面几个部分
- 建立socket通讯
- 服务器处理与客户端的IO
- 解析客户端的HTTP请求,并响应请求
建立socket通讯
Webserver服务器,肯定不可能只接收一个客户端的连接吧。所以这个项目是多线程并发同步执行的,而这之中就存在许多需要处理的细节,共享资源的访问,建立并维护线程池等。
locker.h
线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。
下面就创建一个线程同步机制封装类 —— locker.h
#ifndef LOCKER_H
#define LOCKER_H
#include <pthread.h>
#include <exception>
#include <semaphore.h>
//线程同步机制封装类
//互斥锁类
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;
}
pthread_mutex_t * get(){
return &m_mutex;
}
private:
pthread_mutex_t m_mutex;
};
// 信号量类
class sem{
public:
sem(){
if(sem_init(&m_sem,0,0)!=0){
throw std::exception();
}
}
sem(int num){
if(sem_init(&m_sem,0,num)!=0){
throw std::exception();
}
}
~sem(){
sem_destroy(&m_sem);
}
// 等待信号量
bool wait(){
return sem_wait(&m_sem)==0;
}
// 增加信号量
bool post(){
return sem_post(&m_sem)==0;
}
private:
sem_t m_sem;
};
#endif
线程池
为什么需要线程池?
线程的创建和销毁都是需要时间的。Web服务器在运行时,必然会频繁的创建和销毁线程,那线程池就是用来解决线程生命周期开销问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
线程池的组成部分:线程池管理器、工作线程、任务列队、任务接口等部分
这个项目,我们先创建线程池,任务类就先用模板来代替,template<typename T>
,使用模板或许还可以稍加修改就能在其他项目使用。
那线程池的成员,大致也能分析出来,有需要后面再补充
private:
// 线程的数量
int m_thread_number;
// 描述线程池的数组,大小为m_thread_number
pthread_t * m_threads;
// 请求队列中最多允许的、等待处理的请求的数量
int m_max_requests;
// 请求队列
std::list< T* > m_workqueue;
// 保护请求队列的互斥锁
locker m_queuelocker;
// 是否有任务需要处理
sem m_queuestat;
// 是否结束线程
bool m_stop;
接着写类的构造函数,析构函数,以及一个向任务队列添加任务的函数
public:
//thread_number是线程池中线程的数量,max_requests是请求队列中最多允许的、等待处理的请求的数量
threadpool(int thread_number = 8, int max_requests = 10000);
~threadpool();
bool append(T* request);
// 具体实现
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);
// worker函数必须是一个静态的成员函数
if(pthread_create(m_threads + i, NULL, worker, this ) != 0) {
delete [] m_threads;
throw std::exception();
}
// 成功返回 0,失败返回错误号
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;
}
// 工作线程运行的函数,它不断从工作队列中取出任务并执行之
private:
static void* worker(void* arg);
void run();
// 静态worker,不会有this指针
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();
}
}
pthread_create
函数的第四个参数,是第三个参数worker
函数的参数,但如果把worker
为类成员函数的话,而worker
其实还有一个隐藏的this
指针,那这里就create不成功。所以需要把worker
函数写为静态,将this指针去掉,但又会有另一个问题,就是worker
函数可能会用到threadpool的成员变量或函数,静态函数不能访问非静态成员吧。那解决办法就是,把worker
的参数,写为this指针,不就可以通过这个this访问了。
main()
函数
socket相关的代码就直接写在main()
函数中
#define MAX_FD 65536 // 最大的文件描述符个数
// 内联函数,小且需要多次调用
void perr(int ret,char* err){
if(ret == -1){
perror(err);
exit(-1);
}
}
// 函数指针handler,返回值类型是void,参数类型是int
void addsig(int sig, void( handler )(int)){
struct sigaction sa;
memset( &sa, '\0', sizeof( sa ) );
sa.sa_handler = handler;
sigfillset( &sa.sa_mask );
// 断言assert(expression)宏,当expression为假时,打印错误(表达式)并终止程序,否则无作用
// sigaction注册信号捕捉,当检测到sig信号,就执行sa对象中的handler
// 这里就是检查到SIGPIPE信号,但进行SIG_ING忽视
assert( sigaction( sig, &sa, NULL ) != -1 );
}
// argc是main函数的参数个数,argv[]数组是main函数的参数集合
int main( int argc, char* argv[] ) {
// 只要执行了程序,程序就会有一个参数,那就是程序所在的路径加上程序名
// 即argv[0] == ./webserver
// basename的功能是去掉argv[0]的路径,只留下文件名,有后缀也会去掉后缀
if( argc <= 1 ) {
printf( "usage: %s port_number\n", basename(argv[0]));
return -1;
}
// 这里不把webserver的端口写死,而是在启动程序是通过参数传入,也就是argv[1]
int port = atoi( argv[1] );
// 当客户端向服务器端程序发送了消息,然后关闭客户端
// 服务器端返回消息的时候就会收到内核给的SIGPIPE信号
// SIGPIPE信号会使服务器终止程序
addsig( SIGPIPE, SIG_IGN );
// 创建线程池,初始化线程池 http_conn就是任务类,这个稍后会写
threadpool< http_conn >* pool = NULL;
try {
pool = new threadpool<http_conn>;
} catch( ... ) {
return 1;
}
// 创建一个数组,用于保存所有客户信息。 这里又用到了http_conn类
// 本来应该要把客户信息,和任务方法等区分在不同的类中,但这里为了方便就写在一起了
// MAX_FD, 文件描述符值的最大值
http_conn* users = new http_conn[ MAX_FD ];
// 监听
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
int ret = 0;
struct sockaddr_in address;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_family = AF_INET;
address.sin_port = htons( port );
// 端口复用 在bind之前设置
int reuse = 1;
ret = setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );
perr(ret,(char *)"setsockopt");
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
perr(ret,(char *)"bind");
// 最大的连接数和未连接数之和是5
ret = listen( listenfd, 5 );
perr(ret,(char *)"listen");
// 接着就是服务器等待连接了 accetp
// ......
}
IO
创建epoll对象后,在内核会有一个红黑树,用来存放注册的文件描述符;还会有一个双链表,用来存放发生事件的文件描述符,然后返回给程序,程序就能知道哪些客户端有需要读写数据
创建了epoll对象,把监听的文件描述符添加到epoll对象中,当有新的连接进来,主线程就能检测到并进行accept;accept之后得到的socket文件描述符,也注册到epoll对象中,等待事件发生。
// 接上面的代码👆
#define MAX_EVENT_NUMBER 10000 // 监听的最大的事件数量
// 这两个函数在http_conn.cpp中实现,在http_conn类中也有用到,但并不写作成员函数
// 添加文件描述符 one_shot,用来防止多个线程对一个socket同时操作
extern void addfd( int epollfd, int fd, bool one_shot );
// 删除文件描述符
extern void removefd( int epollfd, int fd );
int main(int argc,char* argv[]){
// ......
// 创建epoll对象,和事件数组,添加
epoll_event events[ MAX_EVENT_NUMBER ];
// 参数是任意非0整数即可
int epollfd = epoll_create( 5 );
// 将监听的文件描述符添加到epoll对象的红黑树中
addfd( epollfd, listenfd, false );
// 所有socket事件都注册到同一个epoll对象上,所以m_epollfd将会设置成静态
http_conn::m_epollfd = epollfd;
while(true) {
// events数组,用来拷贝epollfd对象的双链表,即存放着发生读写的fd
int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
// EINTR 系统调用被中断
// epoll_wait默认阻塞,当系统收到信号可能中断epoll_wait去处理信号
// 处理完返回中断处,epoll_wait就会返回EINTR.这种情况不应该退出循环
if ( ( number < 0 ) && ( errno != EINTR ) ) {
printf( "epoll failure\n" );
break;
}
// 循环遍历发生事件的fd
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 ) {
close(connfd);
continue;
}
// 在init函数中,会将connfd注册到epollfd对象的红黑树中
users[connfd].init( connfd, client_address);
} else if( events[i].events & ( EPOLLRDHUP | EPOLLHUP | EPOLLERR ) ) {
// 客户端异常断开
// close_conn会把sockfd从epollfd对象的红黑树中移除,更新服务器连接总数等
users[sockfd].close_conn();
} else if(events[i].events & EPOLLIN) {
// 有客户端发送HTTP请求
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;
}
EPOLLONESHOT
先补充一些上面提到的http_conn类的成员和函数
http_conn.h
#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H
#include <sys/epoll.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>
#include "locker.h"
#include <sys/uio.h>
class http_conn{
public:
// 所有的socket上的事件都被注册到同一个epoll上
static int m_epollfd;
// 统计用户的数量
static int m_user_count;
http_conn();
~http_conn();
void process(); // 处理客户端的需求
// 初始化新接收的连接
void init(int sockfd,const sockaddr_in & addr);
// 关闭连接
void close_conn();
// 非阻塞 的读和写 epoll的ET模式必备的要求!!!!!!!!
bool read();
bool write();
private:
int m_sockfd; // 该HTTP连接的socket
sockaddr_in m_address; // 客户端socket
};
#endif
#include "http_conn.h"
// 所有的socket上的事件都被注册到同一个epoll上
int http_conn::m_epollfd = -1;
// 统计用户的数量
int http_conn::m_user_count = 0;
// 设置文件描述符非阻塞
void setnonblocking(int fd){
int old_flag = fcntl(fd,F_GETFL);
int new_flag = old_flag | O_NONBLOCK;
fcntl(fd,F_SETFL,new_flag);
}
// 向epoll中添加需要监听的文件描述符
void addfd(int epollfd,int fd,bool one_shot){
epoll_event event;
event.data.fd = fd;
// EPOLLRDHUP 对端异常断开,可以由事件判断
event.events = EPOLLIN | EPOLLRDHUP;
// 判断fd是否需要设置ONTSHOT
if(one_shot){
event.events |= EPOLLONESHOT;
}
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
// 要使用et模式的epoll,要将文件描述符设置成非阻塞
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 | EPOLLONESHOT | EPOLLRDHUP | EPOLLET;
event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&event);
}
// 初始化连接
void http_conn::init(int sockfd,const sockaddr_in & addr){
m_sockfd = sockfd;
m_address = addr;
// 设置一下端口复用
int reuse = 1;
setsockopt(m_sockfd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));
// 添加到epoll对象中
addfd(m_epollfd,m_sockfd,true);
m_user_count += 1; // 总用户数 + 1
}
// 关闭连接
void http_conn::close_conn(){
if(m_sockfd != -1){
removefd(m_epollfd,m_sockfd);
m_sockfd = -1;
m_user_count -= 1;
}
}
bool http_conn::read(){
printf("一次性读完\n");
return true;
}
bool http_conn::write(){
printf("一次性写完\n");
return true;
}
// 由线程池中的工作线程调用,这是处理HTTP请求的入口函数
void http_conn::process(){
// 解析HTTP请求
printf("parse request,create respone\n");
// 响应请求
}
http_conn
建立了socket通讯后,就是开始对HTTP请求报文的解析,以及响应请求
http_conn.h
class http_conn
{
public:
static const int FILENAME_LEN = 200; // 文件名的最大长度
static const int READ_BUFFER_SIZE = 4096; // 读缓冲区的大小
static const int WRITE_BUFFER_SIZE = 1024; // 写缓冲区的大小
private:
char m_read_buf[ READ_BUFFER_SIZE ]; // 读缓冲区
int m_read_idx; // 标识读缓冲区中已经读入的客户端数据的最后一个字节的下一个位置
}
http_conn.cpp
// 循环读取客户数据,直到无数据可读或者对方关闭连接
bool http_conn::read() {
if( m_read_idx >= READ_BUFFER_SIZE ) {
return false;
}
int bytes_read = 0;
// 因为是把epoll设置成边缘触发模式,所以需要一次把缓冲区中的数据读取完
while(true) {
// 从m_read_buf + m_read_idx索引出开始保存数据,大小是READ_BUFFER_SIZE - m_read_idx
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 ) {
// 非阻塞read,没有数据会返回这两个error,但这种情况不应该结束进程
break;
}
return false;
} else if (bytes_read == 0) { // 对方关闭连接
return false;
}
m_read_idx += bytes_read;
}
return true;
}
下面就开始解析HTTP请求报文
这是HTTP的请求报文,可以看到每一行最后都是以回车符+换行符作为结尾
那解析时,可以通过这一标识,来切换程序当前要解析的内容,行,头部,或是数据。
切换这一动作,就需要用到状态机。分别给不同部分定义状态,生成一个状态机,在编写代码时,就可以通过不同的状态来切换目前的解析任务。
GET / HTTP/1.1\r\n
Host: 192.168.11.128:8888
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
下面就开始解析HTTP请求报文
http_conn.h
class http_conn
{
public:
// HTTP请求方法,这里只支持GET
enum METHOD {GET = 0, POST, HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT};
/*
解析客户端请求时,主状态机的状态
CHECK_STATE_REQUESTLINE:当前正在分析请求行
CHECK_STATE_HEADER:当前正在分析头部字段
CHECK_STATE_CONTENT:当前正在解析请求体
*/
enum CHECK_STATE { CHECK_STATE_REQUESTLINE = 0, CHECK_STATE_HEADER, CHECK_STATE_CONTENT };
/*
服务器处理HTTP请求的可能结果,报文解析的结果
NO_REQUEST : 请求不完整,需要继续读取客户数据
GET_REQUEST : 表示获得了一个完成的客户请求
BAD_REQUEST : 表示客户请求语法错误
NO_RESOURCE : 表示服务器没有资源
FORBIDDEN_REQUEST : 表示客户对资源没有足够的访问权限
FILE_REQUEST : 文件请求,获取文件成功
INTERNAL_ERROR : 表示服务器内部错误
CLOSED_CONNECTION : 表示客户端已经关闭连接了
*/
enum HTTP_CODE { NO_REQUEST, GET_REQUEST, BAD_REQUEST, NO_RESOURCE,
FORBIDDEN_REQUEST, FILE_REQUEST, INTERNAL_ERROR, CLOSED_CONNECTION };
// 从状态机的三种可能状态,即行的读取状态,分别表示
// 1.读取到一个完整的行 2.行出错 3.行数据尚且不完整
enum LINE_STATUS { LINE_OK = 0, LINE_BAD, LINE_OPEN };
private:
int m_checked_idx; // 当前正在分析的字符在读缓冲区中的位置
int m_start_line; // 当前正在解析的行的起始位置
CHECK_STATE m_check_state; // 主状态机当前所处的状态
METHOD m_method; // 请求方法
private:
void init();
HTTP_CODE process_read(); // 解析HTTP请求
// 下面这一组函数被process_read调用以分析HTTP请求
HTTP_CODE parse_request_line( char* text ); // 解析请求行
HTTP_CODE parse_headers( char* text ); // 解析请求头部
HTTP_CODE parse_content( char* text ); // 解析请求体
char* get_line() { return m_read_buf + m_start_line; }
LINE_STATUS parse_line(); // 解析具体的报文的某一行
};
http_conn.cpp
// 初始化客户端的一些信息
void http_conn::init(int sockfd, const sockaddr_in& addr){
// ...
init();
}
// 初始化一些http_conn的成员。和上面的分开,是当前的init后面可能还会调用到
// 如果写到一起,后面调用每次都得传参,这就比较麻烦了
void http_conn::init()
{
m_check_state = CHECK_STATE_REQUESTLINE; // 初始状态为检查请求行
m_start_line = 0;
m_checked_idx = 0;
m_read_idx = 0;
}
// 主状态机,解析请求
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)) {
// 这里先看一下parse_line()函数
// 获取一行数据
// ......
}
// 解析一行,判断依据\r\n
http_conn::LINE_STATUS http_conn::parse_line() {
// GET /index.html HTTP/1.1\r\n
// 请求报文的状态行和响应头部,每一行结束都有\r\n
// 这就方便来解析每一行
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' ) {
// 将\r\n替换成字符串结束符'\0',再更新m_checked_idx
m_read_buf[ m_checked_idx++ ] = '\0';
m_read_buf[ m_checked_idx++ ] = '\0';
// 两次m_checked_idx++后,m_checked_idx指向下一行首字母H
// Host: 192.168.11.128:8888
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;
}
// 接着看process_read
http_conn::HTTP_CODE http_conn::process_read() {
// ...
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;
}
case CHECK_STATE_CONTENT: {
ret = parse_content( text );
if ( ret == GET_REQUEST ) {
return do_request();
}
line_status = LINE_OPEN;
break;
}
default: {
return INTERNAL_ERROR;
}
}
}
return NO_REQUEST;
}
// 解析HTTP请求行,获得请求方法,目标URL,以及HTTP版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char* text) {
// GET /index.html HTTP/1.1
m_url = strpbrk(text, " \t"); // 判断第二个参数中的字符哪个在text中最先出现
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;
}
// /index.html HTTP/1.1
// 检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标。
m_version = strpbrk( m_url, " \t" );
if (!m_version) {
return BAD_REQUEST;
}
*m_version++ = '\0';
if (strcasecmp( m_version, "HTTP/1.1") != 0 ) {
return BAD_REQUEST;
}
/**
* http://192.168.110.129:10000/index.html
*/
if (strncasecmp(m_url, "http://", 7) == 0 ) {
m_url += 7;
// 在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。
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 ) {
// 处理Connection 头部字段 Connection: keep-alive
text += 11;
text += strspn( text, " \t" );
if ( strcasecmp( text, "keep-alive" ) == 0 ) {
m_linger = true;
}
} else if ( strncasecmp( text, "Content-Length:", 15 ) == 0 ) {
// 处理Content-Length头部字段
// 请求体的长度
text += 15;
text += strspn( text, " \t" );
m_content_length = atol(text);
} else if ( strncasecmp( text, "Host:", 5 ) == 0 ) {
// 处理Host头部字段
text += 5;
text += strspn( text, " \t" );
m_host = text;
} else {
// 这里只判断了上面几种比较有用的头部信息,其余暂不处理
printf( "oop! unknow 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请求报文的’\r\n’为标志,区分报文的三个部分,再用有限状态机,切换程序解析不同内容的任务。当解析完后,就开始对请求做出响应了。
http_conn::HTTP_CODE http_conn::process_read() {
// ...
case CHECK_STATE_HEADER: {
if ( ret == BAD_REQUEST ) {
return BAD_REQUEST;
} else if ( ret == GET_REQUEST ) {
// 解析完请求头
return do_request();
}
break;
}
case CHECK_STATE_CONTENT: {
if ( ret == GET_REQUEST ) {
// 解析完请求体
return do_request();
}
// 有两种情况结束后就可以开始响应请求了
// 1.检查完请求头,也就是没有请求体
// 2.检查完请求体
}
// do_request()
// 当得到一个完整、正确的HTTP请求时,我们就分析目标文件的属性,
// 如果目标文件存在、对所有用户可读,且不是目录,则使用mmap将其
// 映射到内存地址m_file_address处,并告诉调用者获取文件成功
http_conn::HTTP_CODE http_conn::do_request()
{
// doc_root = "./resources" 服务器目录下的resources文件夹
strcpy( m_real_file, doc_root );
int len = strlen( doc_root );
// m_url = /index.html
// 将m_url拼接到doc_root后面
strncpy( 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 );
// 创建内存映射
// 这个m_file_address就是要发送的index.html网页资源的首地址
m_file_address = ( char* )mmap( 0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );
close( fd );
return FILE_REQUEST;
}
//到这一步,客户端的请求报文已经解析完毕,并且如果请求的资源存在,也已经把资源映射到内存中
//下一步就是发送数据了
// 由线程池中的工作线程调用,这是处理HTTP请求的入口函数
void http_conn::process() {
// 解析HTTP请求
// 以上都是process_read()的操作
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);
}
响应客户端的请求
服务器反馈的响应
// 根据服务器处理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 FILE_REQUEST:
add_status_line(200, ok_200_title );
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;
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;
}
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_content_type();
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" );
}
// 在响应头和响应正文之间添加 \r\n
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 );
}
bool http_conn::add_content_type() {
return add_response("Content-Type:%s\r\n", "text/html");
}
以上是拼接响应报文的状态行和一些必要的响应头部
这里补充一些数据和说明
class http_conn
{
private:
char m_write_buf[ WRITE_BUFFER_SIZE ]; // 写缓冲区
int m_write_idx; // 写缓冲区中待发送的字节数
char* m_file_address; // 客户请求的目标文件被mmap到内存中的起始位置
// 目标文件的状态。通过它判断文件是否存在、是否为目录、是否可读,并获取文件大小等信息
struct stat m_file_stat;
// 采用writev来执行写操作,定义两个iovec对象
struct iovec m_iv[2];
// 表示被写内存块的数量
int m_iv_count;
};
// 将要发送的数据写入缓冲区中
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 );
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;
}
// 这里有两个新的知识点......
va_list
#include <stdarg.h>
// 定义va_list类型的变量arg_list,变量是指向参数列表的指针
va_list arg_list;
//读取可变参数的过程其实就是在栈区中,使用指针,遍历栈区中的参数列表,从低地址到高地址一个一个地把参数内容读出来的过程·
va_start( arg_list, format);
// 释放arg_list指针,防止内存泄漏
va_end(arg_list);
// 第一个参数是va_list的变量,第二个参数是当前指针指向的参数的类型
va_arg( arg_list, type);
// 前三个是必须一起出现的
vsnprintf()
#include <stdio.h>
int vsnprintf (char *__restrict __s, size_t __maxlen,
const char *__restrict __format, _G_va_list __arg);
// 第一个参数是存放将要生成的格式化字符串的容器,通常是字符数组
// 第二个参数是字符数组可接收的最大字符数,(非字节数,UNICODE一个字符两个字节),防止产生数组越界
// 第三个参数是指定输出格式的字符串
// 第四个参数是va_list类型的变量;va:variable-argument可变参数
通过va_list
和vsnprintf
实现myprintf()
#include <stdio.h>
#include <stdarg.h>
void myprint(char* format,...){
va_list va;
va_start(va,format);
char buf[34];
// vsprintf(buf,format,va);
// int len = vsnprintf(buf,34,format,va); // My name is caiyibin, my age is 22
// int len = vsnprintf(buf,33,format,va); // My name is caiyibin, my age is 2
// if(len>0) printf("%s\n",buf);
// else perror("vsnprintf");
// 每次调用完va_arg,va指针都会向下一个参数的地址移动
// 同一行代码多次调用av_arg(),就跟 i++ + i++ 一样了,会出现Segmentation fault错误
printf("my name is %s, ",va_arg(va,char*));
printf("my age is %d\n",va_arg(va,int));
va_end(va);
}
int main(){
char* name = "caiyibin";
int age = 22;
char* format = "My name is %s, my age is %d\n";
myprint(format,name,age);
return 0;
}
目前服务器已经接收到客户端的请求报文并解析了报文,然后把响应数据准备好了。其中响应的数据分为两部分,一部分是响应报文的状态行和响应头,另一部分是响应正文
但第一部分的数据存储在char m_write_buf[ WRITE_BUFFER_SIZE ]; // 写缓冲区
中,而第二部分则是通过内存映射,映射在内存中,通过char* m_file_address; // 客户请求的目标文件被mmap到内存中的起始位置
。
到此,到了这里,程序已经把响应报文写进发送缓冲区中,等待下次epollfd
触发EPOLLOUT
,服务器就会发送响应报文。但响应报文的两个部分的地址并不是连续的,于是需要writev
函数来发送。
int main( int argc, char* argv[] ) {
// ......
}else if( events[i].events & EPOLLOUT ) {
if( !users[sockfd].write() ) { // 把 writev()封装到 write()中
users[sockfd].close_conn();
}
}
}
#include <sys/uio.h>
// 第一个参数是iovec结构体对象数组,第三个参数是对象数量,也就是数组大小
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
// iovec结构体的第一个成员是数据的首地址,第二个参数是数据的大小
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
// 发送HTTP响应报文
bool http_conn::write()
{
int temp = 0;
int bytes_have_send = 0; // 已经发送的字节
int bytes_to_send = m_write_idx;// 将要发送的字节 (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;
}
}
}
}
测试
# 编译程序,链接 pthread
g++ *.cpp -o web -pthread
# 以端口8888启动程序
./web 8888