2.1高性能网络设计专栏-网络编程

查看端口

netstat -anop | grep 2500

fd是一个int,依次增加

  • 假设当前连接的有[3,4,5,6], 断开连接,立刻再连接,下次连接的就是7
    image

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

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

一请求一线程的优劣

优点:

  • 代码逻辑简单

缺点:

  • 不利于并发,只能做到大概1K的并发量

一个线程在poisx下,需要8M的内存,线程越多,导致内核调度的负担越大

IO多路复用

select

fd_set: 把多个IO放在一个集合一起管理

  • fd_set集合的大小?
  • fd集合如何理解,是什么?
  1. 每次调用需要把fd_set集合,从用户空间copy到内核空间
  2. 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_SETrset用于拷贝到内核

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的源码
image

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 实例上注册的文件描述符上发生的事件。这个函数会阻塞调用线程,直到有事件发生或超时。
image
如图所示,用户调用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: 反应堆
image

思考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
posted @ 2025-12-08 10:06  Xiaomostream  阅读(3)  评论(0)    收藏  举报