首页 技术 正文
技术 2022年11月14日
0 收藏 842 点赞 2,522 浏览 25168 个字

这是学习网络编程后写的一个练手的小程序,可以帮助复习I/O模型,epoll使用,线程池,HTTP协议等内容。

程序代码是基于《Linux高性能服务器编程》一书编写的。

首先回顾程序中的核心内容和主要问题,最后给出相关代码。

0. 功能和I/O模型

实现简易的HTTP服务端,现仅支持GET方法,通过浏览器访问可以返回相应内容。

I/O模型采用Reactor(I/O复用 + 非阻塞I/O) + 线程池。 使用epoll事件循环用作事件通知,如果listenfd上可读,则调用accept,把新建的fd加入epoll中;

是已连接sockfd,将其加入到线程池中由工作线程竞争执行任务。

1. 线程池怎么实现?

程序采用c++编写,要自己封装一个简易的线程池类。大致思路是创建固定数目的线程(如跟核数相同),然后类内部维护一个生产者—消费者队列。

提供相应的添加任务(生产者)和执行任务接口(消费者)。按照操作系统书中典型的生产者—消费者模型维护增减队列任务(使用mutex和semaphore)。

mutex用于互斥,保证任意时刻只有一个线程读写队列,semaphore用于同步,保证执行顺序(队列为空时不要读,队列满了不要写)。

2. epoll用条件触发(LT)还是边缘触发(ET)?

考虑这样的情况,一个工作线程在读一个fd,但没有读完。如果采用LT,则下一次事件循环到来的时候,又会触发该fd可读,此时线程池很有可能将该fd分配给其他的线程处理数据。

这显然不是我们想要看到的,而ET则不会在下一次epoll_wait的时候返回,除非读完以后又有新数据才返回。所以这里应该使用ET。

当然ET用法在《Tinychatserver: 一个简易的命令行群聊程序》也有总结过。用法的模式是固定的,把fd设为nonblocking,如果返回某fd可读,循环read直到EAGAIN。

3. 继续上面的问题,如果某个线程在处理fd的同时,又有新的一批数据发来(不是老数据没读完,是来新数据了),即使使用了ET模式,因为新数据的到来,仍然会触发该fd可读,所以仍然存在将该fd分给其他线程处理的情况。

这里就用到了EPOLLONESHOT事件。对于注册了EPOLLONESHOT事件的文件描述符,操作系统最大触发其上注册的一个可读、可写或者异常事件,且只触发一次。

除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程处理某个socket时,其他线程是不可能有机会操作该socket的,

即可解决该问题。但同时也要注意,如果注册了EPOLLONESHOT的socket一旦被某个线程处理完毕,则应该立即重置这个socket上的EPOLLONESHOT事件,

以确保下一次可读时,其EPOLLIN事件能够触发。

4. HTTP协议解析怎么做?数据读到一半怎么办?

首先理解这个问题。HTTP协议并未提供头部字段的长度,判断头部结束依据是遇到一个空行,该空行只包含一对回车换行符(<CR><LF>)。同时,如果一次读操作没有读入整个HTTP请求

的头部,我们必须等待用户继续写数据再次读入(比如读到 GET /index.html HTT就结束了,必须维护这个状态,下一次必须继续读‘P’)。

即我们需要判定当前解析的这一行是什么(请求行?请求头?消息体?),还需要判断解析一行是否结束?

解决上述问题,可以采取有限状态机。

参考【1】中设计方式,设计主从两个状态机(主状态机解决前半部分问题,从状态机解决后半部分问题)。

先分析从状态机,从状态机用于处理一行信息(即parse_line函数)。其中包括三个状态:LINE_OPEN, LINE_OK,LINE_BAD,转移过程如下所示:

Tinywebserver:一个简易的web服务器

当从状态机parse_line读到完整的一行,就可以将改行内容递交给process_read函数中的主状态机处理。

主状态机也有三种状态表示正在分析请求行(CHECK_STATE_REQUESTINE),正在分析头部字段(CHECK_STATE_HEADER),和正在分析内容(CHECK_CONTENT)。

主状态机使用checkstate变量来记录当前的状态。

