服务器项目实战与总结(五)

服务器项目实战与总结(五)

阻塞和非阻塞、同步和异步

 

 

 

 

 

 同步:是应用程序自己主动读取的,是从内核中的TCP接收缓冲区的数据主动搬到用户区,比如recv/read函数。

异步:不是应用程序自己主动读取的,应用程序只需要告诉操作系统通信方式等,然后操作系统搬运数据到用户区,并通知应用程序它把数据搬运好了。

 

Unix、Linux上的五种IO模型

 

 

 

a. 阻塞

调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作。

 

 

 

b.非阻塞(NIO)

非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据 errno 区分这两种情况,对于accept,recv 和 send,事件未发生时,errno 通常被设置成 EAGAIN。

 

 

 

c.IO复用

非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据 errno 区分这两种情况,对于accept,recv 和 send,事件未发生时,errno 通常被设置成 EAGAIN。

 

 

 d.信号驱动

Linux 用套接口进行信号驱动 IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进
程收到SIGIO 信号,然后处理 IO 事件。

内核在第一个阶段是异步,在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需
要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率。

 

 

 e.异步

Linux中,可以调用 aio_read 函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方
式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。

 

 

 

 

 

 

Web服务器简介及HTTP协议

一个 Web Server 就是一个服务器软件(程序),或者是运行这个服务器软件的硬件(计算机)。其主
要功能是通过 HTTP 协议与客户端(通常是浏览器(Browser))进行通信,来接收,存储,处理来自
客户端的 HTTP 请求,并对其请求做出 HTTP 响应,返回给客户端其请求的内容(文件、网页等)或返
回一个 Error 信息。

 

 

 

通常用户使用 Web 浏览器与相应服务器进行通信。在浏览器中键入“域名”或“IP地址:端口号”,浏览器则
先将你的域名解析成相应的 IP 地址或者直接根据你的IP地址向对应的 Web 服务器发送一个 HTTP 请
求。这一过程首先要通过 TCP 协议的三次握手建立与目标 Web 服务器的连接,然后 HTTP 协议生成针
对目标 Web 服务器的 HTTP 请求报文,通过 TCP、IP 等协议发送到目标 Web 服务器上。

HTTP协议(应用层的协议)

 

 

 

 

 

 

 

 

 

https协议默认端口是443

HTTP 请求报文格式

 

 

 

