2.1高性能网络设计专栏-网络编程
查看端口
netstat -anop | grep 2500
fd是一个int,依次增加
-
假设当前连接的有[3,4,5,6], 断开连接,立刻再连接,下次连接的就是7

-
那么4在什么时候会再使用呢?

创建的文件是一个fd,创建的socket也是一个fd,操作驱动也是操作fd
这是因为Linux系统的操作,一切皆文件

一请求一线程的优劣
优点:
- 代码逻辑简单
缺点:
- 不利于并发,只能做到大概1K的并发量
一个线程在poisx下,需要8M的内存,线程越多,导致内核调度的负担越大
IO多路复用
select
fd_set: 把多个IO放在一个集合一起管理
- fd_set集合的大小?
- fd集合如何理解,是什么?
- 每次调用需要把fd_set集合,从用户空间copy到内核空间
- maxfd, 遍历到最大的maxfd
for(i = 0; i < maxfd; i ++)
fd_set结构体:
fd_set其实这是一个数组的宏定义,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(socket、文件、管道、设备等)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪个句柄可读。
struct timeval结构体:
struct timeval{
long tv_sec;//second
long tv_usec;//minisecond
}
结构体成员有两个,第一个单位是秒,第二个单位是微妙 ,作用是时间为两个之和;
FD函数
系统提供了FD_SET, FD_CLR, FD_ISSET, FD_ZERO进行操作,声明如下:
FD_SET(int fd, fd_set *fdset); //将fd加入set集合
FD_CLR(int fd, fd_set *fdset); //将fd从set集合中清除
FD_ISSET(int fd, fd_set *fdset); //检测fd是否在set集合中,不在则返回0
FD_ZERO(fd_set *fdset); //将set清零使集合中不含任何fd
select函数
select函数是一个轮循函数,循环询问文件节点,可设置超时时间,超时时间到了就跳过代码继续往下执行。
select原理:select需要驱动程序的支持,驱动程序实现fops内的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。
函数定义:
int select(int maxfd, fd_set* readset, fd_set* writeset, fe_set* exceptset, struct timeval* timeout);
返回值:
返回fd的总数,错误时返回SOCKET_ERROR
参数:
maxfd 需要检查的文件描述字个数, 通常设置为所有fd中最大值+1。
readset 用来检查可读性的一组文件描述字。
writeset 用来检查可写性的一组文件描述字。
exceptset 用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)
timeout 超时,填NULL为阻塞,填0为非阻塞,其他为一段超时时间
select 函数使我们可以执行I/O多路转接。传给 select 的参数告诉内核∶
-
我们所关心的描述符;
-
对于每个描述符我们所关心的条件(是否想从一个给定的描述符读,是否想写一个给定的描述符,是否关心一个给定描述符的异常条件);
-
愿意等待多长时间(可以永远等待、等待一个固定的时间或者根本不等待)。
select 返回时,内核告诉我们∶
-
已准备好的描述符的总数量;
-
对于读、写或异常这3个条件中的每一个,哪些描述符已准备好。
使用这种返回信息,就可调用相应的 I/O函数,并且确知该函数不会阻塞。
网络IO的两种处理方式
1. accept --> listen //有新的连接进来
2. send, recv --> clientfd
使用select来监听IO里是否有数据
通过FD_SET(sockfd, &fdread)绑定刚刚客户端通过send给服务器的sockfd, 如果确认有数据了,就可以通过recv来读取sockfd中的数据buffer。
使用两个 fd_set rfds, rset, rfds是为了应用层使用, 可以对其进行FD_SET,rset用于拷贝到内核
Code
fd_set rfds, rset;
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
int maxfd = sockfd;
while(1) {
rset = rfds;
int nready = select(maxfd+1, &rset, NULL, NULL, NULL); //有多少个bit位设为1了,也就是这三个集合[r,w,e]有多少个fd可读可写
// maxfd+1最大的fd范围,timeout表示等待时长,多久询问一次,如果是NULL,代表阻塞态,一直等待新的fd
if(FD_ISSET(sockfd, &rset)) { //判断sockfd是否在rset集合中,也就是判断sockfd是否可读
// accept
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("客户端 %d 连接成功!\n", clientfd);
FD_SET(clientfd, &rfds);
if(maxfd < clientfd) maxfd = clientfd; //加入了新的clientfd,判断maxfd是否更新
}
// recv
for (int i = sockfd+1; i <= maxfd; i ++ ) { //处理的是fd
if(FD_ISSET(i, &rset)) {
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
// 客户端断开连接,recv返回0
if(count == 0) { //disconnect;
printf("client %d disconnect\n", i);
close(i);
FD_CLR(i, &rfds);
continue;
}
// parser,对客户端发来的信息进行解析
printf("Recv: %s\n", buffer);
count = send(i, buffer, count, 0);
printf("Send: %d\n", count);
}
}
}
poll的原理和实现
poll的源码