如果当前的状态是CHECK_STATE_REQUESTLINE,则表示parse_line函数解析出的行是请求行,于是主状态机调用parse_requestline来分析请求行;

如果当前的状态是CHECK_STATE_HEADER,则表示parse_line函数解析出来的是头部字段,于是主状态机调用parse_header来分析头部字段。

如果当前状态是CHECK_CONTENT,则表示parse_line函数解析出来的是消息体,我们调用parse_content来分析消息体(实际上实现时候并没有分析,只是判断是否完整读入)

checkstate变量的初始值是CHECK_STATE_REQUESTLINE,调用相应的函数(parse_requestline,parse_header)后更新checkstate实现状态转移。

与主状态机有关的核心函数如下:

http_conn::HTTP_CODE http_conn::process_read()//完整的HTTP解析
{
LINE_STATUS line_status = LINE_OK;
HTTP_CODE ret = NO_REQUEST;
char* text = ; while ( ( ( m_check_state == CHECK_STATE_CONTENT ) && ( line_status == LINE_OK ) )
|| ( ( line_status = parse_line() ) == LINE_OK ) ){//满足条件:正在进行HTTP解析、读取一个完整行
text = get_line();//从读缓冲区(HTTP请求数据)获取一行数据
m_start_line = m_checked_idx;//行的起始位置等于正在每行解析的第一个字节
printf( "got 1 http line: %s", text ); switch ( m_check_state )//HTTP解析状态跳转
{
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();//当获得一个完整的连接请求则调用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;
}

5. HTTP响应怎么做?怎么发送效率高一些?

首先介绍readv和writev函数。其功能可以简单概括为对数据进行整合传输及发送,即所谓分散读,集中写。

也就是说,writev函数可以把分散保存在多个缓冲中的数据一并发送,通过readv函数可以由多个缓冲分别接收。因此适当采用这两个函数可以减少I/O次数。

例如这里要做的HTTP响应。其包含一个状态行,多个头部字段,一个空行和文档的内容。前三者可能被web服务器放置在一块内存中,

而文档的内容则通常被读入到另外一块单独的内存中(通过read函数或mmap函数)。这里可以采用writev函数将他们一并发出。

相关接口如下:

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);ssize_t writev(int fd, const struct iovec *iov, int iovcnt);其中第二个参数为如下结构体的数组
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
第三个参数为第二个参数的传递的数组的长度。

这里还可以再学习一下mmap与munmap函数。但是这里关于mmap与read效率的比较,应该没有那么简单的答案。mmap可以减少系统调用和内存拷贝,但是其引发的pagefault也是开销。效率的比较取决于不同系统对于这两个效率实现的不同,所以这里就简单谈一谈用法。

#include <sys/mman.h>
/**addr参数允许用户使用某个特定的地址作为这段内存的起始地址,设置为NULL则自动分配地址。
*length参数指定内存段的长度.
*prot参数用来设置内*存段的访问权限,比如PROT_READ可读, PROT_WRITE可写。
*flags控制内存段内容被修改后程序的行为。如MAP_PRIVATE指内存段为调用进程所私有,对该内存段的修改不会反映到被映射的文件中。
*/
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);

所以根据不同情况(200,404)填充HTTP的程序如下:

bool http_conn::process_write( HTTP_CODE ret )//填充HTTP应答
{
switch ( ret )
{
case INTERNAL_ERROR:
{
add_status_line( , error_500_title );
add_headers( strlen( error_500_form ) );
if ( ! add_content( error_500_form ) )
{
return false;
}
break;
}
case BAD_REQUEST:
{
add_status_line( , error_400_title );
add_headers( strlen( error_400_form ) );
if ( ! add_content( error_400_form ) )
{
return false;
}
break;
}
case NO_RESOURCE:
{
add_status_line( , error_404_title );
add_headers( strlen( error_404_form ) );
if ( ! add_content( error_404_form ) )
{
return false;
}
break;
}
case FORBIDDEN_REQUEST:
{
add_status_line( , error_403_title );
add_headers( strlen( error_403_form ) );
if ( ! add_content( error_403_form ) )
{
return false;
}
break;
}
case FILE_REQUEST://资源页文件可用
{
add_status_line( , ok_200_title );
if ( m_file_stat.st_size != )
{
add_headers( m_file_stat.st_size );//m_file_stat资源页文件状态
m_iv[ ].iov_base = m_write_buf;//写缓冲区
m_iv[ ].iov_len = m_write_idx;//长度
m_iv[ ].iov_base = m_file_address;//资源页数据内存映射后在m_file_address地址
m_iv[ ].iov_len = m_file_stat.st_size;//文件长度就是该块内存长度
m_iv_count = ;
return true;
}
else
{
const char* ok_string = "<html><body></body></html>";//请求页位空白
add_headers( strlen( ok_string ) );
if ( ! add_content( ok_string ) )
{
return false;
}
}
}
default:
{
return false;
}
} m_iv[ ].iov_base = m_write_buf;
m_iv[ ].iov_len = m_write_idx;
m_iv_count = ;
return true;
}

