꧁ 大家好,我是 兔7 ,一位努力学习C++的博主~ ꧂
☙ 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步❧
🚀 如有不懂,可以随时向我提问,我会全力讲解~💬
🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!👀
🔥 你们的支持是我创作的动力!⛅
🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证!⭐
🧸 人的心态决定姿态!⭐
🚀 本文章CSDN首发!✍
目录
0. 前言
此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。
大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~
感谢大家对我的支持,感谢大家的喜欢, 兔7 祝大家在学习的路上一路顺利,生活的路上顺心顺意~!
1. IO
IO是站在硬件角度进行输入输出。
那么是谁在IO呢?
在大多数,是进程或者线程!当然还有操作系统。
网络的本质其实就是IO!
我们发送数据的本质就是TCP/IP/mac帧拷贝到网卡的发送缓冲区里,或者拷贝到网卡的设备中,然后网卡再根据局域网原理把数据发送出去,发是好发的,但是现在有个问题:网卡上有数据OS如何得知?
第一种方法是操作系统主动去检测网卡,所以我们就知道,如果是这样,那么检测的时候就会有两种情况,网卡里有数据,网卡里没数据,这样就势必会引起操作系统的效率降低的问题,因为操作系统去检测,肯定大多数时间内网卡里并没有数据。
所以冯诺依曼体系结构是采用 硬件中断+OS进行中断处理程序 完成的。
我们之前说过,CPU并不直接和外设打交道,指的是数据层面,所以有些控制信号是可以直接和外设打交道的,CPU上是有很多的针脚的。当发完硬件中断后(中断号对应处理方法),操作系统就知道网卡里有数据了,就会进行一些列的操作
这样操作系统就有了一个维护大量报文的相关数据结构(上面写的是伪代码)。
IO一般分两步进行:
- 等待IO就绪
- 拷贝IO数据到内核或者到外设
IO = 等 + 拷贝,本质上,IO中真正有效的是拷贝。
那么什么叫做高效的IO过程呢?其实并不是我一次拷贝的多了,在硬件上相对是比较确定的,网络中读取的字节数也基本是固定的,所以在硬件上提高效率是很困难的。
所以高效的IO是指:在特定的时间内,大大减少等的比重,增加拷贝的比重,这才是高效的IO!
举个钓鱼的例子:
请问张三、李四、王五的钓鱼效率哪个高?
先说答案:钓鱼的效率是一样的!大家可能有些惊讶,接下来看解释。
它们钓鱼的方式是一样的,对于鱼来说鱼杆数就3个,鱼咬哪个的概率都是一样的,对于这三个人来说,每人只有一个鱼竿,而且无非就是等、钓,有鱼上钩、没鱼上钩,所以它们的效率是一样的,所以他们三个人展现出来的方式不一样是因为他们等的方式不一样,仅此而已,所以我们这里问的是他们钓鱼的效率是否一样,而不是他们做事情的效率是否一样。
赵六呢?
赵六的效率肯定是最高的,比方说赵六现在有97个鱼竿吧。如果鱼咬勾的概率相同,那么鱼咬张三、李四、王五的都是1%,咬赵六的是97%。
所以单位时间内,赵六的至少一个鱼竿上有鱼的概率是其它三人的97倍。
所以单位时间内,赵六"钓"的比重是非常高的,"等"的比重是很低的。
那么为什么赵六的效率高呢?本质上是因为赵六一次等待多个鱼竿,可以将"等"的时间重叠
我们将:
- 张三的等待方式:阻塞IO。
- 李四的等待方式:非阻塞IO。
- 王五的等待方式:信号驱动IO。
- 赵六的等待方式:多路转接(多路复用)。
所以我们现在就知道了,阻塞IO、非阻塞IO、信号驱动不能提高IO的效率,但是可以提高做事的效率。因为单位时间内,IO就绪的效率是没有变化的。
而如果一次等待多个鱼竿(文件描述符),就可以提高我们等的效率。
此时又来了个田七,田七不喜欢钓鱼,喜欢吃鱼,所以他让他的秘书小李去给他钓鱼,如果掉满了一桶鱼给田七打电话,然后田七过来拿鱼。那么此时田七想干什么就干什么,只要小李给他打电话,他就过来拿就行。
对于田七而言,小李帮他钓鱼的这种IO就叫做异步IO,也就是你不用等,也不用拷贝数据,你只要提供一个缓冲区(水桶),提供一种数据就绪时的一种方式(电话),给一个读取数据时的文件描述符(鱼竿),然后操作系统就帮你去读了,到时候直接往缓冲区去拿就行了。
张三、李四、王五、赵六全部都是同步IO:等必须自己等,拷贝必须自己拷贝。
2. 五种IO模型
阻塞IO:在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。
阻塞IO是最常见的IO模型。
所谓的阻塞是用户层的感受,在内核中本质是进程被挂起了(S or T or D,总之是非R),等待数据就绪,这件等待某种事件就绪的操作是由OS来做。阻塞是由操作系统发起,由操作系统完成。
非阻塞IO:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一 般只有特定场景下才使用。
所谓的非阻塞轮询的本质是在做时间就绪的检测工作,这个工作是由OS来做,但是这个工作的发起者是用户。
信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
IO多路转接:虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。
我们平常调用的recv、read、send、write都是做的等、拷贝这样的工作,而且我们在调用它们的时候最关注的还是拷贝这个功能,而且从语义上来讲,用处就是拷贝,那么我们可不可以只让它们去做拷贝呢?
在系统调用接口中有select/poll/epoll这样的接口,它们是专门用来等的(一次等多个文件描述符),而不做拷贝,也就是说等的需求交给多路转接接口,等到等就绪了,再调用recv、read、send、write去拷贝(它们要是负责等的话只能等一个文件描述符)。
异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。
小结
任何IO过程中,都包含两个步骤。第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。让IO更高效,最核心的办法就是让等待的时间尽量少。
3. 高级IO重要概念
3.1 同步通信 vs 异步通信
同步和异步关注的是消息通信机制。
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
- 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
另外,我在讲多进程多线程的时候,也提到同步和互斥。这里的同步通信和进程之间的同步是完全不想干的概念。
- 进程/线程同步也是进程/线程之间直接的制约关系。
- 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、 传递信息所产生的制约关系。尤其是在访问临界资源的时候。
以后在看到 "同步" 这个词,一定要先搞清楚大背景是什么。这个同步,是同步通信异步通信的同步,还是同步与互斥的同步。
3.2 阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
- 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
4. 非阻塞IO
4.1 fcntl
一个文件描述符,默认都是阻塞IO。
函数原型如下:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
传入的cmd的值不同, 后面追加的参数也不相同.
fcntl函数有5种功能:
- 复制一个现有的描述符(cmd=F_DUPFD)。
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)。
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)。
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)。
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)。
我们此处只是用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞。
1 #include <iostream>
2 #include <unistd.h>
3 #include <fcntl.h>
4
5
6 #define NUM 1024
7
8 int main()
9 {
10 while(true){
11 char buffer[NUM];
12 ssize_t size = read(0, buffer, sizeof(buffer) - 1);
13 if(size < 0){
14 std::cerr << "read error: " << size << std::endl;
15 break;
16 }
17 buffer[size] = 0;
18 std::cout << "echo# " << buffer << std::endl;
19 }
20 return 0;
21 }
我们可以看到现在read是被阻塞住的。
让我们输入的时候它就读取到了我的输入,并且把我的输入回显了出来,它读到数据的本质是它在阻塞时突然有数据了,然后直接从内核拷贝到buffer缓冲区中,然后把buffer打印出来,这就是读事件就绪。
1 #include <iostream>
2 #include <unistd.h>
3 #include <fcntl.h>
4
5
6 #define NUM 1024
7
8 bool SetNonBlock(int fd)
9 {
10 int fl = fcntl(fd, F_GETFL);
11 if(fl < 0){
12 std::cerr << "fcntl error" << std::endl;
13 return false;
14 }
15
16 fcntl(fd, F_SETFL, fl | O_NONBLOCK);
17 return true;
18 }
19
20 int main()
21 {
22 SetNonBlock(0);
23 while(true){
24 char buffer[NUM];
25 ssize_t size = read(0, buffer, sizeof(buffer) - 1);
26 if(size < 0){
27 // size小于0不一定一定错了,也可能是底层没有数据
28 std::cerr << "read error: " << size << std::endl;
29 break;
30 }
31 buffer[size] = 0;
32 std::cout << "echo# " << buffer << std::endl;
33 }
34 return 0;
35 }
注意,我循环里写判断的时候是以break判断的。当我们运行的时候发现直接就返回了这个读取出错了,因为底层没数据,它就返回了,它是以出错的形式返回的!那么我们能认为:就意味着出错了么?肯定是不可以的,那么我们怎么判断是真的出错了还是这种以出错的形式返回的呢?
1 #include <iostream>
2 #include <unistd.h>
3 #include <fcntl.h>
4
5
6 #define NUM 1024
7
8 bool SetNonBlock(int fd)
9 {
10 int fl = fcntl(fd, F_GETFL);
11 if(fl < 0){
12 std::cerr << "fcntl error" << std::endl;
13 return false;
14 }
15
16 fcntl(fd, F_SETFL, fl | O_NONBLOCK);
17 return true;
18 }
19
20 int main()
21 {
22 SetNonBlock(0);
23 while(true){
24 char buffer[NUM];
25 ssize_t size = read(0, buffer, sizeof(buffer) - 1);
26 if(size < 0){
27 // size小于0不一定一定错了,也可能是底层没有数据
28 // 这两种errno就是没有数据,它们两个的errno都是11
29 if(errno == EAGAIN || errno == EWOULDBLOCK){
30 std::cout << "底层的数据没有就绪,你再轮询检测一下,try again!" << std::endl;
31 sleep(1);
32 }
33 else{
34 std::cerr << "read error: " << size << std::endl;
35 }
36 continue;
37 //break;
38 }
39 buffer[size] = 0;
40 std::cout << "echo# " << buffer << std::endl;
41 }
42 return 0;
43 }
我们可以看到,在我们没有输入的时候,它就轮询的检测有没有数据,有数据它就读到了,然后回显过来。
除了没有数据就绪会出错之外,如果在进行的读取的时候被信号中断了还是会出错的,所以我们还是要解决这个问题。
1 #include <iostream>
2 #include <unistd.h>
3 #include <fcntl.h>
4
5
6 #define NUM 1024
7
8 bool SetNonBlock(int fd)
9 {
10 int fl = fcntl(fd, F_GETFL);
11 if(fl < 0){
12 std::cerr << "fcntl error" << std::endl;
13 return false;
14 }
15
16 fcntl(fd, F_SETFL, fl | O_NONBLOCK);
17 return true;
18 }
19
20 int main()
21 {
22 SetNonBlock(0);
23 while(true){
24 char buffer[NUM];
25 ssize_t size = read(0, buffer, sizeof(buffer) - 1);
26 if(size < 0){
27 // size小于0不一定一定错了,也可能是底层没有数据
28 // 这两种errno就是没有数据,它们两个的errno都是11
29 if(errno == EAGAIN || errno == EWOULDBLOCK){
30 std::cout << "底层的数据没有就绪,你再轮询检测一下,try again!" << std::endl;
31 sleep(1);
32 continue;
33 }
34 // 被信号中断了
35 else if(errno == EINTR){
36 std::cout << "底层的数据就绪为止,被信号打断" << std::endl;
37 continue;
38 }
39 else{
40 std::cerr << "read error: " << size << std::endl;
41 break;
42 }
43 continue;
44 //break;
45 }
46 buffer[size] = 0;
47 std::cout << "echo# " << buffer << std::endl;
48 }
49 return 0;
50 }
而且我们要知道,底层数据没有就绪、被信号打断这种错误都应该继续往下轮询,只有真正的读取出错了才需要break。
5. I/O多路转接之select
5.1 初识select
系统提供select函数来实现多路复用输入/输出模型。
- select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。
- 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。
select是就绪事件的通知机制,就绪事件:只要底层有数据、只要底层有空间,都叫做select的读事件和写事件就绪,此时就可以recv、read、send、write等,在调用这些接口的时候不会被阻塞住,因为已经确定了有数据了。
select可以一次等待多个文件描述符。
5.2 select函数原型
select的函数原型如下:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数解释:
- 参数nfds是需要监视的最大的文件描述符值+1。
- rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。
- 参数timeout为结构timeval,用来设置select()的等待时间。
参数timeout取值:
- NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
- 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。
关于fd_set结构
其实这个结构就是一个整数数组,更严格的说,是一个 "位图"。使用位图中对应的位来表示要监视的文件描述符。
提供了一组操作fd_set的接口,来比较方便的操作位图。
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
关于timeval结构
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为 0。
函数返回值:
- 执行成功则返回文件描述词状态已改变的个数。
- 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回。
- 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds、writefds、exceptfds和timeout的值变成不可预测。
错误值可能为:
- EBADF:文件描述词为无效的或该文件已关闭。
- EINTR:此调用被信号所中断。
- EINVAL:参数n 为负值。
- ENOMEM:核心内存不足。
我们现在以读文件描述符为例。
而且我们可以看到上面的readfds这个参数是"*",也就是指针,所以所有的fd_set即是输入型参数,又是输出型参数。
而且我们现在更能明白了为什么select可以一次等多个文件描述符了,因为它是存在并传文件描述符集的。
其它的参数都是类似的,所以我们现在知道了,select的参数是这样传的,它的工作方式就是我们上面说的那样,所以select的核心工作就是等底层文件描述符就绪,然后通知上层,所以它叫做就绪事件的通知机制。
5.3 理解select执行过程
由于readfds、writefds、exceptfds,是fd的集合,而且既做输入、又做输出。
还是以读为例!
比方说我们一次一要关心0、1、2、3文件描述符是否就绪了,那么此时我们的位图就应该是:0000 1111(我就这么写了,能理解就行),因为它即是输入型参数又是输出型参数,如果只有2号文件描述符就绪了,此时就返回:0000 0100,这就代表2号文件描述符就绪了,可以进行读了,但是现在有一个问题,因为内核修改了用户的fds,但是第二轮、第三轮...??
所以我们在第二轮、第三轮...的时候应该将文件描述符重新设置成 0000 1111(设置成要检测的文件描述符)。
这是select的特点,也是缺点。
我们现在知道了,select调用,每一次都需要进行对关心的fd进行重新设置,因为在重新设置的时候,readfds已经是被设置的结果,但是在你还没有设置readfds时,你的所有的fd在哪呢?因为我们每次都要设置,所以我们是需要记录下来自己曾经已经打开的套接字的文件描述符。
所以select通常需要借助数组来把自己历史上的所有fd进行保存。
这里写了一段伪代码,就是大致的流程,当然这里有很多的问题,比方说这里的accept,因为在接收的时候还是阻塞的,所以它也要放到select里,也要进行处理。
现在有个问题:在等的时候,如果是链接到来事件就绪呢?其实在链接事件到来,在多路转接看来,都统一当作读事件就绪。
接下来我们就根据上面的伪代码来编写代码。
sock.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <unistd.h>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <netinet/in.h>
10
11
12 namespace ns_sock{
13
14 class Sock{
15 public:
16 static int Socket()
17 {
18 int sock = socket(AF_INET, SOCK_STREAM, 0);
19 if(sock < 0){
20 std::cerr << "socket error" << std::endl;
21 exit(1);
22 }
23 int opt = 1;
24 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
25 return sock;
26 }
27
28 static bool Bind(int sock, unsigned short port)
29 {
30 struct sockaddr_in local;
31 memset(&local, 0, sizeof(local));
32 local.sin_family = AF_INET;
33 local.sin_port = htons(port);
34 local.sin_addr.s_addr = INADDR_ANY;
35
36 if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
37 std::cerr << "bind error" << std::endl;
38 exit(2);
39 }
40 return true;
41 }
42
43 static bool Listen(int sock, int backlog)
44 {
45 if(listen(sock, backlog) < 0){
46 std::cerr << "listen error" << std::endl;
47 exit(3);
48 }
49 return true;
50 }
51 };
52 }
select_server.hpp
1 #pragma once
2
3 #include "sock.hpp"
4 #include <sys/select.h>
5
6 #define BACK_LOG 5
7 #define NUM 1024
8 #define DEFAULT_FD -1
9
10 namespace ns_select{
11
12 class SelectServer{
13 private:
14 int listen_sock;
15 unsigned short port;
16 public:
17 SelectServer(unsigned short _port)
18 :port(_port)
19 {}
20
21 void InitSelectServer()
22 {
23 listen_sock = ns_sock::Sock::Socket();
24 ns_sock::Sock::Bind(listen_sock, port);
25 ns_sock::Sock::Listen(listen_sock, BACK_LOG);
26 }
27
28 void Run()
29 {
30 fd_set rfds;
31 int fd_array[NUM] = {0};
32 ClearArray(fd_array, NUM, DEFAULT_FD);// 用来初始化数据中所有的fd
33
34 fd_array[0] = listen_sock;// 监听套接字sock写入数组的第一个元素
35
36 for(;;)
37 {
38 // 时间也是输入输出,所以如果你是timeout返回,那么就需要对时间也要重新设置
39 struct timeval timeout = {5, 0};// 每个5秒timeout一次
40 // 对所有的合法fd进行在select中重新设定
41 int max_fd = DEFAULT_FD;
42 FD_ZERO(&rfds);//清空所有的read文件描述符集
43 // 第一次循环的时候我们的fd_array数组中至少已经有了一个fd->listen_sock
44 for(auto i = 0; i < NUM; i++)
45 {
46 if(fd_array[i] == DEFAULT_FD){
47 continue;
48 }
49 //到这里说明是需要添加的合法fd
50 FD_SET(fd_array[i], &rfds);
51 if(max_fd < fd_array[i]){
52 max_fd = fd_array[i];
53 }
54 }
55
56 switch(select(max_fd + 1, &rfds, nullptr, nullptr, &timeout))
57 {
58 case 0:
59 std::cout << "time out" << std::endl;
60 break;
61 case -1:
62 std::cerr << "select error" << std::endl;
63 break;
64 default:
65 // 正常的事件处理
66 std::cout << "有事件发生.... timeout: " << timeout.tv_sec << std::endl;
67 break;
68 }
69 }
70 }
71
72 ~SelectServer(){}
73
74 private:
75 void ClearArray(int fd_array[], int num, int default_fd)// 用来初始化数据中的fd
76 {
77 for(auto i = 0; i < num; i++)
78 {
79 fd_array[i] = default_fd;
80 }
81 }
82 };
server.cc
1 #include "select_server.hpp"
2 #include <iostream>
3 #include <string>
4 #include <cstdlib>
5
6
7 static void Usage(std::string proc)
8 {
9 std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
10 }
11
12
13 int main(int argc, char* argv[])
14 {
15 if(argc != 2){
16 Usage(argv[0]);
17 exit(4);
18 }
19
20 unsigned short port = atoi(argv[1]);
21
22 ns_select::SelectServer* select_svr = new ns_select::SelectServer(port);
23 select_svr->InitSelectServer();
24 select_svr->Run();
25
26 return 0;
27 }
其中sock.hpp是用来封装sock套接字的,select_server.hpp是用来封装调用select的,server.cc里直接用select对应的接口。
我们可以看到当我们运行的时候是有这个端口号为8081,进程名为select_server的进程处于监听状态。
我们设置的每隔5秒timeout指的是没有数据的时候一直timeout,有数据的时候timeout返回的就是剩余的多长时间。剩余的时间为4,说明是运行了1秒钟,在第二秒的时候有事件到来了,timeout还剩4秒,然后timeout就被设置了,所以这也证明了timeout是一个输入输出型参数,也就证明了我们为什么要对timeout进行重新设定。
为什么我们telnet链接后,select这里变成了死循环打印有事件发生呢?如果一直死循环对应到代码中是不是就是:
select的返回值一直是大于0的。当然在后面的代码中,这里select的最后一个参数我会设置为nullptr,也就是阻塞的等,要不然他会刷屏。
在我们写的这个代码中,我们是没有处理这个新的链接的,比如:accept。
我们可以看到telnet和这个8081的链接是建立好的,只不过是有没有把这个链接处理,那么就相当于这个select有读事件是一直就绪的,这也就验证了我们前面说的select的策略,只要有链接没有被accept上来就一直通知。
所以这也就有了select一直通知的有事情发生的死循环。
那么此时我们就知道可以正常进行处理事件了,而且所有的事件都在了我们定义好的rfds里了。
select_server.hpp
1 #pragma once
2
3 #include "sock.hpp"
4 #include <sys/select.h>
5
6 #define BACK_LOG 5
7 #define NUM 1024
8 #define DEFAULT_FD -1
9
10 namespace ns_select{
11
12 class SelectServer{
13 private:
14 int listen_sock;
15 unsigned short port;
16 public:
17 SelectServer(unsigned short _port)
18 :port(_port)
19 {}
20
21 void InitSelectServer()
22 {
23 listen_sock = ns_sock::Sock::Socket();
24 ns_sock::Sock::Bind(listen_sock, port);
25 ns_sock::Sock::Listen(listen_sock, BACK_LOG);
26 }
27
28 void Run()
29 {
30 fd_set rfds;
31 int fd_array[NUM] = {0};
32 ClearArray(fd_array, NUM, DEFAULT_FD);// 用来初始化数据中所有的fd
33
34 fd_array[0] = listen_sock;// 监听套接字sock写入数组的第一个元素
35
36 for(;;)
37 {
38 // 时间也是输入输出,所以如果你是timeout返回,那么就需要对时间也要重新设置
39 struct timeval timeout = {5, 0};// 每个5秒timeout一次
40 // 对所有的合法fd进行在select中重新设定
41 int max_fd = DEFAULT_FD;
42 FD_ZERO(&rfds);//清空所有的read文件描述符集
43 // 第一次循环的时候我们的fd_array数组中至少已经有了一个fd->listen_sock
44 for(auto i = 0; i < NUM; i++)
45 {
46 if(fd_array[i] == DEFAULT_FD){
47 continue;
48 }
49 //到这里说明是需要添加的合法fd
50 FD_SET(fd_array[i], &rfds);
51 if(max_fd < fd_array[i]){
52 max_fd = fd_array[i];
53 }
54 }
55
56 switch(select(max_fd + 1, &rfds, nullptr, nullptr, nullptr))
57 {
58 case 0:
59 std::cout << "time out" << std::endl;
60 break;
61 case -1:
62 std::cerr << "select error" << std::endl;
63 break;
64 default:
65 // 正常的事件处理
66 //std::cout << "有事件发生.... timeout: " << timeout.tv_sec << std::endl;
67 HandlerEvent(rfds, fd_array, NUM);
68 break;
69 }
70 }
71 }
72
73 void HandlerEvent(const fd_set& rfds, int fd_array[], int num)
74 {
75 //如何判定哪些文件描述符就绪了呢??只需要判定特定的fd是否在rfds集合中
76 //我们都有哪些文件描述符呢??fd_array[]
77 for(auto i = 0; i < num; i++)
78 {
79 if(fd_array[i] == DEFAULT_FD){
80 continue;
81 }
82
83 //到这里说明是一个合法的fd,但是不一定就绪了。
84 if(fd_array[i] == listen_sock && FD_ISSET(fd_array[i], &rfds)){
85 //是一个合法的fd,并且已经就绪了,是连接事件到来
86 //accept
87 struct sockaddr_in peer;
88 socklen_t len = sizeof(peer);
89 //在这里的时候accept会不会阻塞呢??不会!因为链接已经建立好了
90 int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
91 if(sock < 0){
92 std::cerr << "accept error" << std::endl;
93 continue;
94 }
95 uint16_t peer_port = htons(peer.sin_port);
96 std::string peer_ip = inet_ntoa(peer.sin_addr);
97
98 std::cout << "get a new link: " << peer_ip << ":" << peer_port << std::endl;
95 //是不是可以进行对应的recv??不可以!recv是IO->等+拷贝
96 //recv是不知道有没有数据,但是select知道
97 //所以这里做的肯定不是读,而是将该文件描述符添加到fd_array数组中!为什么??
98 //因为添加后重新进行fd合法判断后,下次在这个要recv的fd就会进入下面"处理正常的fd"判断中
99 //如果读事件就绪了直接就读
100 if(!AddFdToArray(fd_array, num, sock)){
101 close(sock);
102 std::cout << "select server is full, close fd: " << sock << std::endl;
103 }
104 }
105 else{
106 //处理正常的fd
107 if(FD_ISSET(fd_array[i], &rfds)){
108 //是一个合法的fd,并且已经就绪了,是读事件就绪
109 //实现读写会阻塞么??绝对不会!
110 char buffer[1024];
111 ssize_t s = recv(fd_array[i], buffer, sizeof(buffer) - 1, 0);//有BUG!!!
112 if(s > 0){
113 buffer[s] = 0;
114 std::cout << "echo# " << buffer << std::endl;
115 }
116 else if (s == 0){
117 std::cout << "client quit" << std::endl;
118 close(fd_array[i]);
119 fd_array[i] = DEFAULT_FD;//清除数组中的文件描述符
120 }
121 else{
122 std::cerr << "recv error" << std::endl;
123 close(fd_array[i]);
124 }
125 }
126 else{
127 //TODO
128 }
129 }
130 }
131 }
132
133 ~SelectServer(){}
134
135 private:
136 void ClearArray(int fd_array[], int num, int default_fd)// 用来初始化数据中的fd
137 {
138 for(auto i = 0; i < num; i++)
139 {
140 fd_array[i] = default_fd;
141 }
142 }
143 bool AddFdToArray(int fd_array[], int num, int sock)
144 {
145 for(auto i = 0; i < num; i++)
146 {
147 if(fd_array[i] == DEFAULT_FD){
148 fd_array[i] = sock;
149 return true;
150 }
151 }
152 //说明我们的数组内空间被使用完了!
153 return false;
154 }
155 };
156
157
158
159 }
至此我们的select的基本代码就写完了,然后我们测试一下,测试完后里面还有细节需要修改一下(recv...)。
我们可以看到我们select就完成了!!!
现在来解决下recv的问题,首先你能确定你读完了数据么?如果我一条链接给你发了多个请求数据,但是每个数据都只有10字节,那么会不会出现粘包问题?如果发过来的数据太多,没有读到一个完整的报文,数据可能丢失(每次循环buffer都清空了)。
这里我们怎么保证自己能拿到完整的数据呢?根本原因在于我们需要:1.定制协议。2.还要给每一个sock定义对应的缓冲区。
select_server.hpp(更改buffer的)
1 #pragma once
2
3 #include "sock.hpp"
4 #include <sys/select.h>
5 #include <unordered_map>
6
7 #define BACK_LOG 5
8 #define NUM 1024
9 #define DEFAULT_FD -1
10
11 namespace ns_select{
12
13
14 struct bucket{
15 public:
16 std::string in_buferr;
17 std::string out_buffer;
18 int in_curr;//已经接收到了多少数据
19 int out_curr;//已经发送了多少数据
20 public:
21 bucket()
22 :in_curr(0)
23 ,out_curr(0)
24 {}
25
26 };
27
28
29 class SelectServer{
30 private:
31 int listen_sock;
32 unsigned short port;
33 std::unordered_map<int, bucket> buckets;
34 public:
35 SelectServer(unsigned short _port)
36 :port(_port)
37 {}
38
39 void InitSelectServer()
40 {
41 listen_sock = ns_sock::Sock::Socket();
42 ns_sock::Sock::Bind(listen_sock, port);
43 ns_sock::Sock::Listen(listen_sock, BACK_LOG);
44 }
45
46 void Run()
47 {
48 fd_set rfds;
49 int fd_array[NUM] = {0};
50 ClearArray(fd_array, NUM, DEFAULT_FD);// 用来初始化数据中所有的fd
51
52 fd_array[0] = listen_sock;// 监听套接字sock写入数组的第一个元素
53
54 for(;;)
55 {
56 // 时间也是输入输出,所以如果你是timeout返回,那么就需要对时间也要重新设置
W> 57 struct timeval timeout = {5, 0};// 每个5秒timeout一次
58 // 对所有的合法fd进行在select中重新设定
59 int max_fd = DEFAULT_FD;
60 FD_ZERO(&rfds);//清空所有的read文件描述符集
61 // 第一次循环的时候我们的fd_array数组中至少已经有了一个fd->listen_sock
62 for(auto i = 0; i < NUM; i++)
63 {
64 if(fd_array[i] == DEFAULT_FD){
65 continue;
66 }
67 //到这里说明是需要添加的合法fd
68 FD_SET(fd_array[i], &rfds);
69 if(max_fd < fd_array[i]){
70 max_fd = fd_array[i];
71 }
72 }
73
74 switch(select(max_fd + 1, &rfds, nullptr, nullptr, /*&timeout*/nullptr))
75 {
76 case 0:
77 std::cout << "time out" << std::endl;
78 break;
79 case -1:
80 std::cerr << "select error" << std::endl;
81 break;
82 default:
83 // 正常的事件处理
84 //std::cout << "有事件发生.... timeout: " << timeout.tv_sec << std::endl;
85 HandlerEvent(rfds, fd_array, NUM);
86 break;
87 }
88 }
89 }
90
91 void HandlerEvent(const fd_set& rfds, int fd_array[], int num)
92 {
93 //如何判定哪些文件描述符就绪了呢??只需要判定特定的fd是否在rfds集合中
94 //我们都有哪些文件描述符呢??fd_array[]
95 for(auto i = 0; i < num; i++)
96 {
97 if(fd_array[i] == DEFAULT_FD){
98 continue;
99 }
100
101 //到这里说明是一个合法的fd,但是不一定就绪了。
102 if(fd_array[i] == listen_sock && FD_ISSET(fd_array[i], &rfds)){
103 //是一个合法的fd,并且已经就绪了,是连接事件到来
104 //accept
105 struct sockaddr_in peer;
106 socklen_t len = sizeof(peer);
107 //在这里的时候accept会不会阻塞呢??不会!因为链接已经建立好了
108 int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
109 if(sock < 0){
110 std::cerr << "accept error" << std::endl;
111 continue;
112 }
113 uint16_t peer_port = htons(peer.sin_port);
114 std::string peer_ip = inet_ntoa(peer.sin_addr);
115
116 struct bucket b;
117 buckets.insert({sock, b});
118 std::cout << "get a new link: " << peer_ip << ":" << peer_port << std::endl;
119 //是不是可以进行对应的recv??不可以!recv是IO->等+拷贝
120 //recv是不知道有没有数据,但是select知道
121 //所以这里做的肯定不是读,而是将该文件描述符添加到fd_array数组中!为什么??
122 //因为添加后重新进行fd合法判断后,下次在这个要recv的fd就会进入下面"处理正常的fd"判断中
123 //如果读事件就绪了直接就读
124 if(!AddFdToArray(fd_array, num, sock)){
125 close(sock);
126 std::cout << "select server is full, close fd: " << sock << std::endl;
127 }
128 }
129 else{
130 //处理正常的fd
131 if(FD_ISSET(fd_array[i], &rfds)){
132 //是一个合法的fd,并且已经就绪了,是读事件就绪
133 //实现读写会阻塞么??绝对不会!
134 char buffer[1024];
135 ssize_t s = recv(fd_array[i], buffer, sizeof(buffer) - 1, 0);//有BUG!!!
136 if(s > 0){
137 buffer[s] = 0;
138 buckets[fd_array[i]].in_buferr = buffer;
139 std::cout << "echo# " << buffer << std::endl;
140 }
141 else if (s == 0){
142 std::cout << "client quit" << std::endl;
143 close(fd_array[i]);
144 fd_array[i] = DEFAULT_FD;//清除数组中的文件描述符
145 }
146 else{
147 std::cerr << "recv error" << std::endl;
148 close(fd_array[i]);
149 }
150 }
151 else{
152 //TODO
153 }
154 }
155 }
156 }
157
158 ~SelectServer(){}
159
160 private:
161 void ClearArray(int fd_array[], int num, int default_fd)// 用来初始化数据中的fd
162 {
163 for(auto i = 0; i < num; i++)
164 {
165 fd_array[i] = default_fd;
166 }
167 }
168 bool AddFdToArray(int fd_array[], int num, int sock)
169 {
170 for(auto i = 0; i < num; i++)
171 {
172 if(fd_array[i] == DEFAULT_FD){
173 fd_array[i] = sock;
174 return true;
175 }
176 }
177 //说明我们的数组内空间被使用完了!
178 return false;
179 }
180 };
181
182
183
184 }
我们可以看到,在这里的服务器编写的时候,我们这里用的是数组(fd_array[1024]),这里可以存放的文件描述符是一个确定大小的,如果是确定大小的话,多了我们就处理不了,那么我们是不是可以用vector、set呢?当然是可以。
因为是定长的,所以我们可以处理的服务器连接是有上线的,那么这里没有问题么?没有!!下面我来解释一下。
那么也就表示你(select)肯定是有上线的。
我们可以看到,我们的服务器上最多也是只能表示1024个,所以select最多只能表示1024个文件描述符,那么我们将数组定义成特定大小是没有问题的!!因为它只能保存这么多!当然我们可以直接写成sizeof(fd_set)*8。
然后:
我们可以看到我们现在可以看到:进程可以打开的文件描述符最多是32,但是实际上操作系统还是可以拓展的!
这时select可能会说:你操作系统进程对多可以打开的文件只有32个,虽然可以拓展,我有1024个又有什么关系呢?但是不能这么说,操作系统能打开多少个是操作系统的问题,你的参数只能传1024个是你的问题,你先保证你可以等待的文件描述符没有上线的话,那么就可以说是操作系统的问题。
这时select的特征->缺点!!
我们可以看到,我们的云服务器默认打开的已经很多了,100001,而不是32个。所以是可以调的。
select需要和OS交互数据,涉及到较多是数据来回拷贝。当select面临的链接很多,就绪的也较多时,而导致效率降低。
select每次调用都必须重新添加fd,一定会有影响程序运行效率,而且非常麻烦,容易出错。
select(nfds ...),maxfd+1:操作系统在检测fd就绪的时候需要遍历的,所以当有大量的链接的时候,内核同步select->底层遍历,成本会变得越来越高。
上面是select的4个缺点。当然select还是有优点的!
select可以同时等多个fd,而且只负责等,有具体的recv、send...来完成实际的IO操作,并且不会被阻塞住。
因为可以同时等多个fd,所以任何一个fd就绪的概率就增加了,我们的服务区可能在单位时间内等的比重就在降低,可以调高效率!
但是其实这个可以同时等多个fd,是select、poll、epool都具有的优点,
适用场景 :
如果有一定的链接,每个链接都很活跃,是不是必须使用多路转接呢?不是!!因为链接数不是很多,而且都很活跃,我们完全可以用多线程,一个线程用来接收,一个线程用来完成服务。
这个更适合有大量的链接,但是只有少量是活跃的!
6. I/O多路转接之poll
poll解决了select的两个缺点。
1. 解决了select能检测的文件描述符是有上线的这个问题。
2. 将用户告诉内核让OS帮我关心哪些文件描述符上面的哪些事件、将内核告诉用户哪些文件描述符的哪些是已经就绪。进行分离。
这样的好处是不用在每次调用poll的时候,重新添加fd以及fd关心的那点事件了。
6.1 poll函数接口
events和revents的取值:
其中我们主要关心红框里的两个。
现在我编写的代码先只关心可以接收到链接。
poll_server.hpp
1 #include "sock.hpp"
2 #include <poll.h>
3
4 namespace ns_poll{
5
6 class PollServer{
7 private:
8 int listen_sock;
9 int port;
10
11 public:
12 PollServer(int _port):port(_port)
13 {}
14 void InitServer()
15 {
16 listen_sock = ns_sock::Sock::Socket();
17 ns_sock::Sock::Bind(listen_sock, port);
18 ns_sock::Sock::Listen(listen_sock, 5);
19 }
20 void Run()
21 {
22 struct pollfd rfds[64];
23 for(int i = 0; i < 64; i++){
24 rfds[i].fd = -1;
25 rfds[i].events = 0;
26 rfds[i].revents = 0;
27 }
28 rfds[0].fd = listen_sock;
29 rfds[0].events |= POLLIN;
30 rfds[0].revents = 0; //kernel
31
32 for(;;){
33 switch(poll(rfds, 64, -1)){
34 case 0:
35 std::cout << "timeout" << std::endl;
36 break;
37 case -1:
38 std::cerr << "poll error" << std::endl;
39 break;
40 default:
41 for(int i =0; i < 64; i++){
42 if(rfds[i].fd == -1){
43 continue;
44 }
45
46 if(rfds[i].revents & POLLIN ){
47 if(rfds[i].fd == listen_sock){
48 //accept;
49 std::cout << "get a new link ..." << std::endl;
50 }
51 else{
52 //recv
53 }
54 }
55 }
56 break;
57 }
58 }
59 }
60 ~PollServer()
61 {}
62 };
63 }
server.cc
1 #include "poll_server.hpp"
2 #include <iostream>
3 #include <string>
4 #include <cstdlib>
5
6
7 static void Usage(std::string proc)
8 {
9 std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
10 }
11
12
13 int main(int argc, char* argv[])
14 {
15 if(argc != 2){
16 Usage(argv[0]);
17 exit(4);
18 }
19
20 unsigned short port = atoi(argv[1]);
21
22 ns_poll::PollServer *ps = new ns_poll::PollServer(port);
23 ps->InitServer();
24 ps->Run();
25
26
27 return 0;
28 }
sock.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <unistd.h>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <netinet/in.h>
10
11
12 namespace ns_sock{
13
14 class Sock{
15 public:
16 static int Socket()
17 {
18 int sock = socket(AF_INET, SOCK_STREAM, 0);
19 if(sock < 0){
20 std::cerr << "socket error" << std::endl;
21 exit(1);
22 }
23 int opt = 1;
24 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
25 return sock;
26 }
27
28 static bool Bind(int sock, unsigned short port)
29 {
30 struct sockaddr_in local;
31 memset(&local, 0, sizeof(local));
32 local.sin_family = AF_INET;
33 local.sin_port = htons(port);
34 local.sin_addr.s_addr = INADDR_ANY;
35
36 if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
37 std::cerr << "bind error" << std::endl;
38 exit(2);
39 }
40 return true;
41 }
42
43 static bool Listen(int sock, int backlog)
44 {
45 if(listen(sock, backlog) < 0){
46 std::cerr << "listen error" << std::endl;
47 exit(3);
48 }
49 return true;
50 }
51 };
52
53
54
55 }
我们现在就已经写好了。
我们可以看到,我们可以获取链接了,接下来就可以编写recv、accept...,我们可以看到,相对而言,poll还是比较简单的。
这也就印证了前面说的解决了select的两个缺点。
其中一个是应用events和revents实现了输出输入分离,就不需要重新添加fd以及fd关心的那点事件了。
然后还解决了上限的问题,但是你们可能会说,这不还是设置成了一个数组么?这不还是有上线的么?那么这解决了么?其实poll是解决了,我们可以看到上面的参数是:struct pollfd *fds 。可以看到这是一个指针,而不是像select那样fd_set这种特定的类型,所以是解决了上线的问题,这里的数组你想写多少写多少,写的大小取决于你的内存。
但是其它的两个缺点还是没有解决的。
7. I/O多路转接之epoll
7.1 epoll初识
按照man手册的说法: 是为处理大批量句柄而作了改进的poll。
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)。
它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
7.2 epoll的相关系统调用
epoll 有3个相关的系统调用。
epoll_create
int epoll_create(int size);
创建一个epoll的句柄
- 自从linux2.6.8之后,size参数是被忽略的。
- 用完之后,必须调用close()关闭。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epoll的事件注册函数。
- 它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
- 第一个参数是epoll_create()的返回值(epoll的句柄)。
- 第二个参数表示动作,用三个宏来表示。
- 第三个参数是需要监听的fd。
- 第四个参数是告诉内核需要监听什么事。
第二个参数的取值:
- EPOLL_CTL_ADD :注册新的fd到epfd中;
- EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL :从epfd中删除一个fd;
struct epoll_event结构如下
events可以是以下几个宏的集合:
- EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭)。
- EPOLLOUT : 表示对应的文件描述符可以写。
- EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来)。
- EPOLLERR : 表示对应的文件描述符发生错误。
- EPOLLHUP : 表示对应的文件描述符被挂断。
- EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件。
- 参数events是分配好的epoll_event结构体数组。
- epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存)。
- maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size。
- 参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞)。
- 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时,返回小于0表示函数失败。
前面说的select和poll都借助了第三方数组,这数组是用户定义的,充当容器,增删改都需要自己维护。
7.3 epoll工作原理
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
- 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。
- 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是,其中n为树的高度)。
- 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法。
- 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
- 在epoll中,对于每一个事件,都会建立一个epitem结构体。
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
- 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。
- 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。这个操作的时间复杂度是O(1)。
总结一下,epoll的使用过程就是三部曲:
- 调用epoll_create创建一个epoll句柄。
- 调用epoll_ctl,将要监控的文件描述符进行注册。
- 调用epoll_wait,等待文件描述符就绪。
注意!!
网上有些博客说,epoll中使用了内存映射机制。
- 内存映射机制:内核直接将就绪队列通过mmap的方式映射到用户态。避免了拷贝内存这样的额外性能开销。
这种说法是不对的,因为操作系统并没有使用这样的机制。我们定义的struct epoll_event是我们在用户空间中分配好的内存。势必还是需要将内核的数据拷贝到这个用户空间的内存中的。而且还因为操作系统是不相信用户的,所以不可能暴露给用户。
sock.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <unistd.h>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <netinet/in.h>
10
11
12 namespace ns_sock{
13
14 class Sock{
15 public:
16 static int Socket()
17 {
18 int sock = socket(AF_INET, SOCK_STREAM, 0);
19 if(sock < 0){
20 std::cerr << "socket error" << std::endl;
21 exit(1);
22 }
23 int opt = 1;
24 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
25 return sock;
26 }
27
28 static bool Bind(int sock, unsigned short port)
29 {
30 struct sockaddr_in local;
31 memset(&local, 0, sizeof(local));
32 local.sin_family = AF_INET;
33 local.sin_port = htons(port);
34 local.sin_addr.s_addr = INADDR_ANY;
35
36 if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
37 std::cerr << "bind error" << std::endl;
38 exit(2);
39 }
40 return true;
41 }
42
43 static bool Listen(int sock, int backlog)
44 {
45 if(listen(sock, backlog) < 0){
46 std::cerr << "listen error" << std::endl;
47 exit(3);
48 }
49 return true;
50 }
51 };
52
53
54
55 }
server.cc
1 #include "epoll_server.hpp"
2 #include <iostream>
3 #include <string>
4 #include <cstdlib>
5
6
7 static void Usage(std::string proc)
8 {
9 std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
10 }
11
12 int main(int argc, char* argv[])
13 {
14 if(argc != 2){
15 Usage(argv[0]);
16 exit(5);
17 }
18
19 unsigned short port = atoi(argv[1]);
20
21 ns_epoll::EpollServer *ep_svr = new ns_epoll::EpollServer(port);
22 ep_svr->InitEpollServer();
23 ep_svr->Run();
24
25 return 0;
26 }
epoll_server.hpp
1 #pragma once
2
3 #include "sock.hpp"
4 #include <sys/epoll.h>
5
6 namespace ns_epoll{
7
8 #define MAX_NUM 64
9
10 const int back_log = 5;
11 const int size = 256;
12
13
14 class EpollServer{
15 private:
16 int listen_sock;
17 int epfd;
18 uint16_t port;
19 public:
20 EpollServer(uint16_t _port)
21 :port(_port)
22 {}
23
24 void InitEpollServer()
25 {
26 listen_sock = ns_sock::Sock::Socket();
27 ns_sock::Sock::Bind(listen_sock, port);
28 ns_sock::Sock::Listen(listen_sock, back_log);
29
30 std::cout << "debug, listen_sock: " << listen_sock << std::endl;
31
32 if((epfd = epoll_create(size)) < 0){
33 std::cerr << "epoll_create error" << std::endl;
34 exit(4);
35 }
36
37
38 std::cout << "debug, epfd: " << epfd << std::endl;
39 }
40
41 void AddEvent(int sock, uint32_t event)
42 {
43 struct epoll_event ev;
44 ev.events = event;
45 if(epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev) < 0){
46 std::cerr << "epoll_ctl error, fd: " << sock << std::endl;
47 }
48 }
49 void Run()
50 {
51 //在这里我们目前只有一个socket是能够关心读写的->listen_sock->read event
52 AddEvent(listen_sock, EPOLLIN);
53 int timeout = -1;
54 struct epoll_event revs[MAX_NUM];
55 for(;;)
56 {
57 //返回值num表明有多少个事件就绪了,内核会将就绪事件依次放入revs中
58 int num = epoll_wait(epfd, revs, MAX_NUM, timeout);
59 if(num > 0){
60 std::cout << "有事件发生了..." << std::endl;
61 }
62 else if (num == 0){
63 std::cout << "timeout..." << std::endl;//just print
64 }
65 else{
66 std::cerr << "epoll_wait error" << std::endl;
67 }
68 }
69 }
70
71
72
73 ~EpollServer()
74 {
75 if(listen_sock >= 0){
76 close(listen_sock);
77 }
78 if(epfd >= 0){
79 close(epfd);
80 }
81 }
82
83
84 };
85
86 }
我们可以看到不同的timeout有不同的等待方式。
而且我们现在一个事件都没有,目前我们只添加了一个listen事件,但是我们现在没有处理它。
当我们用浏览器链接的时候(用telnet也行)会发现一直打印有事件发生了。这是因为:默认情况下,epoll也是有事件就绪但是没有处理的话,epoll会一直通知你,直到你处理掉!
那么你们知道如果事件没有处理它就一直通知么?很简单,所谓的事件处理不是将这个事件读上去就完了,很明显,我们是读了的,我们调用了epoll_wait,我们将事件已经获取上来了,我们没有处理它,所以所谓的事件处理不是就读上来,而是底层引起事件就绪的时候,底层一直有空间,可以理解成这个数据一直在就绪队列中,我们只是拷贝上来的,但是数据没有被拿走。
我们刚刚只是把epoll的接口都跑了一次,但是没有做后续的事件处理工作。
1 #pragma once
2
3 #include "sock.hpp"
4 #include <sys/epoll.h>
5
6 namespace ns_epoll{
7
8 #define MAX_NUM 64
9
10 const int back_log = 5;
11 const int size = 256;
12
13
14 class EpollServer{
15 private:
16 int listen_sock;
17 int epfd;
18 uint16_t port;
19 public:
20 EpollServer(uint16_t _port)
21 :port(_port)
22 {}
23
24 void InitEpollServer()
25 {
26 listen_sock = ns_sock::Sock::Socket();
27 ns_sock::Sock::Bind(listen_sock, port);
28 ns_sock::Sock::Listen(listen_sock, back_log);
29
30 std::cout << "debug, listen_sock: " << listen_sock << std::endl;
31
32 if((epfd = epoll_create(size)) < 0){
33 std::cerr << "epoll_create error" << std::endl;
34 exit(4);
35 }
36
37
38 std::cout << "debug, epfd: " << epfd << std::endl;
39 }
40
41 void AddEvent(int sock, uint32_t event)
42 {
43 struct epoll_event ev;
44 ev.events = 0;
45 ev.events |= event;
46 ev.data.fd = sock;//我们在处理的时候只知道有事件就绪了,但是不知道哪个文件描述符就绪了
47 if(epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev) < 0){
48 std::cerr << "epoll_ctl error, fd: " << sock << std::endl;
49 }
50 }
51 void DelEvent(int sock)
52 {
53 if(epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr) < 0){
54 std::cerr << "epoll_ctl_del error, fd: " << sock << std::endl;
55 }
56 }
57 void Run()
58 {
59 //在这里我们目前只有一个socket是能够关心读写的->listen_sock->read event
60 AddEvent(listen_sock, EPOLLIN);
61 int timeout = -1;
62 struct epoll_event revs[MAX_NUM];
63 for(;;)
64 {
65 //返回值num表明有多少个事件就绪了,内核会将就绪事件依次放入revs中
66 int num = epoll_wait(epfd, revs, MAX_NUM, timeout);
67 if(num > 0){
68 //std::cout << "有事件发生了..." << std::endl;
69 for(int i = 0; i < num; i++)
70 {
71 int sock = revs[i].data.fd;//哪个文件描述符就绪了呢??
72 if(revs[i].events & EPOLLIN){//读事件就绪
73 if(sock == listen_sock){
74 //1.listen_sock,链接时间就绪
75 struct sockaddr_in peer;
76 socklen_t len = sizeof(peer);
77 int sk = accept(listen_sock, (struct sockaddr*)&peer, &len);
78 if(sk < 0){
79 std::cout << "accept error" << std::endl;
80 continue;
81 }
82 std::cout << "get a new line: " << inet_ntoa(peer.sin_addr) << ":" << ntohs(peer.sin_port) << std::endl;
83 //不要读取和写入一起设置,先进行读取,只要需要写入的时候才主动设置EPOLLOUT
84 AddEvent(sk, EPOLLIN);
85 }
86 else{
87 //sock,可读事件就绪
88 char buffer[1024];
89 ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);//有bug,和我们前面改的那个select一样
90 if(s > 0){
91 buffer[s] = 0;
92 std::cout << buffer << std::endl;
93 }
94 else{
95 std::cout << "client close" << std::endl;
96 close(sock);
97 DelEvent(sock);
98 }
99 }
100 }
101 else if(revs[i].events & EPOLLOUT){
102
103 }
104 else{
105
106 }
107 }
108 }
109 else if (num == 0){
110 std::cout << "timeout..." << std::endl;//just print
111 }
112 else{
113 std::cerr << "epoll_wait error" << std::endl;
114 }
115 }
116 }
117
118
119
120 ~EpollServer()
121 {
122 if(listen_sock >= 0){
123 close(listen_sock);
124 }
125 if(epfd >= 0){
126 close(epfd);
127 }
128 }
129
130
131 };
132
133 }
这次我们就把第一个版本的只读的epoll服务器写完了,接下来我们测试一下。
我们可以看到我们设计是服务器可以同时对多个客户端进行读取数据~
7.4 epoll的优点(和 select 的缺点对应)
- 接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效。不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开。
- 数据拷贝轻量(设置只设置一次,拷贝只拷贝就绪的):只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)。
- 事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪。这个操作时间复杂度O(1)。即使文件描述符数目很多,效率也不会受到影响。
- 没有数量限制:文件描述符数目无上限。
7.5 epoll工作方式
epoll有2种工作方式-水平触发(LT)和边缘触发(ET)。(epoll就绪事件的通知机制)
LT:只要底层有数据,没有被取走,就会一直通知上层,需要上层下来取数据。
ET:只要底层的数据从无到有,从有到多(变化)的时候,会通知上层一次,需要上层马上下来进行数据读取!
epoll默认的工作方式是LT(水平触发)。
因为LT如果底层有数据就会一直通知你,所以不用担心数据丢失的问题。
接下来我们来谈谈ET!
因为ET是只要底层的数据从无到有,从有到多(变化)的时候,会通知上层一次,需要上层马上下来进行数据读取!所以它是倒逼应用层必须尽快下来将所有的数据取走!
也就是一旦事件就绪(以读为例),程序员就应该尽可能的一次把所有的数据全部读取完毕!
但是你如何得知本轮数据已经全部被读取完毕了呢?我们只是单纯的调用recv么?当然不行,因为无论你recv的缓冲区设置位多大,数据的多少永远可能比缓冲区的大小要大(除非缓冲区特别特别大,但是这种方式太粗暴了),所以需要我们循环调用recv,直到通过recv的返回值判断是否读取完毕。
但是还是有特殊情况!
前面都是常规的recv,但是在最后一次正好没数据了,recv会被阻塞住!这种问题是很严重的,因为进程就相当于被挂起了,服务器无法再响应任何外部事件。
- 期望读取n字节,返回小于n字节就说明读完了。
- 次次满足,最后一次为0的情况。
所以ET工作模式下,recv或者read必须是处于非阻塞模式下进行读取(没数据了就有出错的形式返回了)。
假如有这样一个例子:
- 我们已经把一个tcp socket添加到epoll描述符。
- 这个时候socket的另一端被写入了2KB的数据。
- 调用epoll_wait,并且它会返回。说明它已经准备好读取操作。
- 然后调用read,只读取了1KB的数据。
- 继续调用epoll_wait......
水平触发Level Triggered 工作模式
- 当epoll检测到socket上事件就绪的时候,可以不立刻进行处理。或者只处理一部分。
- 如上面的例子,由于只读了1K数据,缓冲区中还剩1K数据,在第二次调用 epoll_wait 时,epoll_wait 仍然会立刻返回并通知socket读事件就绪。
- 直到缓冲区上所有的数据都被处理完,epoll_wait 才不会立刻返回。
- 支持阻塞读写和非阻塞读写。
边缘触发Edge Triggered工作模式
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志,epoll进入ET工作模式。
- 当epoll检测到socket上事件就绪时,必须立刻处理。
- 如上面的例子,虽然只读了1K的数据,缓冲区还剩1K的数据,在第二次调用 epoll_wait 的时候,epoll_wait 不会再返回了。
- 也就是说,ET模式下,文件描述符上的事件就绪后,只有一次处理机会。
- ET的性能比LT性能更高( epoll_wait 返回的次数少了很多)。Nginx默认采用ET模式使用epoll。
- 只支持非阻塞的读写。
select和poll其实也是工作在LT模式下。epoll既可以支持LT,也可以支持ET。
7.6 对比LT和ET
LT是 epoll 的默认行为。使用 ET 能够减少 epoll 触发的次数。但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完。
相当于一个文件描述符就绪之后,不会反复被提示就绪,看起来就比 LT 更高效一些。但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理,不让这个就绪被重复提示的话,其实性能也是一样的。
另一方面,ET 的代码复杂程度更高了。
7.7 理解ET模式和非阻塞文件描述符
使用 ET 模式的 epoll,需要将文件描述设置为非阻塞。这个不是接口上的要求,而是 "工程实践" 上的要求。
假设这样的场景:服务器接受到一个10k的请求,会向客户端返回一个应答数据。如果客户端收不到应答,不会发送第二个10k请求。
如果服务端写的代码是阻塞式的read,并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来,参考 man 手册的说明,可能被信号打断),剩下的9k数据就会待在缓冲区中。
此时由于 epoll 是ET模式,并不会认为文件描述符读就绪。epoll_wait 就不会再次返回。剩下的 9k 数据会一直在缓冲区中。直到下一次客户端再给服务器写数据。epoll_wait 才能返回但是问题来了:
- 服务器只读到1k个数据,要10k读完才会给客户端返回响应数据。
- 客户端要读到服务器的响应,才会发送下一个请求。
- 客户端发送了下一个请求,epoll_wait 才会返回,才能去读缓冲区中剩余的数据。
所以,为了解决上述问题(阻塞read不一定能一下把完整的数据读完),于是就可以使用非阻塞轮训的方式来读缓冲区,保证一定能把完整的请求都读出来。
而如果是LT没这个问题。只要缓冲区中的数据没读完,就能够让 epoll_wait 返回文件描述符读就绪。
接下来改一下上面的epoll(升级版!)
下面先测试可不可以读取上来数据,并将报文和报文之间进行分离。
sock.hpp (plus)
1 #pragma once
2
3 #include <iostream>
4 #include <unistd.h>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <netinet/in.h>
10
11
12 namespace ns_sock{
13
14 class Sock{
15 public:
16 static int Socket()
17 {
18 int sock = socket(AF_INET, SOCK_STREAM, 0);
19 if(sock < 0){
20 std::cerr << "socket error" << std::endl;
21 exit(1);
22 }
23 int opt = 1;
24 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
25 return sock;
26 }
27
28 static bool Bind(int sock, unsigned short port)
29 {
30 struct sockaddr_in local;
31 memset(&local, 0, sizeof(local));
32 local.sin_family = AF_INET;
33 local.sin_port = htons(port);
34 local.sin_addr.s_addr = INADDR_ANY;
35
36 if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
37 std::cerr << "bind error" << std::endl;
38 exit(2);
39 }
40 return true;
41 }
42
43 static bool Listen(int sock, int backlog)
44 {
45 if(listen(sock, backlog) < 0){
46 std::cerr << "listen error" << std::endl;
47 exit(3);
48 }
49 return true;
50 }
51 };
52
53
54
55 }
server.cc (plus)
1 #include "sock.hpp"
2 #include "util.hpp"
3 #include "app_interface.hpp"
4 #include "epoller.hpp"
5 #include <iostream>
6 #include <string>
7 #include <cstdlib>
8
9
10 const int back_log = 5;
11 static void Usage(std::string proc)
12 {
13 std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
14 }
15
16 int main(int argc, char* argv[])
17 {
18 if(argc != 2){
19 Usage(argv[0]);
20 exit(5);
21 }
22
23 uint16_t port = atoi(argv[1]);
24 //这个是与服务器listen_sock相关
25 int listen_sock = ns_sock::Sock::Socket();
26 ns_util::SetNonBlock(listen_sock);
27 ns_sock::Sock::Bind(listen_sock, port);
28 ns_sock::Sock::Listen(listen_sock, back_log);
29
30 std::cout << "listen_sock: " << listen_sock << std::endl;
31
32 //这个是我们的Epoll事件管理器
33 ns_epoll::Epoller epoller;
34 epoller.InitEpoller();
35
36 ns_epoll::EventItem item;
37 item.sock = listen_sock;
38 item.R = &epoller;
39
40 //listen只需要关心读事件就行
41 //accepter函数TODU
42 item.ManagerCallBack(ns_appInterface::accepter, nullptr, nullptr);
43
44 //将我们的listen_sock托管给epoller! ET模式->一次把所有的事件添加完
45 epoller.AddEvent(listen_sock, EPOLLIN|EPOLLET, item);
46
47
48 int timeout = 1000;
49 while(true){
50 epoller.Dispatcher(timeout);
51 }
52
53 return 0;
54 }
util.hpp (plus)
1 #pragma once
2
3 #include <iostream>
4 #include <unistd.h>
5 #include <fcntl.h>
6 #include <vector>
7
8 namespace ns_util{
9
10 void SetNonBlock(int sock)
11 {
12 int fl = fcntl(sock, F_GETFL);
13 if(fl < 0){
14 std::cerr << "fcntl error" << std::endl;
15 exit(6);
16 }
17 fcntl(sock, F_SETFL, fl | O_NONBLOCK);
18 }
19
20 class StringUtill{
21 public:
22 static void Split(std::string& in, std::vector<std::string>* out, std::string sep)
23 {
24 //aaXbbX ->X:代表的是一个完整报文结束
25 //aaXbbb
26 while(true){
27 size_t pos = in.find(sep);
28 if(pos == std::string::npos){
29 break;
30 }
31
32 std::string s = in.substr(0, pos);
33 out->push_back(s);
34 in.erase(0, pos+sep.size());
35 }
36 }
37 };
38
39 }
epoller.hpp (plus)
1 #pragma once
2
3 #include "sock.hpp"
4 #include <sys/epoll.h>
5 #include <string>
6 #include <unordered_map>
7
8
9 namespace ns_epoll{
10
11 #define MAX_NUM 64
12
13 class Epoller;
14 class EventItem;
15
16
17 typedef int(*callback_t)(EventItem *);//const &:输入,*:输出,&输入输出
18
19 const int size = 256;
20
21 //当成一个结构体使用
22 class EventItem{
23 public:
24 //与通信相关
25 int sock;
26 //回指Epoller
27 Epoller* R;
28
29 //有关数据处理的回调函数,用来进行逻辑解耦的!
30 //应用数据就绪等通信细节和数据的处理模块使用该方法进行解耦!
31 callback_t recv_hander;
32 callback_t send_hander;
33 callback_t error_hander;
34
35 std::string inbuffer;//读取到的数据缓冲区
36 std::string outbuffer;//待发送的数据缓冲区
37 public:
38 EventItem()
39 :sock(0)
40 ,R(nullptr)
41 ,recv_hander(nullptr)
42 ,send_hander(nullptr)
43 ,error_hander(nullptr)
44 {}
45 void ManagerCallBack(callback_t _recv, callback_t _send, callback_t _error)
46 {
47 recv_hander = _recv;
48 send_hander = _send;
49 error_hander = _error;
50 }
51
52 ~EventItem()
53 {
54 //TODU
55 }
56 };
57
58
59
60 class Epoller{
61 private:
62 int epfd;
63
64 std::unordered_map<int, EventItem> event_items;//sock:EventItem
65 public:
66 Epoller()
67 :epfd(-1)
68 {}
69
70 void InitEpoller()
71 {
72 if((epfd = epoll_create(size)) < 0){
73 std::cerr << "epoll_create error" << std::endl;
74 exit(4);
75 }
76 std::cout << "debug, epfd: " << epfd << std::endl;
77 }
78
79 void AddEvent(int sock, uint32_t event, const EventItem& item)
80 {
81 struct epoll_event ev;
82 ev.events = 0;
83 ev.events |= event;
84 ev.data.fd = sock;//我们在处理的时候只知道有事件就绪了,但是不知道哪个文件描述符就绪了
85 if(epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev) < 0){
86 std::cerr << "epoll_ctl error, fd: " << sock << std::endl;
87 }
88 event_items.insert({sock, item});
89
90 std::cout << "debug, 添加:" << sock << "到epoller中,成功" << std::endl;
91 }
92
93 void DelEvent(int sock)
94 {
95 if(epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr) < 0){
96 std::cerr << "epoll_ctl_del error, fd: " << sock << std::endl;
97 }
98 event_items.erase(sock);
99 }
100
101 //事件分派器
102 void Dispatcher(int timeout)
103 {
104 //如果底层特定的事件就绪,我们就把对应的事件分派给指定的回调函数进行统一处理
105 struct epoll_event revs[MAX_NUM];
106 //返回值num表明有多少个事件就绪了,内核会将就绪事件依次放入revs中
107 int num = epoll_wait(epfd, revs, MAX_NUM, timeout);
108 for(int i = 0; i < num; i++)
109 {
110 int sock = revs[i].data.fd;
111
112 if((revs[i].events & EPOLLERR) || (revs[i].events & EPOLLHUP))
113 if(event_items[sock].error_hander) event_items[sock].error_hander(&event_items[sock]);
114
115 if(revs[i].events & EPOLLIN)
116 if(event_items[sock].recv_hander) event_items[sock].recv_hander(&event_items[sock]);//如果读取回调被设置,调用读回调
117
118 if(revs[i].events & EPOLLOUT)
119 if(event_items[sock].send_hander) event_items[sock].send_hander(&event_items[sock]);
120 }
121 }
122
123
124
125 ~Epoller()
126 {
127 if(epfd >= 0){
128 close(epfd);
129 }
130 }
131
132
133 };
134
135 }
app_interface.hpp (plus)
1 #pragma once
2
3 #include "util.hpp"
4 #include "epoller.hpp"
5 #include <vector>
6
7 namespace ns_appInterface{
8
9 using namespace ns_epoll;
10 int recver(EventItem* item);
11 int sender(EventItem* item);
12 int errorer(EventItem* item);
13
14 //请问:什么时候会调用这里的accepter
15 int accepter(EventItem* item)
16 {
17 std::cout << "get a new link: " << item->sock << std::endl;
18 while(true)
19 {
20 struct sockaddr_in peer;
21 socklen_t len = sizeof(peer);
22 int sock = accept(item->sock, (struct sockaddr*)&peer, &len);
23 if(sock < 0){
24 if(errno == EAGAIN || errno == EWOULDBLOCK){
25 //说明没有读取出错,只是底层没有链接了!
26 return 0;
27 }
28 else if(errno == EINTR){
29 //读取过程中被信号打断了
30 continue;
31 }
32 else{
33 //真出错了!
34 return -1;
35 }
36 }
37 else{
38 ns_util::SetNonBlock(sock);
39 //读取成功了!
40 EventItem tmp;
41 tmp.sock = sock;
42 tmp.R = item->R;
43 tmp.ManagerCallBack(recver, sender, errorer);
44 Epoller* epoller = item->R;
45 //epoll经常一定会设置读事件就绪,而写事件我们按序打开!
46 epoller->AddEvent(sock, EPOLLIN|EPOLLET, tmp);
47
48 }
49 }
50
51
52
53 return 0;
54 }
55
56 //0:读取成功
57 //-1:读取失败
58 int recver_helper(int sock, std::string* out)
59 {
60 while(true)
61 {
62 char buffer[128];
63 ssize_t size = recv(sock, buffer, sizeof(buffer) - 1, 0);
64 if(size < 0){
65 if(errno == EAGAIN || errno == EWOULDBLOCK){
66 //循环读取完毕
67 return 0;
68 }
69 else if(errno == EINTR){
70 //被信号中断,继续尝试
71 continue;
72 }
73 else{
74 //真正读取出错了 TODU
75 return -1;
76 }
77 }
78 else{
79 buffer[size] = 0;//string
80 *out += buffer;//将我们读到的数据添加到inbuffer中
81 }
82 }
83 }
84
85 int recver(EventItem* item)
86 {
87 //1.需要整体读,非阻塞
88 if(recver_helper(item->sock, &(item->inbuffer)) < 0)//读取失败
89 {
90 //item->error_hander
91 return -1;
92 }
93 std::cout << "client# " << item->inbuffer << std::endl;
94 //2.根据发来的数据流,进行包和包之间的分离,防止粘包问题->这里是涉及到协议定制呢!
95 //当然可以设置成不可显的/3 /n,但是这里我设置成X,为了好看清楚!
96 std::vector<std::string> messages;
97 ns_util::StringUtill::Split(item->inbuffer, &messages, "X");
98 for(auto s : messages){
99 std::cout << "################################" << std::endl;
100 std::cout << "提取出: " << s << std::endl;
101 std::cout << "################################" << std::endl;
102 }
103 std::cout << "剩余:" << item->inbuffer << std::endl;
104 //3.针对一个一个的报文协议反序列化decode
105 //4.业务处理
106 //5.形成响应报文,序列化转成一个字符串encode
107 //6.写回
108 return 0;
109 }
W>110 int sender(EventItem* item)
111 {
112
113 return 0;
114 }
W>115 int errorer(EventItem* item)
116 {
117
118 return 0;
119 }
120
121 }
我们可以看到,我们可以通过自己定制的协议完成了报文和报文之间的分离,解决了粘包的问题!
我们通过我们实现的反序列化将我们预设好的协议进行了拆分。当然如果做项目的话肯定就用json、xml这样的工具了。
8. 核反应堆模式
接下来先做实验,代码后面就给。
此时我们可以将数据通过完成任务并写回给用户!
当然我们如果还想分层也是可以的!!!
在这里Reactor如果只负责读取数据流,进行报文和报文的分离,不在进行后续处理。把后续的业务处理、形成报文、构建响应这个工作交给后端软件层或者线程池,这就是基于Reactor的半同步半异步的工作方式。
半同步体现在Reactor帮我们读了数据,没有说数据就绪就直接通知我,半异步体现在它虽然读了,但是没有做处理。
而这种方式是Linux中最常用的工作方式,几乎没有之一!!!
其中具体细节大家去观看代码,当然其中还有可以优化的部分。
sock.hpp (final)
1 #pragma once
2
3 #include <iostream>
4 #include <unistd.h>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <netinet/in.h>
10
11
12 namespace ns_sock{
13
14 class Sock{
15 public:
16 static int Socket()
17 {
18 int sock = socket(AF_INET, SOCK_STREAM, 0);
19 if(sock < 0){
20 std::cerr << "socket error" << std::endl;
21 exit(1);
22 }
23 int opt = 1;
24 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
25 return sock;
26 }
27
28 static bool Bind(int sock, unsigned short port)
28 static bool Bind(int sock, unsigned short port)
29 {
30 struct sockaddr_in local;
31 memset(&local, 0, sizeof(local));
32 local.sin_family = AF_INET;
33 local.sin_port = htons(port);
34 local.sin_addr.s_addr = INADDR_ANY;
35
36 if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
37 std::cerr << "bind error" << std::endl;
38 exit(2);
39 }
40 return true;
41 }
42
43 static bool Listen(int sock, int backlog)
44 {
45 if(listen(sock, backlog) < 0){
46 std::cerr << "listen error" << std::endl;
47 exit(3);
48 }
49 return true;
50 }
51 };
52
53 }
server.cc (final)
1 #include "sock.hpp"
2 #include "util.hpp"
3 #include "app_interface.hpp"
4 #include "reactor.hpp"
5 #include <iostream>
6 #include <string>
7 #include <cstdlib>
8
9
10 const int back_log = 5;
11 static void Usage(std::string proc)
12 {
13 std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
14 }
15
16 int main(int argc, char* argv[])
17 {
18 if(argc != 2){
19 Usage(argv[0]);
20 exit(5);
21 }
22
23 uint16_t port = atoi(argv[1]);
24 //这个是与服务器listen_sock相关
25 int listen_sock = ns_sock::Sock::Socket();
26 ns_util::SetNonBlock(listen_sock);
27 ns_sock::Sock::Bind(listen_sock, port);
28 ns_sock::Sock::Listen(listen_sock, back_log);
29
30 std::cout << "listen_sock: " << listen_sock << std::endl;
31
32 //这个是我们的Epoll事件管理器
33 ns_epoll::Reactor R;
34 R.InitReactor();
35
36 ns_epoll::EventItem item;
37 item.sock = listen_sock;
38 item.R = &R;
39
40 //listen只需要关心读事件就行
41 //accepter函数TODU
42 item.ManagerCallBack(ns_appInterface::accepter, nullptr, nullptr);
43
44 //将我们的listen_sock托管给epoller! ET模式->一次把所有的事件添加完
45 R.AddEvent(listen_sock, EPOLLIN|EPOLLET, item);
46
47
48 int timeout = 1000;
49 while(true){
50 R.Dispatcher(timeout);
51 }
52
53 return 0;
54 }
util.hpp (final)
1 #pragma once
2
3 #include <iostream>
4 #include <unistd.h>
5 #include <fcntl.h>
6 #include <vector>
7
8 namespace ns_util{
9
10 void SetNonBlock(int sock)
11 {
12 int fl = fcntl(sock, F_GETFL);
13 if(fl < 0){
14 std::cerr << "fcntl error" << std::endl;
15 exit(6);
16 }
17 fcntl(sock, F_SETFL, fl | O_NONBLOCK);
18 }
19
20 class StringUtill{
21 public:
22 static void Split(std::string& in, std::vector<std::string>* out, std::string sep)
23 {
24 //aaXbbX ->X:代表的是一个完整报文结束
25 //aaXbbb
26 while(true){
27 size_t pos = in.find(sep);
28 if(pos == std::string::npos){
29 break;
30 }
31
32 std::string s = in.substr(0, pos);
33 out->push_back(s);
34 in.erase(0, pos+sep.size());
35 }
36 }
37
38 static void Deserialize(std::string& in, int* x, int* y)
39 {
40 size_t pos = in.find("+");
41 std::string left = in.substr(0, pos);
42 std::string right = in.substr(pos+1);
43 *x = atoi(left.c_str());
44 *y = atoi(right.c_str());
45 }
46 };
47
48
49
50 }
reactor.hpp (final)
1 #pragma once
2
3 #include "sock.hpp"
4 #include <sys/epoll.h>
5 #include <string>
6 #include <unordered_map>
7
8
9 namespace ns_epoll{
10
11 #define MAX_NUM 64
12
13 class Reactor;
14 class EventItem;
15
16
17 typedef int(*callback_t)(EventItem *);//const &:输入,*:输出,&输入输出
18
19 const int size = 256;
20
21 //当成一个结构体使用
22 class EventItem{
23 public:
24 //与通信相关
25 int sock;
26 //回指Reactor
27 Reactor* R;
28
29 //有关数据处理的回调函数,用来进行逻辑解耦的!
30 //应用数据就绪等通信细节和数据的处理模块使用该方法进行解耦!
31 callback_t recv_hander;
32 callback_t send_hander;
33 callback_t error_hander;
34
35 std::string inbuffer;//读取到的数据缓冲区
36 std::string outbuffer;//待发送的数据缓冲区
37 public:
38 EventItem()
39 :sock(0)
40 ,R(nullptr)
41 ,recv_hander(nullptr)
42 ,send_hander(nullptr)
43 ,error_hander(nullptr)
44 {}
45 void ManagerCallBack(callback_t _recv, callback_t _send, callback_t _error)
46 {
47 recv_hander = _recv;
48 send_hander = _send;
49 error_hander = _error;
50 }
51
52 ~EventItem()
53 {
54 //TODU
55 }
56 };
57
58
59
60 class Reactor{
61 private:
62 int epfd;
63
64 std::unordered_map<int, EventItem> event_items;//sock:EventItem
65 public:
66 Reactor()
67 :epfd(-1)
68 {}
69
70 void InitReactor()
71 {
72 if((epfd = epoll_create(size)) < 0){
73 std::cerr << "epoll_create error" << std::endl;
74 exit(4);
75 }
76 std::cout << "debug, epfd: " << epfd << std::endl;
77 }
78
79 void AddEvent(int sock, uint32_t event, const EventItem& item)
80 {
81 struct epoll_event ev;
82 ev.events = 0;
83 ev.events |= event;
84 ev.data.fd = sock;//我们在处理的时候只知道有事件就绪了,但是不知道哪个文件描述符就绪了
85 if(epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev) < 0){
86 std::cerr << "epoll_ctl error, fd: " << sock << std::endl;
87 }
88 event_items.insert({sock, item});
89
90 std::cout << "debug, 添加:" << sock << "到epoller中,成功" << std::endl;
91 }
92
93 void EnableReadWrite(int sock, bool read, bool write)
94 {
95 struct epoll_event evt;
96 evt.data.fd = sock;
97 evt.events = (read ? EPOLLIN : 0) | (write ? EPOLLOUT : 0) | EPOLLET;
98 if(epoll_ctl(epfd, EPOLL_CTL_MOD, sock, &evt) < 0){
99 std::cerr << "epoll_ctl_mod error, fd: " << sock << std::endl;
100 }
101 }
102
103 void DelEvent(int sock)
104 {
105 if(epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr) < 0){
106 std::cerr << "epoll_ctl_del error, fd: " << sock << std::endl;
107 }
108 event_items.erase(sock);
109 }
110
111 //事件分派器
112 void Dispatcher(int timeout)
113 {
114 //如果底层特定的事件就绪,我们就把对应的事件分派给指定的回调函数进行统一处理
115 struct epoll_event revs[MAX_NUM];
116 //返回值num表明有多少个事件就绪了,内核会将就绪事件依次放入revs中
117 int num = epoll_wait(epfd, revs, MAX_NUM, timeout);
118 for(int i = 0; i < num; i++)
119 {
120 int sock = revs[i].data.fd;
121
122 //优化的
123 uint32_t mask = revs[i].events;
124 //把所有的异常事件统一交给read,write去处理
125 if((revs[i].events & EPOLLERR) || (revs[i].events & EPOLLHUP)) mask |= (EPOLLIN|EPOLLOUT);
126
127 // if((revs[i].events & EPOLLERR) || (revs[i].events & EPOLLHUP))
128 // if(event_items[sock].error_hander) event_items[sock].error_hander(&event_items[sock]);
129
130 if(revs[i].events & EPOLLIN)
131 if(event_items[sock].recv_hander) event_items[sock].recv_hander(&event_items[sock]);//如果读取回调被设置,调用读回调
132
133 if(revs[i].events & EPOLLOUT)
134 if(event_items[sock].send_hander) event_items[sock].send_hander(&event_items[sock]);
135 }
136 }
137
138
139
140 ~Reactor()
141 {
142 if(epfd >= 0){
143 close(epfd);
144 }
145 }
146
147
148 };
149
150 }
app_interface.hpp (final)
1 #pragma once
2
3 #include "util.hpp"
4 #include "reactor.hpp"
5 #include <vector>
6
7 namespace ns_appInterface{
8
9 using namespace ns_epoll;
10 int recver(EventItem* item);
11 int sender(EventItem* item);
12 int errorer(EventItem* item);
13
14 //请问:什么时候会调用这里的accepter
15 int accepter(EventItem* item)
16 {
17 std::cout << "get a new link: " << item->sock << std::endl;
18 while(true)
19 {
20 struct sockaddr_in peer;
21 socklen_t len = sizeof(peer);
22 int sock = accept(item->sock, (struct sockaddr*)&peer, &len);
23 if(sock < 0){
24 if(errno == EAGAIN || errno == EWOULDBLOCK){
25 //说明没有读取出错,只是底层没有链接了!
26 return 0;
27 }
28 else if(errno == EINTR){
29 //读取过程中被信号打断了
30 continue;
31 }
32 else{
33 //真出错了!
34 //item->error_handler(item);
35 return -1;
36 }
37 }
38 else{
39 ns_util::SetNonBlock(sock);
40 //读取成功了!
41 EventItem tmp;
42 tmp.sock = sock;
43 tmp.R = item->R;
44 tmp.ManagerCallBack(recver, sender, errorer);
45 Reactor* epoller = item->R;
46 //epoll经常一定会设置读事件就绪,而写事件我们按序打开!
47 epoller->AddEvent(sock, EPOLLIN|EPOLLET, tmp);
48
49 }
50 }
51
52
53
54 return 0;
55 }
56
57 //0:读取成功
58 //-1:读取失败
59 int recver_helper(int sock, std::string* out)
60 {
61 while(true)
62 {
63 char buffer[128];
64 ssize_t size = recv(sock, buffer, sizeof(buffer) - 1, 0);
65 if(size < 0){
66 if(errno == EAGAIN || errno == EWOULDBLOCK){
67 //循环读取完毕
68 return 0;
69 }
70 else if(errno == EINTR){
71 //被信号中断,继续尝试
72 continue;
73 }
74 else{
75 //真正读取出错了 TODO
76 return -1;
77 }
78 }
79 else{
80 buffer[size] = 0;//string
81 *out += buffer;//将我们读到的数据添加到inbuffer中
82 }
83 }
84 }
85
86 int recver(EventItem* item)
87 {
88 //1.需要整体读,非阻塞
89 if(recver_helper(item->sock, &(item->inbuffer)) < 0)//读取失败
90 {
91 //item->error_hander
92 //item->error_handler(item);
93 return -1;
94 }
95 std::cout << "client# " << item->inbuffer << std::endl;
96 //2.根据发来的数据流,进行包和包之间的分离,防止粘包问题->这里是涉及到协议定制呢!
97 //当然可以设置成不可显的/3 /n,但是这里我设置成X,为了好看清楚!
98 std::vector<std::string> messages;
99 ns_util::StringUtill::Split(item->inbuffer, &messages, "X");
100 //for(auto s : messages){
101 // std::cout << "################################" << std::endl;
102 // std::cout << "提取出: " << s << std::endl;
103 // std::cout << "################################" << std::endl;
104 //}
105 //std::cout << "剩余:" << item->inbuffer << std::endl;
106
107 //3.针对一个一个的报文协议反序列化decode,也是协议定制的一部分
108 //1+1X2*3X6/2X
109 //1+1 2*3 6/2
110 struct data{
111 int x;
112 int y;
113 };
114 for(auto s :messages){
115 struct data d;
116 ns_util::StringUtill::Deserialize(s, &d.x, &d.y);
117
118 //当然这里可以将数据添加到线程池里去进行处理
119 // task t(d);
120 // thread_poll->push(t);
121
122 //std::cout << d.x << ":" << d.y << std::endl;
123 //4.业务处理
124 int z = d.x + d.y;
125
126 //5.形成响应报文,序列化转成一个字符串encode
127 std::string response;
128 response += std::to_string(d.x);
129 response += "+";
130 response += std::to_string(d.y);
131 response += "=";
132 response += std::to_string(z);
133 //将数据放到outbufferr里
134 item->outbuffer += response;
135 //设置响应报文和响应报文之间的分隔符
136 item->outbuffer += "X"; //encode
137 }
138
139 //6.写回(这里是读,不能写回,所以我们要将写打开)
140 if(!item->outbuffer.empty()) item->R->EnableReadWrite(item->sock, true, true);
141 return 0;
142 }
143
144 //0:写完inbuffer
145 //1:缓冲区打满,下次再写入
146 //-1:写出错
147 int sender_helper(int sock, std::string& in)
148 {
149 size_t total = 0;
150 size_t begin_pos = 0;
151 while(true)
152 {
153 size_t s = send(sock, in.c_str()+total, in.size()-total, 0);
154 if(s > 0){
155 total += s;
156 if(total >= in.size()){
157 return 0;
158 }
159 }
160 else if(s < 0){
161 if(errno == EAGAIN || errno == EWOULDBLOCK){
162 //无论是否发送完,inbuffer都需要将已经发送的数据全部移出缓冲区
163 in.erase(begin_pos, total);//移出去后将没发完的继续发
164 begin_pos = total;
165 return 1;//已经将缓冲区打满,不能再写入了(没写完)
166 }
167 else if(errno == EINTR){
168 continue;
169 }
170 else{
171 //TODO
172 return -1;
173 }
174 }
175 }
176 }
177
178 int sender(EventItem* item)
179 {
180 int ret = sender_helper(item->sock, item->outbuffer);
181 if(ret == 0){
182 item->R->EnableReadWrite(item->sock, true, false);
183 }
184 else if(ret == 1){
185 //默认不是打开了么?
186 //一般我们只要设置了EPOLLOUT,默认epoll会在下次自动触发一次
187 item->R->EnableReadWrite(item->sock, true, true);
188 }
189 else{
190 //TODO
191 //item->error_handler(item);
192 }
193
194 return 0;
195 }
196
197 //可能会有bug!
198 //(因为出错的时候我们将读写都设置了,所以在读和写中close和DelEvent会执行两次)
199 int errorer(EventItem* item)
200 {
201 close(item->sock);
202 item->R->DelEvent(item->sock);
203 return 0;
204 }
205
206 }
如上就是 高级IO 的所有知识,如果大家喜欢看此文章并且有收获,可以支持下 兔7 ,给 兔7 三连加关注,你的关注是对我最大的鼓励,也是我的创作动力~!
再次感谢大家观看,感谢大家支持!