张家港安监站网址,黑帽seo优化软件,百度建站官网,做网站代理需要办什么营业执照第8章 高性能服务器程序框架
8.1 服务器模型 服务器启动后#xff0c;首先创建一个#xff08;或多个#xff09;监听socket#xff0c;并调用bind函数将其绑定到服务器感兴趣的端口#xff0c;然后调用listen函数等待客户连接。服务器稳定运行之后#xff0c;客户端就可…第8章 高性能服务器程序框架
8.1 服务器模型 服务器启动后首先创建一个或多个监听socket并调用bind函数将其绑定到服务器感兴趣的端口然后调用listen函数等待客户连接。服务器稳定运行之后客户端就可以调用connect函数向服务器发起连接。由于客户连接请求是随机到达的异步事件版务器需要使用某种I/O模型来监听这一事件。 下图服务器使用的是I/O复用技术之一的select系统调用。当监听到连接请求后服务器就调用accept函数接受它并分配一个逻辑单元为新的连接服务。逻辑单元可以是新创建的子进程、子线程或者其他下图服务器给客户端分配的逻辑单元是fork系统调用创建的子进程。逻辑单元读取客户请求处理该请求然后将处理结果返回给客户端客户端接收到服务器反馈的结果之后可以继续向服务器发送请求也可以主动关闭连接。如果客户端主动关团连接则服务器执行被动关闭连接。至此双方的通信结束。服务器同时监听多个客户请求是通过select系统调用实现的。 访问量过大时响应较慢。 P2PPeer to Peer点对点模型使得每台机器在消耗服务的同时也给别人提供服务每台主机既是客户端也是服务器专门的发现服务器用来提供查找服务。
8.2 服务器编程框架 8.3 I/O模型
socket创建时默认是阻塞的。阻塞的概念也能应用于文件描述符非阻塞的文件描述符称为非阻塞I/O。 针对阻塞I/O执行的系统调用可能因为无法立即完成而被OS挂起针对非阻塞I/O执行的系统调用则总是立即返回而不管事件是否已经发生。 只有在事件已经发生的情况下操作非阻塞I/O才能提高效率非阻塞I/O要和其他I/O通知机制I/O复用和SIGIO信号一起使用。 同步I/O阻塞I/O、I/O复用和信号驱动I/O要求用户代码自行执行I/O 操作将数据从内核缓冲区读入用户缓冲区或将数据从用户缓冲区写入内核缓冲区而异步 I/O机制则由内核来执行I/O操作数据在内核缓冲区和用户缓冲区之间的移动是用内核在“后台”完成的。 同步I/O向应用程序通知的是I/O就绪事件由应用程序完成I/O读写而异步I/O向应用程序通知的是I/O完成事件由内核完成I/O读写。
8.4 两种高效的事件处理模式
三类事件I/O事件、信号事件、定时事件。 Reactor事件处理模式要求主线程I/O处理单元只负责监听文件描述上是否有事件发生。有则立即将事件通知工作线程逻辑单元然后读写数据接受新连接以及处理客户请求。 同步I/O模型epoll_wait为例实现 1主线程往epoll内核事件表中注册socket上的读就绪事件 2主线程调用epoll_wait等待socket上有数据可读 3socket上有数据时epoll_wait通知主线程将socket可读事件放入请求队列 4睡眠在请求队列上的某个工作线程被唤醒从socket上读取数据处理客户请求然后往epoll内核事件表中注册该socket上的写就绪事件。 5主线程调用epoll_wait等待socket可写。 6socket可写时epoll_wait通知主线程将socket可写事件放入请求队列 7睡眠在请求队列上的某个工作线程被唤醒它往socket上写入服务器处理客户请求的结果。 Proactor事件处理模式将所有I/O操作交给主线程和内核处理工作线程仅负责业务逻辑。 异步I/O模型aio_read和aio_write为例实现 1主线程调用aio_read函数向内核注册socket上的读完成事件并告诉内核用户读缓冲区的位置以及读操作完成时如何通知应用程序信号为例 2主线程继续处理其他逻辑 3当socket上的数据被读入用户缓冲区后内核向应用程序发送信号以通知应用程序数据已经可用 4应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后调用aio_write函数向内核注册socket上的写完成事件并告诉内核用户写缓冲区的位置以及写操作完成时如何通知应用程序信号为例 5主线程继续处理其他逻辑 6用户缓冲区的数据被写入socket后内核将向应用程序发送信号通知应用程序数据发送完毕 7应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理比如决定是否关闭socket。 使用同步I/O模型模拟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上写入服务器处理客户请求的结果。
8.5 两种高效的并发模式
这里讨论模式。并发模式指I/O处理单元和多个逻辑单元之间协调完成任务。 半同步/异步模式这里的同步异步指程序的执行是按顺序还是由系统事件驱动。 半同步/半异步模式中同步线程用于处理客户逻辑相当于逻辑单元异步线程用于处理I/O事件相当于I/O处理单元。异步线程监听到客户请求后就将其封装成请求对象并插入请求队列中请求队列将通知某个工作在同步模式的工作线程选取方法Round Robin、条件变量、信号量读取并处理该请求对象。 其变体半同步/半反应堆模式。 半同步/半反应堆模式采用的事件处理模式是Reactor模式要求工作线程从socket上读取客户请求和往socket写入服务器应答。也可以使用模拟的Proactor事件处理模式。 缺点 1主线程和工作线程共享请求队列添加和取出任务都要对请求队列加锁保护耗费CPU时间 2每个工作线程在同一时间只能处理一个客户请求客户多则队列堆积客户端响应慢若增加工作线程则工作线程的切换也会耗费CPU时间。 主线程只管理监听socket工作线程管理连接socket。派发方式往主线程和工作线程之间的管道里写数据工作线程检测到管道上有数据可读时就分析是否是一个新的客户连接请求到来。如果是则把该新soeket上的读写事件注册到自己的epoll内核事件表中。 领导者/追随者模式多个工作线程轮流获得事件源集合轮流监听、分发并处理事件。在任意时间点程序都仅有一个领导者线程负责监听I/O事件而其他线程则都是追随者它们休眠在线程池中等待成为新的领导者当前的领导者如果检测到 I/O事件首先要从线程池中推选出新的领导者线程然后处理I/O事件。此时新的领导者等得新的I/O事件而原来的领导者则处理I/O事件二者实现了并发。 组件句柄集HandleSet、线程集ThreadSet、事件处理器EventHandler、具体的事件处理器ConcreteEventHandler。 句柄集句柄表示I/O资源文件描述符句柄集用wait_for_event方法监听句柄上的I/O事件将就绪事件通知领导者线程。领导者则调用绑定到Handle上的事件处理器来处理事件。领导者将Handle和事件处理器绑定是通过调用句柄集中的register_handle方法实现。 线程集所有工作线程管理者。负责个线程之间的同步及新领导者推选。线程集中线程的三种状态Leader领导者、Processing处理事件中、Follower追随者。 事件处理器和具体的事件处理器事件处理器通常包含回调函数用于处理事件对应的业务逻辑使用前被绑定到句柄上。具体的事件处理器是其派生类必须重新实现基类的handle_event方法来处理特定任务。
8.6 有限状态机
用于逻辑单元内部。有的应用层协议头部包含数据包类型字段每种类型可以映射为逻辑单元的一种执行状态服务器可以根据它来编写相应的处理逻辑通过内部驱动实现状态转移。 很多网络协议包括TCP协议和IP协议都在其头部中提供头部长度字段。程序根据该字段值就可以知道是否接收到一个完整的协议头部。但HTTP协议并未提供这样的头部长度字段并且其头部长度变化也很大。 判断HTTP头部结束的依据遇到一个空行仅包含一对回车换行符CRLF。如果一次读操作没有读入HTTP请求的整个头部即没有遇到空行那么必须等待客户继续写数据并再次读入。不过在寻找空行的过程中可以同时完成对整个HTTP请求头部的分析空行前面还有请求行和头部域以提高解析HTTP请求的效率。 使用主、从两个有限状态机实现最简单的HTTP请求的读取和分析
#include sys/socket.h
#include sys/types.h
#include netinet/in.h
#include arpa/inet.h
#include stdio.h
#include stdlib.h
#include unistd.h
#include errno.h
#include string.h
#include fcntl.h
#include assert.h#define BUFFER_SIZE 4096 /* 读缓冲区大小 */
/* 主状态机的两种可能状态分别表示:当前正在分析请求行 当前正在分析头部字段 */
enum CHECK_STATE{CHECK_STATE_REQUESTLINE 0,CHECK_STATE_HEADER
};/* 从状态机的三种可能状态即行的读取状态分别表示:读取到一个完整的行、行出错* 和行数据尚且不完整 */
enum LINE_STATUS{LINE_OK 0,LINE_BAD,LINE_OPEN
};/* 服务器处理HTTP请求的结果: NO_REQUEST表示请求不完整需要继续读取客户数据;* GET_REQUEST表示获得了一个完整的客户请求;* BAD_REQUEST表示客户请求有语法错误;* FORBIDDEN_REQUEST表示客户对资源没有足够的访问权限;* INTERNAL_ERROR表示服务内部错误;* CLOSED_CONNECTION表示客户端已经关闭连接了 */
enum HTTP_CODE{NO_REQUEST,GET_REQUEST,BAD_REQUEST,FORBIDDEN_REQUEST,INTERNAL_ERROR,CLOSED_CONNECTION
};/* 为了监护问题我们没有给客户端发送一个完整的HTTP应答报文* 而只是根据服务器的处理结果发送如下成功或失败信息 */
static const char *szret[] {I get a correct result\n,
Somethin wrong\n};/* 从状态机用于解析出一行内容 */
LINE_STATUS parse_line(char *buffer, int checked_index, int read_index)
{char temp;/* checked_index指向buffer(应用程序的读缓冲区)中当前正在分析的字节* read_index指向buffer中客户数据的尾部的下一字节buffer中第0~checked_index* 字节都已经分析完毕第checked_index~(read_index - 1)字节由下面的循环挨个分析 */for (; checked_index read_index; checked_index){/* 获得当前要分析的字节 */temp buffer[checked_index];/* 如果当前的字节是\r, 即回车符则说明可能读取到一个完整的行 */if (temp \r){/* 如果\r字符碰巧是目前buffer中的最后一个已经被读入的客户数据* 那么分析没有读取到一个完整的行返回LINE_OPEN以表示还需要继续* 读取客户数据才能进一步分析 */if ((checked_index 1) read_index){return LINE_OPEN;}/* 如果下一个字符是“\n”,则说明我们成功读取到一个完整的行 */else if (buffer[checked_index 1] \n){buffer[checked_index] \0;buffer[checked_index] \0;return LINE_OK;}/* 否则的话说明客户范松的HTTP请求存在语法问题 */return LINE_BAD;}/* 如果当前的字节是\n, 即换行符则也说明可能读取到一个完整的行 */else if (temp \n){if ((checked_index 1) buffer[checked_index - 1] \r){buffer[checked_index] \0;buffer[checked_index] \0;return LINE_OK;}return LINE_BAD;}}/* 如果所有内容都分析完毕也没有遇到\r字符则返回LINE_OPEN* 表示还需要继续读取客户数据才能进一步分析 */return LINE_OPEN;
}/* 分析请求行 */
HTTP_CODE parse_requestline(char *temp, CHECK_STATE checkstate)
{char *url strpbrk(temp, \t);/* 如果请求航中没有空白字符或“\t”字符则HTTP请求必有问题 */if (!url){return BAD_REQUEST;}*url \0;char *method temp;if (strcasecmp(method, GET) 0) /* 仅支持GET方法 */{printf(The request method is GET\n);}else{return BAD_REQUEST;}url strspn(url, \t);char *version strpbrk(url, \t);if (!version){return BAD_REQUEST;}*version \0;version strspn(version, \t);/* 仅支持HTTP/1.1 */if (strcasecmp(version, HTTP/1.1) ! 0){return BAD_REQUEST;}/* 检查URL是否合法 */if (strncasecmp(url, http://, 7) 0){url 7;url strchr(url, /);}if (!url || url[0] ! /){return BAD_REQUEST;}printf(The request URL is %s\n, url);/* HTTP 请求行处理完毕状态转移到头部字段的分析 */checkstate CHECK_STATE_HEADER;return NO_REQUEST;
}/* 分析头部字段 */
HTTP_CODE parse_headers(char *temp)
{/* 遇到一个空行说明我们得到了一个正确的HTTP请求 */if (temp[0] \0){return GET_REQUEST;}else if (strncasecmp(temp, Host:, 5) 0) /* 处理HOST头部字段 */{temp 5;temp strspn(temp, \t);printf(the request host is: %s\n, temp);}else /* 其他头部字段暂不处理 */{printf(I can not handle this header\n);}return NO_REQUEST;
}/* 分析HTTP请求的入口函数 */
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; /* 记录HTTP请求的处理结果 *//* 主状态机用于从buffer中取出所有完整的行 */while ((linestatus parse_line(buffer, checked_index, read_index)) LINE_OK){char *temp buffer start_line; /* start_line是行在buffer中的起始位置 */start_line checked_index; /* 记录下一行的起始位置 *//* checkstate 记录主状态机当前的状态 */switch(checkstate){case CHECK_STATE_REQUESTLINE: /* 第一个状态分析请求行 */{retcode parse_requestline(temp, checkstate);if (retcode BAD_REQUEST){return BAD_REQUEST;}break;}case CHECK_STATE_HEADER: /* 第二个状态分析头部字段 */{retcode parse_headers(temp);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; /* 行在buffer中的起始位置 *//* 设置主状态机的初始状态 */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); /* 尚未得到一个完整的HTTP请求 */if (result NO_REQUEST){continue;}else if (result GET_REQUEST) /* 得到一个完整的、正确的HTTP请求 */{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;
}8.7 提高服务器性能的其他建议
软件上系统软件资源OS允许用户打开的最大文件描述符数量、服务器程序本身。 池用服务器硬件资源换取运行效率。池是一组资源的集合在服务器启动时就被完全创建好并初始化静态资源分配。直接从池中获取资源也可在处理完客户连接后放回池中。池相当于服务器管理系统资源的应用层设施避免对内核的频繁访问。内存池用于socket的接收缓存和发送缓存。比如HTTP请求预先分配足够大的接收缓存区。 进程池、线程池需要工作进程或线程处理客户请求时从池中取得执行实体无须动态调用fork或pthread_create。 连接池用于服务器或服务器机群的内部永久连接。如服务器预先和DB建立连接。 数据复制内核直接处理从socket或者文件读入的数据如ftp服务器使用”零拷贝“函数sendfile使用共享内存而不是管道或消息队列指针。 上下文切换和锁进程切换或线程切换导致的的系统开销半同步/半异步模式一个线程同时处理多个客户连接、多线程服务器的一个优点是不同的线程可以同时运行在不同的CPU上当线程的数量不大于CPU的数目时上下文的切换就不是问题共享资源的加锁保护高效的半同步/半异步模式、减小锁的粒度使用读写锁读一块共享内存不增加开销写内存才会锁。**