填充HTTP应答

bool http_conn::write()//将资源页文件发送给客户端
{
int temp = ;
int bytes_have_send = ;
int bytes_to_send = m_write_idx;
if ( bytes_to_send == )
{
modfd( m_epollfd, m_sockfd, EPOLLIN );//EPOLLONESHOT事件每次需要重置事件
init();
return true;
} while( )//
{
temp = writev( m_sockfd, m_iv, m_iv_count );//集中写,m_sockfd是http连接对应的描述符,m_iv是iovec结构体数组表示内存块地址,m_iv_count是数组的长度即多少个内存块将一次集中写到m_sockfd
if ( temp <= - )//集中写失败
{
if( errno == EAGAIN )
{
modfd( m_epollfd, m_sockfd, EPOLLOUT );//重置EPOLLONESHOT事件,注册可写事件表示若m_sockfd没有写失败则关闭连接
return true;
}
unmap();//解除内存映射
return false;
} bytes_to_send -= temp;//待发送数据
bytes_have_send += temp;//已发送数据
if ( bytes_to_send <= bytes_have_send )
{
unmap();//该资源页已经发送完毕该解除映射
if( m_linger )//若要保持该http连接
{
init();//初始化http连接
modfd( m_epollfd, m_sockfd, EPOLLIN );
return true;
}
else
{
modfd( m_epollfd, m_sockfd, EPOLLIN );
return false;
}
}
}
}

将应答发送给客户端

6.忽略SIGPIPE

这是一个看似很小,但是如果不注意会直接引发bug的地方。如果往一个读端关闭的管道或者socket中写数据,会引发SIGPIPE,程序收到SIGPIPE信号后默认的操作时终止进程。

这也就是说,如果客户端意外关闭,那么服务器可能也就跟着直接挂了,这显然不是我们想要的。所以网络程序中服务端一般会忽略SIGPIPE信号。

7. 程序代码

程序中有比较详细的注释,虽然主干在上面问题中分析过了,但是诸如如何解析一行数据之类的操作,还是很烦的…可以直接参考代码

 #ifndef THREADPOOL_H
#define THREADPOOL_H #include <list>
#include <cstdio>
#include <exception>
#include <pthread.h>
#include "locker.h" //简单封装了互斥量和信号量的接口 //线程池类模板参数T是任务类型,T中必须有接口process
template< typename T >
class threadpool
{
public:
threadpool( int thread_number = , int max_requests = );//线程数目和最大连接处理数
~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;//线程id数组
std::list< T* > m_workqueue;//工作队列:各线程竞争该队列并处理相应的任务逻辑T
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 <= ) || ( max_requests <= ) )
{
throw std::exception();
} m_threads = new pthread_t[ m_thread_number ];//工作线程数组
if( ! m_threads )
{
throw std::exception();
} for ( int i = ; i < thread_number; ++i )//创建工作线程
{
printf( "create the %dth thread\n", i );
if( pthread_create( m_threads + i, NULL, worker, this ) != )
{
delete [] m_threads;
throw std::exception();
}
if( pthread_detach( m_threads[i] ) ) //分离线程使得其它线程回收和杀死该线程
{
delete [] m_threads;
throw std::exception();
}
}
} template< typename T >
threadpool< T >::~threadpool()
{
delete [] m_threads;
m_stop = true;
} template< typename T >
bool threadpool< T >::append( T* request )//向工作队列添加任务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();//信号量的V操作,多了一个工作任务T使得信号量+1
return true;
} template< typename T >
void* threadpool< T >::worker( void* arg )//工作线程函数
{
threadpool* pool = ( threadpool* )arg; //获取线程池对象,之前创建的时候传的this
pool->run(); //调用线程池run函数
return pool;
} template< typename T >
void threadpool< T >::run() //工作线程真正工作逻辑:从任务队列领取任务T并执行任务T,消费者
{
while ( ! m_stop )
{
m_queuestat.wait();//信号量P操作,申请信号量获取任务T
m_queuelocker.lock();//对工作队列操作前加锁
if ( m_workqueue.empty() )
{
m_queuelocker.unlock();//任务队列空无法消费
continue;
}
T* request = m_workqueue.front();//获取任务T
m_workqueue.pop_front();
m_queuelocker.unlock();
if ( ! request )
{
continue;
}
request->process();//执行任务T的相应逻辑,任务T中必须有process接口
}
} #endif