例如,HTTP请求头部原始信息:

 1 GET /topics/391887078 HTTP/2
 2 Host: bbs.csdn.net
 3 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0
 4 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
 5 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
 6 Accept-Encoding: gzip, deflate, br
 7 Referer: https://www.baidu.com/link?url=NZx_amw_JwKNfYbX5eBYtLsusVpiKpup9kp5fn7G5m5y6T0Je5Xdw8j4Vw2erCor&wd=&eqid=b590649600029c9000000005621f30a1
 8 Connection: keep-alive
 9 Cookie: uuid_tt_dd=10_37405684820-1640156095208-848782; log_Id_pv=43; Hm_lvt_6bcd52f51e9b3dce32bec4a3997715ac=1646229342,1646381047,1646724951,1646724980; Hm_up_6bcd52f51e9b3dce32bec4a3997715ac=%7B%22islogin%22%3A%7B%22value%22%3A%221%22%2C%22scope%22%3A1%7D%2C%22isonline%22%3A%7B%22value%22%3A%221%22%2C%22scope%22%3A1%7D%2C%22isvip%22%3A%7B%22value%22%3A%220%22%2C%22scope%22%3A1%7D%2C%22uid_%22%3A%7B%22value%22%3A%22qq_59374912%22%2C%22scope%22%3A1%7D%7D; Hm_ct_6bcd52f51e9b3dce32bec4a3997715ac=6525*1*10_37405684820-1640156095208-848782!5744*1*qq_59374912; log_Id_view=422; log_Id_click=15; __gads=ID=6a47c56ca9b87d48-22fa344982cf0021:T=1640156105:RT=1640156105:S=ALNI_MbXxcGg5d8Q8Gk0uY7yiJkJlILCjw; ssxmod_itna=eqfhGIxRhGkDCzDXbRx0InqDq7I5GQ=G3qW=beDl=CYxA5D8D6DQeGTbu5Cb3=HYjEh3h1jADnKFkBA+uRdkFt7f3pkmDb4GLDmKDySj10FDx1q0rD74irDDxD3ExWKDwDlKDgDQKZyExDaDGck302kDimHSr5sDiH5ot0DXxG1DQ5DsrGIklKD06OvkDDd33opuY4DUDRDj94LeCrqqDi3fmKzBx0OD09sDme7l2fCyDGa4vA0I1LF5CpdsxKDmh5DyhgkyeBka2SKDjukavrgDGHKbmcbOE0xYAm41Gb5cLnq=GXP6VVKDDW5B4VkD4D==; ssxmod_itna2=eqfhGIxRhGkDCzDXbRx0InqDq7I5GQ=G3qW=D6h92D0H9K03G1=2je6qN27u5SSnTK=12m=ArLqZS1gbd42Dn+0m6ja6vbiW64pgnUHXi1jbzYatAMyAyC+=I6Llmzal2s38=15aI0kxX+TjxAfj1SqmqZT6/b+393Qb5y4RRlu3plT3d2DR03BbVBrQySWbzD=aVAMxYPSn4uCGqXbKVUaoO6ZbsVefrvbhdOWbmG+efATEqRltpY0WSWtLeHL2u8ELm=RX7iBMkMGkf+/6EbWOuK4PDKkwFD7=DekqxD==; UserName=qq_59374912; UserInfo=5407bd51ea8f4231b7daaea5cb1b87d9; UserToken=5407bd51ea8f4231b7daaea5cb1b87d9; UserNick=%E5%AE%9E%E5%B9%B2375; AU=1B5; UN=qq_59374912; BT=1641803856440; p_uid=U000000; dc_sid=186a79b8d2fbe0fa93b76b7d5315051b; c_pref=https%3A//www.baidu.com/link; c_ref=https%3A//www.baidu.com/link; c_first_ref=www.baidu.com; c_first_page=https%3A//bbs.csdn.net/topics/391887078; c_segment=14; Hm_lpvt_6bcd52f51e9b3dce32bec4a3997715ac=1646724980; c_dl_prid=1646211467077_827614; c_dl_rid=1646211629296_539584; c_dl_fref=https://www.baidu.com/link; c_dl_fpage=/download/zsf250/10880412; c_dl_um=distribute.pc_aggpage_search_result.none-task-download-2%7Eaggregatepage%7Efirst_rank_ecpm_v1%7Erank_v31_ecpm-1-10880412.pc_agg_new_rank; csrfToken=2vdK7sXuLQapQ9JA68jKqDfd; dc_session_id=10_1646548189085.802620; dc_tos=r8f14j; c_page_id=default; FCNEC=[["AKsRol-GBWj_ZDmUuf7GGbQB1YuX6J-HDa1B1I-ytK-zIMCkE1mtQgjQ28bO9HJNF87kKMUQCuzsKtiOAGWX4dqza2ErJx3HxQCdz_-8DFKoP7wHFBPoWlCTmOqHJtkOJhbH_0_VJHXhmL8MAFwf4QlHl3y7Sf1WNg=="],null,[]]
10 Upgrade-Insecure-Requests: 1
11 Sec-Fetch-Dest: document
12 Sec-Fetch-Mode: navigate
13 Sec-Fetch-Site: cross-site
14 Cache-Control: max-age=0

 

HTTP响应报文格式

 

 

 例如:

 1 HTTP/2 200 OK
 2 server: openresty
 3 date: Tue, 08 Mar 2022 07:43:54 GMT
 4 content-type: text/html; charset=utf-8
 5 vary: Accept-Encoding
 6 x-response-time: 118
 7 x-xss-protection: 1; mode=block
 8 x-content-type-options: nosniff
 9 x-download-options: noopen
10 x-readtime: 118
11 strict-transport-security: max-age=31536000
12 content-encoding: gzip
13 X-Firefox-Spdy: h2

 

HTTP请求方法

HTTP/1.1 协议中共定义了八种方法(也叫“动作”)来以不同方式操作指定的资源:

1. GET:向指定的资源发出“显示”请求。使用 GET 方法应该只用在读取数据,而不应当被用于产生“副
作用”的操作中,例如在 Web Application 中。其中一个原因是 GET 可能会被网络蜘蛛等随意访
问。
2. HEAD:与 GET 方法一样,都是向服务器发出指定资源的请求。只不过服务器将不传回资源的本文
部分。它的好处在于,使用这个方法可以在不必传输全部内容的情况下,就可以获取其中“关于该
资源的信息”(元信息或称元数据)。
3. POST:向指定资源提交数据,请求服务器进行处理(例如提交表单或者上传文件)。数据被包含
在请求本文中。这个请求可能会创建新的资源或修改现有资源,或二者皆有。
4. PUT:向指定资源位置上传其最新内容。
5. DELETE:请求服务器删除 Request-URI 所标识的资源。
6. TRACE:回显服务器收到的请求,主要用于测试或诊断。
7. OPTIONS:这个方法可使服务器传回该资源所支持的所有 HTTP 请求方法。用'*'来代替资源名称,
向 Web 服务器发送 OPTIONS 请求,可以测试服务器功能是否正常运作。
8. CONNECT:HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。通常用于SSL加密服
务器的链接(经由非加密的 HTTP 代理服务器)。

 

补充,面试考:get和post请求的区别

GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。

 

GET和POST还有一个重大区别,简单的说:

GET产生一个TCP数据包;POST产生两个TCP数据包。

长的说:

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

也就是说,GET只需要汽车跑一趟就把货送到了,而POST得跑两趟,第一趟,先去和服务器打个招呼“嗨,我等下要送一批货来,你们打开门迎接我”,然后再回头把货送过去。

因为POST需要两步,时间上消耗的要多一点,看起来GET比POST更有效。因此Yahoo团队有推荐用GET替换POST来优化网站性能。但这是一个坑!跳入需谨慎。为什么?

1. GET与POST都有自己的语义,不能随便混用。

2. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。

3. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

 

HTTP状态码

 

 

 

服务器编程基本框架和两种高效的事件处理模式

服务器编程基本框架

虽然服务器程序种类繁多,但其基本框架都一样,不同之处在于逻辑处理。

 

 

 

 

  两种高效的事件处理模式

服务器程序通常需要处理三类事件:I/O 事件、信号及定时事件。有两种高效的事件处理模式:Reactor
和 Proactor,同步 I/O 模型通常用于实现 Reactor 模式,异步 I/O 模型通常用于实现 Proactor 模式。

Reactor模式

要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作
线程(逻辑单元),将 socket 可读可写事件放入请求队列,交给工作线程处理。除此之外,主线程不做
任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
使用同步 I/O(以 epoll_wait 为例)实现的 Reactor 模式的工作流程是:
1. 主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
2. 主线程调用 epoll_wait 等待 socket 上有数据可读。
3. 当 socket 上有数据可读时, epoll_wait 通知主线程。主线程则将 socket 可读事件放入请求队列。
4. 睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后往 epoll
内核事件表中注册该 socket 上的写就绪事件。
5. 当主线程调用 epoll_wait 等待 socket 可写。
6. 当 socket 可写时,epoll_wait 通知主线程。主线程将 socket 可写事件放入请求队列。
7. 睡眠在请求队列上的某个工作线程被唤醒,它往 socket 上写入服务器处理客户请求的结果。

 

Reactor 模式的工作流程:

 

 Proactor模式

Proactor 模式将所有 I/O 操作都交给主线程和内核来处理(进行读、写),工作线程仅仅负责业务逻
辑。使用异步 I/O 模型(以 aio_read 和 aio_write 为例)实现的 Proactor 模式的工作流程是:
1. 主线程调用 aio_read 函数向内核注册 socket 上的读完成事件,并告诉内核用户读缓冲区的位置,
以及读操作完成时如何通知应用程序(这里以信号为例)。
2. 主线程继续处理其他逻辑。
3. 当 socket 上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据
已经可用。
4. 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求
后,调用 aio_write 函数向内核注册 socket 上的写完成事件,并告诉内核用户写缓冲区的位置,以
及写操作完成时如何通知应用程序。
5. 主线程继续处理其他逻辑。
6. 当用户缓冲区的数据被写入 socket 之后,内核将向应用程序发送一个信号,以通知应用程序数据
已经发送完毕。
7. 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket。

Proactor 模式的工作流程:

 

 模拟 Proactor 模式

