八 高性能服务器程序框架
按照服务器的一般原理。可以将服务器结构为三个主要模块:
🗡I/O处理单元
🐰逻辑单元
✴️存储单元
8.1 服务器模型
C/S模型:服务器启动后,首先创建一个或者多个监听socket, 并调用bind函数将其绑定到服务器感兴趣的端口上, 然后调用listen函数等待连接。等待服务器稳定运行后就可以调用connect函数向服务器发起连接了。
注意: 客户连接请求是随机到达的异步事件, 所以服务器需要某种IO模型来监听这一事件, IO模型有多种, 比如select系统调用留待下回分解。
总结: C/S模型适合资源相对集中的场合, 实现比较简单; 但是缺点也比较明显, 服务器是通信的中心, 当访问量过大的时候, 所有客户都会得到很慢的响应.
P2P模型:网络上的所有主机重回对等的地位, 每台机器在消耗服务的同时也会给别人提供服务, 这样的话资源就能够充分、自由的共享. 此模型存在的缺点就是用户之间传输的请求过多时, 网络的负载会加重。另一个实际问题是主机之间很难被发现, 所以实际使用的模型会有一个专门的发现服务器, 这个服务器主要提供查找服务。
8.2 服务器编程框架
服务器基本框架如图所示:
服务器的基本模块描述:
I/O处理单元: 服务器管理客户连接的模块, 主要完成以下任务🔢等待并接受新的客户连接、接受客户数据、将服务器响应数据传回给客户端。根据事件处理模式的不同, 数据的收发可能在I/O处理单元中进行。
逻辑单元:一个逻辑单元通常是一个进程或者线程, 它分析并处理客户数据, 将结果传送给I/O处理单元或者客户端(根据事件处理模式不同)。
网络存储单元 可以是数据库, 缓存或者文件, 甚至是一台独立的服务器, 但不是必要的。
请求队列:是各单元之间通信方式的抽象, I/O接收到客户请求时需要以某种方式通知逻辑单元来处理请求; 多个逻辑单元同时访问一个存储单元时, 需要采用某种机制处理静态条件。
8.3 I/O模型
socket在创建的时候默认是阻塞的。阻塞和非阻塞的概念可以应用与所有的文件描述符, 而不仅仅是socket; 阻塞的文件描述符称之为阻塞I/O,非阻塞的文件描述符称之为非阻塞IO。
针对阻塞IO执行的系统调用可能因为系统调用可能因为无法立即完成而被系统挂起, 直到等待的事件发生为止。
针对非阻塞IO执行的系统调用总是会立即返回, 不管事件是否已经发生。如果事件没有发生, 系统调用会返回-1, 和出错一样, 此时必须根据errno来区分这两种情况。
因此, 非阻塞IO通常要和其他IO通知机制一起使用, 比如IO复用和SIGIO信号。
IO复用⚡️最常使用的IO通知机制, 应用程序通过IO复用函数向内核注册一组事件, 内核通过IO复用函数把其中就绪的事件通知给应用程序。note🍨IO复用函数本身是阻塞的, 他们能提高程序效率的原因是在于他们具有监听多个IO事件的能力。
SIGIO信号🤐也可以用来报告IO事件, 我们可以为一个文件描述符指定宿主进程, 那么宿主进程将捕获到SIGIO信号。当目标文件描述符上有时间发生时。SIGIO信号的信号处理函数将被触发, 我们也就可以在信号处理函数中执行非阻塞IO操作了。
同步I/O模型💃 I/O的读写操作都是在I/O事件发生之后, 由应用程序来进行的。
总结: 同步I/O模型要求用户代码自行执行IO操作;异步I/O机制则由内核来执行I/O操作。同步I/O向应用程序通知的是I/O就绪事件, 异步I/O向应用程序通知的是I/O完成事件。
8.4 两种高效的事件处理模式
Reactor模式
要求主线程只负责监听文件描述符是否有事件发生,有的话就立即将该事件通知工作线程,除此之外不做其他任何实质性的工作。读写数据,接收新的连接,以及处理客户请求均在工作线程中完成。
Proactor模式
Proactor模式将所有的IO操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。
模拟proactor模式
主线程执行数据读写操作,读写完成后,主线程向工作线程通知这一完成事件,那么从工作线程的角度来看,他们就直接获得了数据读写的结果。接下来要做的就是对读写的结果进行逻辑处理。
8.5 两种高效的并发模式
并发编程的任务是让程序同时执行多个任务,如果程序是计算密集型的,那并发编程并没有优势;而如果是I/O密集型的任务,那么程序阻塞于IO操作将会浪费大量的CPU时间, 在这种情况下使用多线程就会使CPU的利用率显著提升。服务器主要有两种并发编程模式🔌半同步/半异步模式、领导者/追随者模式。
半同步/半异步模式
同步线程用来处理客户逻辑,异步线程用于处理IO事件,异步线程监听到客户请求后,异步线程监听到客户请求后,就将其封装成请求对象并插入请求队列中。请求队列将通知某个工作在同步线程模式的工作线程来读取并处理该请求对象。具体选择哪一个请求对象,取决于请求队列的设计。
半同步/半反应堆模式
异步线程只有一个,由主线程来充当,负责监听所有socket上的事件,如果监听socket上有可读事件发生,主线程就接收并得到新的socket,然后往epoll内核时间表中注册该事件;如果连接socket上有读写事件发生,主线程就像该连接socket插入请求队列中,所有工作线程都睡眠在请求队列中,他们通过竞争获得任务的管辖权。
高效的半同步/半异步模式
主线程只管监听socket,连接socket由工作线程来管理。存在新的连接到来时,主线程就接受并派给工作线程。所有IO操作都由被选中的工作线程来处理。
领导者/追随者模式
在任意时间点,程序都仅有一个领导者线程,负责监听IO事件,其他线程都是追随者,当前的领导者线程检测到新的I/O事件,首先要从线程池中推选出新的领导者线程,然后处理I/O事件;新的领导者等待新的事件,原来的领导者处理I/O事件,二者实现了并发。
8.6 有限状态机
关于http协议🇶🇦
strpbrk函数
C 库函数 char *strpbrk(const char *str1, const char *str2) 检索字符串 str1 中第一个匹配字符串 str2 中字符的字符,不包含空结束字符。也就是说,依次检验字符串 str1 中的字符,当被检验字符在字符串 str2 中也包含时,则停止检验,并返回该字符位置。
strcasecmp函数
比较参数s1和s2字符串,比较时会自动忽略大小写的差异。若参数s1和s2字符串相等则返回0。s1大于s2则返回大于0 的值,s1 小于s2 则返回小于0的值。
strspn函数
C 库函数 size_t strspn(const char *str1, const char *str2) 检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标。
样例程序: 读取和解析http请求(摘自https://github.com/raichen/LinuxServerCodes/blob/master/8/8-3httpparser.cpp)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#define BUFFER_SIZE 4096
enum CHECK_STATE { CHECK_STATE_REQUESTLINE = 0, CHECK_STATE_HEADER, CHECK_STATE_CONTENT };
enum LINE_STATUS { LINE_OK = 0, LINE_BAD, LINE_OPEN };
enum HTTP_CODE { NO_REQUEST, GET_REQUEST, BAD_REQUEST, FORBIDDEN_REQUEST, INTERNAL_ERROR, CLOSED_CONNECTION };
static const char* szret[] = { "I get a correct result\n", "Something wrong\n" };
LINE_STATUS parse_line( char* buffer, int& checked_index, int& read_index )
{
char temp;
for ( ; checked_index < read_index; ++checked_index )
{
temp = buffer[ checked_index ];
if ( temp == '\r' )
{
if ( ( checked_index + 1 ) == read_index )
{
return LINE_OPEN;
}
else if ( buffer[ checked_index + 1 ] == '\n' )
{
buffer[ checked_index++ ] = '\0';
buffer[ checked_index++ ] = '\0';
return LINE_OK;
}
return LINE_BAD;
}
else if( temp == '\n' )
{
if( ( checked_index > 1 ) && buffer[ checked_index - 1 ] == '\r' )
{
buffer[ checked_index-1 ] = '\0';
buffer[ checked_index++ ] = '\0';
return LINE_OK;
}
return LINE_BAD;
}
}
return LINE_OPEN;
}
HTTP_CODE parse_requestline( char* szTemp, CHECK_STATE& checkstate )
{
char* szURL = strpbrk( szTemp, " \t" );
if ( ! szURL )
{
return BAD_REQUEST;
}
*szURL++ = '\0';
char* szMethod = szTemp;
if ( strcasecmp( szMethod, "GET" ) == 0 )
{
printf( "The request method is GET\n" );
}
else
{
return BAD_REQUEST;
}
szURL += strspn( szURL, " \t" );
char* szVersion = strpbrk( szURL, " \t" );
if ( ! szVersion )
{
return BAD_REQUEST;
}
*szVersion++ = '\0';
szVersion += strspn( szVersion, " \t" );
if ( strcasecmp( szVersion, "HTTP/1.1" ) != 0 )
{
return BAD_REQUEST;
}
if ( strncasecmp( szURL, "http://", 7 ) == 0 )
{
szURL += 7;
szURL = strchr( szURL, '/' );
}
if ( ! szURL || szURL[ 0 ] != '/' )
{
return BAD_REQUEST;
}
//URLDecode( szURL );
printf( "The request URL is: %s\n", szURL );
checkstate = CHECK_STATE_HEADER;
return NO_REQUEST;
}
HTTP_CODE parse_headers( char* szTemp )
{
if ( szTemp[ 0 ] == '\0' )
{
return GET_REQUEST;
}
else if ( strncasecmp( szTemp, "Host:", 5 ) == 0 )
{
szTemp += 5;
szTemp += strspn( szTemp, " \t" );
printf( "the request host is: %s\n", szTemp );
}
else
{
printf( "I can not handle this header\n" );
}
return NO_REQUEST;
}
HTTP_CODE parse_content( char* buffer, int& checked_index, CHECK_STATE& checkstate, int& read_index, int& start_line )
{
LINE_STATUS linestatus = LINE_OK;
HTTP_CODE retcode = NO_REQUEST;
while( ( linestatus = parse_line( buffer, checked_index, read_index ) ) == LINE_OK )
{
char* szTemp = buffer + start_line;
start_line = checked_index;
switch ( checkstate )
{
case CHECK_STATE_REQUESTLINE:
{
retcode = parse_requestline( szTemp, checkstate );
if ( retcode == BAD_REQUEST )
{
return BAD_REQUEST;
}
break;
}
case CHECK_STATE_HEADER:
{
retcode = parse_headers( szTemp );
if ( retcode == BAD_REQUEST )
{
return BAD_REQUEST;
}
else if ( retcode == GET_REQUEST )
{
return GET_REQUEST;
}
break;
}
default:
{
return INTERNAL_ERROR;
}
}
}
if( linestatus == LINE_OPEN )
{
return NO_REQUEST;
}
else
{
return BAD_REQUEST;
}
}
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
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 );
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 0 );
int ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( listenfd, 5 );
assert( ret != -1 );
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int fd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
if( fd < 0 )
{
printf( "errno is: %d\n", errno );
}
else
{
char buffer[ BUFFER_SIZE ];
memset( buffer, '\0', BUFFER_SIZE );
int data_read = 0;
int read_index = 0;
int checked_index = 0;
int start_line = 0;
CHECK_STATE checkstate = CHECK_STATE_REQUESTLINE;
while( 1 )
{
data_read = recv( fd, buffer + read_index, BUFFER_SIZE - read_index, 0 );
if ( data_read == -1 )
{
printf( "reading failed\n" );
break;
}
else if ( data_read == 0 )
{
printf( "remote client has closed the connection\n" );
break;
}
read_index += data_read;
HTTP_CODE result = parse_content( buffer, checked_index, checkstate, read_index, start_line );
if( result == NO_REQUEST )
{
continue;
}
else if( result == GET_REQUEST )
{
send( fd, szret[0], strlen( szret[0] ), 0 );
break;
}
else
{
send( fd, szret[1], strlen( szret[1] ), 0 );
break;
}
}
close( fd );
}
close( listenfd );
return 0;
}
执行效果:
主状态机使用checkstate变量来记录当前的状态, 如果当前的状态是CHECK_STATE_REQUESTLINE,则表示parse_line函数解析出的行是请求行,于是状态机调用parse_requestline来分析请求行, 如果当前的状态是CHECK_STATE_HEADER, 表示parse_line解析出的是头部字段,于是主状态机,于是主状态机调用parse_headers来分析头部字段。checkstate变量的初始值为CHECK_STATE_REQUESTLINE, parse_requestline函数在成功分析完请求行之后将其设置为CHECK_STATE_HEADER, 从而实现状态转移。
8.7 提高服务器性能的其他建议
📦 池
📒 数据复制
🔓 上下文切换和锁
e变量来记录当前的状态, 如果当前的状态是CHECK_STATE_REQUESTLINE,则表示parse_line函数解析出的行是请求行,于是状态机调用parse_requestline来分析请求行, 如果当前的状态是CHECK_STATE_HEADER, 表示parse_line解析出的是头部字段,于是主状态机,于是主状态机调用parse_headers来分析头部字段。checkstate变量的初始值为CHECK_STATE_REQUESTLINE, parse_requestline函数在成功分析完请求行之后将其设置为CHECK_STATE_HEADER, 从而实现状态转移。
8.7 提高服务器性能的其他建议
📦 池
📒 数据复制
🔓 上下文切换和锁