threadpool.h

#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>
#include "locker.h"class http_conn
{
public:
static const int FILENAME_LEN = ;//文件名最大长度,文件是HTTP请求的资源页文件
static const int READ_BUFFER_SIZE = ;//读缓冲区,用于读取HTTP请求
static const int WRITE_BUFFER_SIZE = ;//写缓冲区,用于HTTP回答
enum METHOD { GET = , POST, HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT, PATCH };//HTTP请求方法,本程序只定义了GET逻辑
enum CHECK_STATE { CHECK_STATE_REQUESTLINE = , CHECK_STATE_HEADER, CHECK_STATE_CONTENT };//HTTP请求状态:正在解析请求行、正在解析头部、解析中
enum HTTP_CODE { NO_REQUEST, GET_REQUEST, BAD_REQUEST, NO_RESOURCE, FORBIDDEN_REQUEST, FILE_REQUEST, INTERNAL_ERROR, CLOSED_CONNECTION };//HTTP请求结果:未完整的请求(客户端仍需要提交请求)、完整的请求、错误请求...只用了前三个
enum LINE_STATUS { LINE_OK = , LINE_BAD, LINE_OPEN };//HTTP每行解析状态:改行解析完毕、错误的行、正在解析行public:
http_conn(){}
~http_conn(){}public:
void init( int sockfd, const sockaddr_in& addr );//初始化新的HTTP连接
void close_conn( bool real_close = true );
void process();//处理客户请求,这是HTTP请求的入口函数,与在线程池中调用!!!
bool read();//读取客户发送来的数据(HTTP请求)
bool write();//将请求结果返回给客户端
private:
void init();//初始化连接,用于内部调用
HTTP_CODE process_read();//解析HTTP请求,内部调用parse_系列函数
bool process_write( HTTP_CODE ret );//填充HTTP应答,通常是将客户请求的资源页发送给客户,内部调用add_系列函数 HTTP_CODE parse_request_line( char* text );//解析HTTP请求的请求行
HTTP_CODE parse_headers( char* text );//解析HTTP头部数据
HTTP_CODE parse_content( char* text );//获取解析结果
HTTP_CODE do_request();//处理HTTP连接:内部调用process_read(),process_write()
char* get_line() { return m_read_buf + m_start_line; }//获取HTTP请求数据中的一行数据
LINE_STATUS parse_line();//解析行内部调用parse_request_line和parse_headers
//下面的函数被process_write填充HTTP应答 void unmap();//解除内存映射,这里内存映射是指将客户请求的资源页文件映射通过mmap映射到内存
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:
static int m_epollfd;//所有socket上的事件都注册到一个epoll事件表中所以用static
static int m_user_count;//用户数量private:
int m_sockfd;//HTTP连接对应的客户在服务端的描述符m_sockfd和地址m_address
sockaddr_in m_address; char m_read_buf[ READ_BUFFER_SIZE ];//读缓冲区,读取HTTP请求
int m_read_idx;//已读入的客户数据最后一个字节的下一个位置,即未读数据的第一个位置
int m_checked_idx;//当前已经解析的字节(HTTP请求需要逐个解析)
int m_start_line;//当前解析行的起始位置
char m_write_buf[ WRITE_BUFFER_SIZE ];//写缓冲区
int m_write_idx;//写缓冲区待发送的数据 CHECK_STATE m_check_state;//HTTP解析的状态:请求行解析、头部解析
METHOD m_method;//HTTP请求方法,只实现了GET char m_real_file[ FILENAME_LEN ];//HTTP请求的资源页对应的文件名称,和服务端的路径拼接就形成了资源页的路径
char* m_url;//请求的具体资源页名称,如:www.baidu.com/index.html
char* m_version;//HTTP协议版本号,一般是:HTTP/1.1
char* m_host;//主机名,客户端要在HTTP请求中的目的主机名
int m_content_length;//HTTP消息体的长度,简单的GET请求这个为空
bool m_linger;//HTTP请求是否保持连接 char* m_file_address;//资源页文件内存映射后的地址
struct stat m_file_stat;//资源页文件的状态,stat文件结构体
struct iovec m_iv[];//调用writev集中写函数需要m_iv_count表示被写内存块的数量,iovec结构体存放了一段内存的起始位置和长度,
int m_iv_count;//m_iv_count是指iovec结构体数组的长度即多少个内存块
};#endif