使用同步 I/O 方式模拟出 Proactor 模式。原理是:主线程执行数据读写操作,读写完成之后,主线程向
工作线程通知这一”完成事件“。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下
来要做的只是对读写的结果进行逻辑处理。
使用同步 I/O 模型(以 epoll_wait为例)模拟出的 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 上写入服务器处理客户请求的结果。

同步 I/O 模拟 Proactor 模式的工作流程:

 

 

线程同步机制类封装及线程池实现

线程池

线程池是由服务器预先创建的一组子线程,线程池中的线程数量应该和 CPU 数量差不多。线程池中的所
有子线程都运行着相同的代码。当有新的任务到来时,主线程将通过某种方式选择线程池中的某一个子
线程来为之服务。相比与动态的创建子线程,选择一个已经存在的子线程的代价显然要小得多。至于主
线程选择哪个子线程来为新任务服务,则有多种方式:
主线程使用某种算法来主动选择子线程。最简单、最常用的算法是随机算法和 Round Robin(轮流
选取)算法,但更优秀、更智能的算法将使任务在各个工作线程中更均匀地分配,从而减轻服务器
的整体压力。
主线程和所有子线程通过一个共享的工作队列来同步,子线程都睡眠在该工作队列上。当有新的任
务到来时,主线程将任务添加到工作队列中。这将唤醒正在等待任务的子线程,不过只有一个子线
程将获得新任务的”接管权“,它可以从工作队列中取出任务并执行之,而其他子线程将继续睡眠在
工作队列上。

 

线程池的一般模型为:

 

 

代码

locker.c

  1 #ifndef LOCKER_H
  2 #define LOCKER_H
  3 
  4 #include <exception>
  5 #include <pthread.h>
  6 #include <semaphore.h>
  7 
  8 // 线程同步机制封装类
  9 
 10 // 互斥锁类
 11 class locker {
 12 public:
 13     locker() {
 14         if(pthread_mutex_init(&m_mutex, NULL) != 0) {
 15             throw std::exception();
 16         }
 17     }
 18 
 19     ~locker() {
 20         pthread_mutex_destroy(&m_mutex);
 21     }
 22 
 23     bool lock() {
 24         return pthread_mutex_lock(&m_mutex) == 0;
 25     }
 26 
 27     bool unlock() {
 28         return pthread_mutex_unlock(&m_mutex) == 0;
 29     }
 30 
 31     pthread_mutex_t *get()
 32     {
 33         return &m_mutex;
 34     }
 35 
 36 private:
 37     pthread_mutex_t m_mutex;
 38 };
 39 
 40 
 41 // 条件变量类
 42 class cond {
 43 public:
 44     cond(){
 45         if (pthread_cond_init(&m_cond, NULL) != 0) {
 46             throw std::exception();
 47         }
 48     }
 49     ~cond() {
 50         pthread_cond_destroy(&m_cond);
 51     }
 52 
 53     bool wait(pthread_mutex_t *m_mutex) {
 54         int ret = 0;
 55         ret = pthread_cond_wait(&m_cond, m_mutex);
 56         return ret == 0;
 57     }
 58     bool timewait(pthread_mutex_t *m_mutex, struct timespec t) {
 59         int ret = 0;
 60         ret = pthread_cond_timedwait(&m_cond, m_mutex, &t);
 61         return ret == 0;
 62     }
 63     bool signal() {
 64         return pthread_cond_signal(&m_cond) == 0;
 65     }
 66     bool broadcast() {
 67         return pthread_cond_broadcast(&m_cond) == 0;
 68     }
 69 
 70 private:
 71     pthread_cond_t m_cond;
 72 };
 73 
 74 
 75 // 信号量类
 76 class sem {
 77 public:
 78     sem() {
 79         if( sem_init( &m_sem, 0, 0 ) != 0 ) {
 80             throw std::exception();
 81         }
 82     }
 83     sem(int num) {
 84         if( sem_init( &m_sem, 0, num ) != 0 ) {
 85             throw std::exception();
 86         }
 87     }
 88     ~sem() {
 89         sem_destroy( &m_sem );
 90     }
 91     // 等待信号量
 92     bool wait() {
 93         return sem_wait( &m_sem ) == 0;
 94     }
 95     // 增加信号量
 96     bool post() {
 97         return sem_post( &m_sem ) == 0;
 98     }
 99 private:
100     sem_t m_sem;
101 };
102 
103 #endif

 