int nready = poll(fds, maxfd+1, -1);
poll的使用场景
struct pollfd fds[1024] = {0};
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLL_IN;
int maxfd = sockfd;
while(1) {
int nready = poll(fds, maxfd+1, -1);
if(fds[sockfd].revents & POLL_IN) {
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("客户端 %d 连接成功!\n", clientfd);
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLL_IN;
if(clientfd > maxfd) maxfd = clientfd;
}
for (int i = sockfd+1; i <= maxfd; i ++ ) {
if(fds[i].revents & POLL_IN) {
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
// 客户端断开连接,recv返回0
if(count == 0) { //disconnect;
printf("client %d disconnect\n", i);
close(i);
fds[i].fd = -1;
fds[i].events = 0;
continue;
}
// parser,对客户端发来的信息进行解析
printf("Recv: %s\n", buffer);
count = send(i, buffer, count, 0);
printf("Send: %d\n", count);
}
}
}
epoll
Linux 2.6以后, 引入epoll,致使海量用户选择使用Linux操作系统作为云服务器
epoll是什么?
epoll 是 Linux 内核提供的一种 I/O 多路复用机制,用于高效地监控大量文件描述符(file descriptor, fd)上的事件,例如可读、可写、错误等。 它允许一个进程同时监控多个文件描述符,并在某个或某些文件描述符就绪时,通知进程。 这使得单线程的程序可以同时处理多个网络连接或其他 I/O 事件,从而提高程序的并发性能和资源利用率。
————————————————
比如一个服务器(小区),里面有很多个客户端,每个客户端都在服务器有连接(socket),每个IO相当于小区的住户收发快递
epoll是来管理这些IO,能够检测到哪个IO有数据,从而把这个提示返回给应用层,便于实现业务逻辑。这个epoll相当于小区的快递员,来检测哪个住户有快递了, 这样就能高效地管理IO, 而不像一请求一线程那样存在大量且无效摆烂的IO。I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
epoll主要有这三个函数:
参考链接:https://zhuanlan.zhihu.com/p/17856755436
(1) epoll_create
创建一个struct eventpoll实例。
#include <sys/epoll.h>
int epoll_create(int size);
参数:size参数并没有实际意义,但一定要大于0。
返回值:成功返回epoll文件描述符;失败返回-1,并设置errno。
这是其内核源码实现:
SYSCALL_DEFINE1(epoll_create, int, size){
if (size <= 0) return -EINVAL; //size小于等于0,返回错误
return do_epoll_create(0); //传入参数没用到size
}
总结而言,epoll_create() :可以看作成,聘请一个快递员
(2) epoll_ctl
epoll_ctl函数用于向epoll实例中添加、修改或删除文件描述符,并设置这些IO关注的事件类型,如可读、可写或者有异常发生。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
-
epfd:指向由epoll_create 创建的 epoll 实例的文件描述符。
-
op:表示要对目标文件描述符执行的操作,可以是以下几个值之一:
EPOLL_CTL_ADD:向 epoll 实例中添加一个新的文件描述符。
EPOLL_CTL_MOD:修改已存在文件描述符的事件类型。
EPOLL_CTL_DEL:从 epoll 实例中删除一个文件描述符。
- fd:需要操作的目标文件描述符。
- event:指向
struct epoll_event结构的指针,该结构指定了需要监听的事件类型。可以看作是快递员接发的快递箱子。
struct epoll_event结构体:
struct epoll_event {
uint32_t events;
epoll_data_t data;
};
- events:指定要监听的事件类型,常见事件类型包括。
EPOLLIN:表示对应的文件描述符可以读。
EPOLLOUT:表示对应的文件描述符可以写。
EPOLLRDHUP:表示对端关闭连接或者半关闭连接。
EPOLLPRI:表示有紧急数据可读(带外数据)。
EPOLLERR:表示对应的文件描述符发生错误。
EPOLLHUP:表示对应的文件描述符挂起事件。
- data:用户自定义的数据
epoll_data有以下成员
typedefunion epoll_data {
void *ptr;
int fd; //设置socket文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
总结而言,epoll_ctl() 是来管理添加/关闭一个IO; 或者update一个IO从A到B
(3) epoll_wait
epoll_wait()函数用于等待在 epoll 实例上注册的文件描述符上发生的事件。这个函数会阻塞调用线程,直到有事件发生或超时。

如图所示,用户调用epoll_wait后,内核循环检测就绪队列是否有就绪事件,如果有就绪事件,将就绪事件返回给用户,否则继续往下执行,判断epoll是否超时,超时返回0,如果没有超时则将epoll线程挂起,epoll线程陷入休眠状态,同时插入一个epoll等待队列项。
当socket接收到数据后,会通过socket等待队列回调函数去检测epoll等待队列项,并将epoll线程唤醒,epoll线程被唤醒成功后,epoll线程再次查询就绪队列,此时就能成功返回socket事件。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数:
-
epfd:epoll文件描述符。
-
events:epoll事件数组。
-
maxevents:指定events 数组的大小,即可以存储的最大事件数。
-
timeout:超时时间
-1:表示无限等待,直到有事件发生。
0 :表示立即返回,不等待任何事件。
正数:表示等待的最大时间(毫秒)。
返回值: 小于0表示出错;等于0表示超时;大于0表示获取事件成功,返回就绪事件个数。
总结而言,epoll_wait()规定了等待特定的事件,以及多久时间去一次小区。
epoll在TCP服务器中的应用
epoll 是一种事件通知机制,它会监视一组文件描述符(包括套接字socket)的事件发生情况。
epoll_ctl这一步至关重要。它告诉epoll实例:"请开始监控sockfd这个充当服务器监听listen的文件描述符,我关心它上面的特定事件(例如EPOLLIN,表示可读事件)"
Code
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
while(1) {
struct epoll_event events[1024] = {0};
int nready = epoll_wait(epfd, events, 1024, -1);
for (int i = 0; i < nready; i ++ ) {
int connfd = events[i].data.fd;
if(connfd == sockfd) {
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("客户端 %d 连接成功!\n", clientfd);
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
} else if(events[i].events & EPOLLIN){
char buffer[1024] = {0};
int count = recv(connfd, buffer, 1024, 0);
if(count == 0) {
printf("client %d disconnect\n", connfd);
close(connfd);
// epoll删除connfd,可以不传入第4个参数event的配置, 传入NULL即可。
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
continue;
}
// parser,对客户端发来的信息进行解析
printf("Recv: %s\n", buffer);
count = send(connfd, buffer, count, 0);
printf("Send: %d\n", count);
}
}
}
Review
select/poll/epoll
io的事件触发,每一个io对应一个事件
io的生命周期,无数多个事件组成,不通的事件执行不同的回调函数
reactor: 反应堆

思考select/poll/epoll的使用场景
xxxxxxxxxxxxxxxxxxx
为什么需要reactor
什么是reactor:
- 作为Server端,我们关注的往往不是IO,而更关注的是IO所发生的事件,或者说对事件的管理
- 每当我注册一个事件,这个事件发生的时候就发生对应的回调函数;
- 由以前的IO管理,转变为了对IO事件管理。
实现reactor
不同的IO事件,对应不同的回调函数
- register
- callback
io --> event --> callback
listenfd --> EPOLLIN --> accept_cb
clientfd --> EPOLLIN --> recv_cb
clientfd --> EPOLLOUT --> send_cb

浙公网安备 33010602011771号