http_conn.h

 #include "http_conn.h" 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";//服务端资源页的路径,将其和HTTP请求中解析的m_url拼接形成资源页的位置 int setnonblocking( int fd )//将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 )//将fd添加到事件表epollfd
{
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 )//将fd从事件表epollfd中移除
{
epoll_ctl( epollfd, EPOLL_CTL_DEL, fd, );
close( fd );
} void modfd( int epollfd, int fd, int ev )//EPOLLONESHOT需要重置事件后事件才能进行下次侦听
{
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 = ;//连接数
int http_conn::m_epollfd = -;//事件表,注意是static故所有http_con类对象共享一个事件表 void http_conn::close_conn( bool real_close )//关闭连接,从事件表中移除描述符
if( real_close && ( m_sockfd != - ) )
{
//modfd( m_epollfd, m_sockfd, EPOLLIN );
removefd( m_epollfd, m_sockfd );
m_sockfd = -;
m_user_count--;
}
} void http_conn::init( int sockfd, const sockaddr_in& addr )//初始化连接
{
m_sockfd = sockfd;//sockfd是http连接对应的描述符用于接收http请求和http回答
m_address = addr;//客户端地址
int error = ;
socklen_t len = sizeof( error );
getsockopt( m_sockfd, SOL_SOCKET, SO_ERROR, &error, &len );
int reuse = ;
setsockopt( m_sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );//获取描述符状态,可以在调试时用
addfd( m_epollfd, sockfd, true );
m_user_count++;//多了一个http用户 init();//调用重载函数
} void http_conn::init()//重载init函数进行些连接前的初始化操作
{
m_check_state = CHECK_STATE_REQUESTLINE;
m_linger = false; m_method = GET;
m_url = ;
m_version = ;
m_content_length = ;
m_host = ;
m_start_line = ;
m_checked_idx = ;
m_read_idx = ;
m_write_idx = ;
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;
//checked_index指向buffer(应用程序读缓冲区)中当前正在分析的字节,read_index指向buffer中客户数据尾部的下一个字节;
//buffer中的第0 ~ checked_index字节都已分析完毕, 第checked_index ~ (read_index - 1)字节由下面的循环挨个分析
for ( ; m_checked_idx < m_read_idx; ++m_checked_idx )
{
//获得当前要分析的字节;
temp = m_read_buf[ m_checked_idx ];
//如果是“\r”,即回车符号,说明可能读取到一个完整的行
if ( temp == '\r' )
{
//如果\r恰巧是目前buffer中最后一个已经被读入的客户数据,那么这次分析没有读取到完整的行,
//返回LINE_OPEN表示还需要继续读取客户数据来进一步分析
if ( ( m_checked_idx + ) == m_read_idx )
{
return LINE_OPEN;
}
//如果下一个字符是\n,说明读到一个完整的行
else if ( m_read_buf[ m_checked_idx + ] == '\n' )
{
m_read_buf[ m_checked_idx++ ] = '\0';
m_read_buf[ m_checked_idx++ ] = '\0';
return LINE_OK;
}
//否则,说明存在语法问题
return LINE_BAD;
}
//如果当前字符是\n,说明也可能读到一个完整的行
//not necessary
else if( temp == '\n' )
{
if( ( m_checked_idx > ) && ( m_read_buf[ m_checked_idx - ] == '\r' ) )
{
m_read_buf[ m_checked_idx- ] = '\0';
m_read_buf[ m_checked_idx++ ] = '\0';
return LINE_OK;
}
return LINE_BAD;
}
}
//如果所有内容分析完毕都没有遇到\r字符,则返回LINE_OPEN,表示还需要继续读取客户数据才能进一步分析
return LINE_OPEN;
} //循环读取客户数据,直到无数据可读或者对方关闭连接
bool http_conn::read()
{
if( m_read_idx >= READ_BUFFER_SIZE )
{
return false;
} int bytes_read = ;
while( true )
{
bytes_read = recv( m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, );
if ( bytes_read == - )
{
if( errno == EAGAIN || errno == EWOULDBLOCK )
{
break;
}
return false;
}
else if ( bytes_read == )
{
return false;
} m_read_idx += bytes_read;
}
return true;
} http_conn::HTTP_CODE http_conn::parse_request_line( char* text )
{
//strpbrk()函数检索两个字符串中首个相同字符的位置,其原型为:
// char *strpbrk( char *s1, char *s2)
m_url = strpbrk( text, " \t" ); //请求行中如果没有\t,则该HTTP请求有问题
if ( ! m_url )
{
return BAD_REQUEST;
} *m_url++ = '\0'; char* method = text;
//定义函数:int strcasecmp (const char *s1, const char *s2);
//函数说明:strcasecmp()用来比较参数s1 和s2 字符串,比较时会自动忽略大小写的差异。
//返回值:若参数s1 和s2 字符串相同则返回0。s1 长度大于s2 长度则返回大于0 的值,s1 长度若小于s2 长度则返回小于0 的值。
if ( strcasecmp( method, "GET" ) == )
{
m_method = GET;
}
else
{
return BAD_REQUEST;
}
//strspn() 函数用来计算字符串 str 中连续有几个字符都属于字符串 accept,其原型为:
//size_t strspn(const char *str, const char * accept);
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" ) != )
{
return BAD_REQUEST;
} if ( strncasecmp( m_url, "http://", ) == )
{
m_url += ;
//strchr() 用来查找某字符在字符串中首次出现的位置,其原型为:
//char * strchr (const char *str, int c)
m_url = strchr( m_url, '/' );
} if ( ! m_url || m_url[ ] != '/' )
{
return BAD_REQUEST;
}
//HTTP请求行处理完毕,状态转移到头部字段的分析
m_check_state = CHECK_STATE_HEADER;
return NO_REQUEST;
} /*解析http请求的一个头部信息*/
http_conn::HTTP_CODE http_conn::parse_headers( char* text )
{ //空行,头部字段解析完毕
if( text[ ] == '\0' )
{
if ( m_method == HEAD )
{
return GET_REQUEST;
}
//如果HTTP请求有消息体,则还需要读取m_content_length字节的消息体,从状态机转移到CHECK_STATE_CONTENT状态
if ( m_content_length != )
{
m_check_state = CHECK_STATE_CONTENT;
return NO_REQUEST;
}
//否则,说明得到了一个完整的HTTP请求
return GET_REQUEST;
}
//处理connection头部字段
else if ( strncasecmp( text, "Connection:", ) == )
{
text += ;
text += strspn( text, " \t" );
if ( strcasecmp( text, "keep-alive" ) == )
{
m_linger = true;
}
}
//处理Content_length头部字段
else if ( strncasecmp( text, "Content-Length:", ) == )
{
text += ;
text += strspn( text, " \t" );
m_content_length = atol( text );
}
//处理host头部字段
else if ( strncasecmp( text, "Host:", ) == )
{
text += ;
text += strspn( text, " \t" );
m_host = text;
}
else
{
printf( "oop! unknow header %s\n", text );
} return NO_REQUEST; }
//消息体不解析,判断一下是否完整读入
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 = ; 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_conn::HTTP_CODE http_conn::do_request()//用于获取资源页文件的状态
{
strcpy( m_real_file, doc_root );
int len = strlen( doc_root );
strncpy( m_real_file + len, m_url, FILENAME_LEN - len - );
if ( stat( m_real_file, &m_file_stat ) < )
{
return NO_RESOURCE;//若资源页不存在则HTTP解析结果为404
} 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( , m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, );//将资源页文件映射到内存
close( fd );
return FILE_REQUEST;//资源页请求成功
} void http_conn::unmap()//解除资源页文件映射的内存
{
if( m_file_address )
{
munmap( m_file_address, m_file_stat.st_size );//解除映射
m_file_address = ;
}
} bool http_conn::write()//将资源页文件发送给客户端
{
int temp = ;
int bytes_have_send = ;
int bytes_to_send = m_write_idx;
if ( bytes_to_send == )
{
modfd( m_epollfd, m_sockfd, EPOLLIN );//EPOLLONESHOT事件每次需要重置事件
init();
return true;
} while( )//
{
temp = writev( m_sockfd, m_iv, m_iv_count );//集中写,m_sockfd是http连接对应的描述符,m_iv是iovec结构体数组表示内存块地址,m_iv_count是数组的长度即多少个内存块将一次集中写到m_sockfd
if ( temp <= - )//集中写失败
{
if( errno == EAGAIN )
{
modfd( m_epollfd, m_sockfd, EPOLLOUT );//重置EPOLLONESHOT事件,注册可写事件表示若m_sockfd没有写失败则关闭连接
return true;
}
unmap();//解除内存映射
return false;
} bytes_to_send -= temp;//待发送数据
bytes_have_send += temp;//已发送数据
if ( bytes_to_send <= bytes_have_send )
{
unmap();//该资源页已经发送完毕该解除映射
if( m_linger )//若要保持该http连接
{
init();//初始化http连接
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, ... )//HTTP应答主要是将应答数据添加到写缓冲区m_write_buf
{
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 - - m_write_idx, format, arg_list );//将fromat内容输出到m_write_buf
if( len >= ( WRITE_BUFFER_SIZE - - 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();
} 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 );
} bool http_conn::process_write( HTTP_CODE ret )
{
switch ( ret )
{
case INTERNAL_ERROR:
{
add_status_line( , error_500_title );
add_headers( strlen( error_500_form ) );
if ( ! add_content( error_500_form ) )
{
return false;
}
break;
}
case BAD_REQUEST:
{
add_status_line( , error_400_title );
add_headers( strlen( error_400_form ) );
if ( ! add_content( error_400_form ) )
{
return false;
}
break;
}
case NO_RESOURCE:
{
add_status_line( , error_404_title );
add_headers( strlen( error_404_form ) );
if ( ! add_content( error_404_form ) )
{
return false;
}
break;
}
case FORBIDDEN_REQUEST:
{
add_status_line( , error_403_title );
add_headers( strlen( error_403_form ) );
if ( ! add_content( error_403_form ) )
{
return false;
}
break;
}
case FILE_REQUEST:
{
add_status_line( , ok_200_title );
if ( m_file_stat.st_size != )
{
add_headers( m_file_stat.st_size );
m_iv[ ].iov_base = m_write_buf;
m_iv[ ].iov_len = m_write_idx;
m_iv[ ].iov_base = m_file_address;
m_iv[ ].iov_len = m_file_stat.st_size;
m_iv_count = ;
return true;
}
else
{
const char* ok_string = "<html><body></body></html>";
add_headers( strlen( ok_string ) );
if ( ! add_content( ok_string ) )
{
return false;
}
}
}
default:
{
return false;
}
} m_iv[ ].iov_base = m_write_buf;
m_iv[ ].iov_len = m_write_idx;
m_iv_count = ;
return true;
} 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 );
}