threadpoll.c

  1 #ifndef THREADPOOL_H
  2 #define THREADPOOL_H
  3 
  4 #include <list>
  5 #include <cstdio>
  6 #include <exception>
  7 #include <pthread.h>
  8 #include "locker.h"
  9 
 10 // 线程池类,将它定义为模板类是为了代码复用,模板参数T是任务类
 11 template<typename T>
 12 class threadpool {
 13 public:
 14     /*thread_number是线程池中线程的数量,max_requests是请求队列中最多允许的、等待处理的请求的数量*/
 15     threadpool(int thread_number = 8, int max_requests = 10000);
 16     ~threadpool();
 17     bool append(T* request);
 18 
 19 private:
 20     /*工作线程运行的函数,它不断从工作队列中取出任务并执行之*/
 21     static void* worker(void* arg);
 22     void run();
 23 
 24 private:
 25     // 线程的数量
 26     int m_thread_number;  
 27     
 28     // 描述线程池的数组,大小为m_thread_number    
 29     pthread_t * m_threads;
 30 
 31     // 请求队列中最多允许的、等待处理的请求的数量  
 32     int m_max_requests; 
 33     
 34     // 请求队列
 35     std::list< T* > m_workqueue;  
 36 
 37     // 保护请求队列的互斥锁
 38     locker m_queuelocker;   
 39 
 40     // 是否有任务需要处理
 41     sem m_queuestat;
 42 
 43     // 是否结束线程          
 44     bool m_stop;                    
 45 };
 46 
 47 template< typename T >
 48 threadpool< T >::threadpool(int thread_number, int max_requests) : 
 49         m_thread_number(thread_number), m_max_requests(max_requests), 
 50         m_stop(false), m_threads(NULL) {
 51 
 52     if((thread_number <= 0) || (max_requests <= 0) ) {
 53         throw std::exception();
 54     }
 55 
 56     m_threads = new pthread_t[m_thread_number];
 57     if(!m_threads) {
 58         throw std::exception();
 59     }
 60 
 61     // 创建thread_number 个线程,并将他们设置为脱离线程。
 62     for ( int i = 0; i < thread_number; ++i ) {
 63         printf( "create the %dth thread\n", i);
 64         if(pthread_create(m_threads + i, NULL, worker, this ) != 0) {
 65             delete [] m_threads;
 66             throw std::exception();
 67         }
 68         
 69         if( pthread_detach( m_threads[i] ) ) {
 70             delete [] m_threads;
 71             throw std::exception();
 72         }
 73     }
 74 }
 75 
 76 template< typename T >
 77 threadpool< T >::~threadpool() {
 78     delete [] m_threads;
 79     m_stop = true;
 80 }
 81 
 82 template< typename T >
 83 bool threadpool< T >::append( T* request )
 84 {
 85     // 操作工作队列时一定要加锁,因为它被所有线程共享。
 86     m_queuelocker.lock();
 87     if ( m_workqueue.size() > m_max_requests ) {
 88         m_queuelocker.unlock();
 89         return false;
 90     }
 91     m_workqueue.push_back(request);
 92     m_queuelocker.unlock();
 93     m_queuestat.post();
 94     return true;
 95 }
 96 
 97 template< typename T >
 98 void* threadpool< T >::worker( void* arg )
 99 {
100     threadpool* pool = ( threadpool* )arg;
101     pool->run();
102     return pool;
103 }
104 
105 template< typename T >
106 void threadpool< T >::run() {
107 
108     while (!m_stop) {
109         m_queuestat.wait();
110         m_queuelocker.lock();
111         if ( m_workqueue.empty() ) {
112             m_queuelocker.unlock();
113             continue;
114         }
115         T* request = m_workqueue.front();
116         m_workqueue.pop_front();
117         m_queuelocker.unlock();
118         if ( !request ) {
119             continue;
120         }
121         request->process();
122     }
123 
124 }
125 
126 #endif

 

  项目整体流程代码实现

 有限状态机

 

 

 

 EPOLLONESHOT事件

 

 服务器压力测试

 

 

 

参考链接:https://www.cnblogs.com/logsharing/p/8448446.html

 

posted @ 2022-03-08 18:02  白雪儿  Views(369)  Comments(0Edit  收藏  举报