http_conn.cpp

 #ifndef LOCKER_H
#define LOCKER_H #include <exception>
#include <pthread.h>
#include <semaphore.h> class sem
{
public:
sem()
{
if( sem_init( &m_sem, , ) != )
{
throw std::exception();
}
}
~sem()
{
sem_destroy( &m_sem );
}
bool wait()
{
return sem_wait( &m_sem ) == ;
}
bool post()
{
return sem_post( &m_sem ) == ;
} private:
sem_t m_sem;
}; class locker
{
public:
locker()
{
if( pthread_mutex_init( &m_mutex, NULL ) != )
{
throw std::exception();
}
}
~locker()
{
pthread_mutex_destroy( &m_mutex );
}
bool lock()
{
return pthread_mutex_lock( &m_mutex ) == ;
}
bool unlock()
{
return pthread_mutex_unlock( &m_mutex ) == ;
} private:
pthread_mutex_t m_mutex;
}; class cond
{
public:
cond()
{
if( pthread_mutex_init( &m_mutex, NULL ) != )
{
throw std::exception();
}
if ( pthread_cond_init( &m_cond, NULL ) != )
{
pthread_mutex_destroy( &m_mutex );
throw std::exception();
}
}
~cond()
{
pthread_mutex_destroy( &m_mutex );
pthread_cond_destroy( &m_cond );
}
bool wait()
{
int ret = ;
pthread_mutex_lock( &m_mutex );
ret = pthread_cond_wait( &m_cond, &m_mutex );
pthread_mutex_unlock( &m_mutex );
return ret == ;
}
bool signal()
{
return pthread_cond_signal( &m_cond ) == ;
} private:
pthread_mutex_t m_mutex;
pthread_cond_t m_cond;
}; #endif

locker.h

 #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 <cassert>
#include <sys/epoll.h> #include "locker.h"
#include "threadpool.h"
#include "http_conn.h" #define MAX_FD 65536
#define MAX_EVENT_NUMBER 10000 extern int addfd( int epollfd, int fd, bool one_shot );
extern int removefd( int epollfd, int fd ); void addsig( int sig, void( handler )(int), bool restart = true )
{
struct sigaction sa;
memset( &sa, '\0', sizeof( sa ) );
sa.sa_handler = handler;
if( restart )
{
sa.sa_flags |= SA_RESTART;
}
sigfillset( &sa.sa_mask );
assert( sigaction( sig, &sa, NULL ) != - );
} void show_error( int connfd, const char* info )
{
printf( "%s", info );
send( connfd, info, strlen( info ), );
close( connfd );
} int main( int argc, char* argv[] )
{
if( argc <= )
{
printf( "usage: %s ip_address port_number\n", basename( argv[] ) );
return ;
}
const char* ip = argv[];
int port = atoi( argv[] ); addsig( SIGPIPE, SIG_IGN ); //创建线程池
threadpool< http_conn >* pool = NULL;
try
{
pool = new threadpool< http_conn >;
}
catch( ... )
{
return ;
} //预先为每个客户连接分配一个http_conn对象
http_conn* users = new http_conn[ MAX_FD ];
assert( users );
int user_count = ; int listenfd = socket( PF_INET, SOCK_STREAM, );
assert( listenfd >= );
struct linger tmp = { , };
setsockopt( listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof( tmp ) ); int ret = ;
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 >= ); ret = listen( listenfd, );
assert( ret >= ); epoll_event events[ MAX_EVENT_NUMBER ];
int epollfd = epoll_create( );
assert( epollfd != - );
addfd( epollfd, listenfd, false );
http_conn::m_epollfd = epollfd; while( true )
{
int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, - );
if ( ( number < ) && ( errno != EINTR ) )
{
printf( "epoll failure\n" );
break;
} for ( int i = ; 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 < )
{
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();
}
}
else
{}
}
} close( epollfd );
close( listenfd );
delete [] users;
delete pool;
return ;
}

main.cpp

8. 参考资料

[1]. 游双. Linux高性能服务器编程[M]. 机械工业出版社, 2013.

[2]. 如何写一个Web服务器 http://lifeofzjs.com/blog/2015/05/16/how-to-write-a-server/

[3]. 尹圣雨. TCP/IP网络编程[M]. 人民邮电出版社, 2014.

[4]. W.RICHARDSTEVENS, STEPHENA.RAGO. UNIX环境高级编程[M]. 人民邮电出版社, 2014.

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,497
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,910
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,744
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,498
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:8,135
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:5,298