网络编程 - 网络io与多路复用select/ poll/epoll

网络io与多路复用select/ poll/epoll 笔记

重点:socket与文件描述符的关联,多路复用与select/poll 代码实现LT/ET的区别

1. 网络io

数据在网络空间的发送必然涉及到读写, 两个设备要通讯必然要建立连接, 而网络io就是这个连接,。

网络io: 你发数据别人接受数据就是网络io

2.socket(套接字)

socket是网络传输用的软件设备,它封装了一些接口方便我们操作。

详情阅读:

服务端套接字创建流程:

  • 第一步:调用socket函数接口
  • 第二步:调用bind函数分配IP地址和端口号
  • 第三步:调用listen函数转为可接受请求状态
  • 第四步:调用accept函数受理连接请求

客户端套接字创建流程:

  • 第一步:调用socket函数创建套接字
  • 第二部:调用connect函数向服务器端发送连接请求

(接受recv/发送send)

3.文件描述符

为了方便起了个别名 (文件描述符)fd 表示一个io,Linux把io都当作文件来操作。

fd(0,1,2)已经分配给系统,后面获得的fd都是依次递增的。

当连接断开原fd在一定时间不会重新分配(系统默认60s,你可以进行设置让一些fd可以快速回收),时间过了之后就可以重新设置了。

4.socket与文件描述符的关联

一些好用的指令:

查看端口:netstat -anop | grep 2000

从代码上可以知道listen后端口就存在了

查看端口:lsof -i:port

socket连接的现象

1.端口已经被绑定不可再次绑定

2.执行的listen,就可以通过netstat看到io的状态

3.进入listen后就可以被连接,并且会产生新的连接状态 (有io没分配)

4.io与tcp连接

在这里插入图片描述

netstat -anop | grep 2000 可以看到下面有两条信息说明代码产生两个fd

信息中fd与TCP连接信息1对1,多少个就对应多少个

fd(又叫io)与tcp连接不同。但在使用accpet的时候它们生命周期很近,没有要分开讲。

socket与文件描述符的关联:

1.listen就可以建立连接,但是建立连接是没有获得客户端的文件描述符(fd)

2.accept()的作用就是获取已经建立连接的文件描述符(fd)

ps:连接不代表获得fd,获得了fd才能进行操作

5.多线程实现io的处理(1请求1线程)

accept只能处理1个socket获得1个fd,如果直接使用循环:

  • 一个连接只能发一次,如果同一个客户端要再次发送要断开连接再连接1次。

  • 因为每次accpet后都会产生一个新的fd,所以需要关闭。

  • 还要按照连接顺序来发送数据,否则根本发不了

  • 因为它先accept后就阻塞了,后面再accept的客户端根本收不到数据,要等连接的发完才别的才能发

#include <iostream>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <pthread.h>
#include <string>

#include <poll.h>
#include <sys/epoll.h>

int main(){
    // 服务员
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 安排位置
    struct sockaddr_in serv_addr;
    serv_addr.sin_family = AF_INET;
    // INADDR_ANY = 0.0.0.0 是通配符,表示任意地址,可以监听任意地址的端口
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(2000);
    
    // 占好位置
    if ( bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0 ) {
        cout << "bind error" << endl;
    };

    // 开始监听(可以点菜)
    printf( "listening...\n" );
    listen(sockfd, 10);
    
    struct sockaddr_in cli_addr;
    socklen_t len = sizeof(cli_addr);

#if 0
    // 选择顾客(点菜) 
    printf( "waiting for client...\n" );
    int clientfd = accept(sockfd, (struct sockaddr*)&cli_addr, &len);
    
    char buffer[1024] = {0};

    printf("recv start\n");
    int count = recv(clientfd, buffer, 1024, 0);
    printf("recv: %s\n", buffer);
    
    count = send(clientfd, "00000", 5, 0);
    printf( "send: %d\n", count );
#elif 0
    // 这个一个连接只能发一次,如果同一个客户端要再次发送要断开连接再连接1次
    // 因为每次accpet后都会产生一个新的fd,所以需要关闭
    // 而且还要按照连接顺序来发送数据,否则根本发不了
    // 因为它先accept后就阻塞了,后面再accept的客户端根本收不到数据,要等连接的发完才别的才能发
    while(1){
        printf( "waiting for client...\n" );
        int clientfd = accept(sockfd, (struct sockaddr*)&cli_addr, &len);
        
        char buffer[1024] = {0};

        printf("recv start\n");
        int count = recv(clientfd, buffer, 1024, 0);
        if (count == 0) { // disconect
            printf("client close\n");
            close(clientfd);
            break;
        }

        printf("recv: %s\n", buffer);
        
        count = send(clientfd, "00000", 5, 0);
        printf( "send: %d\n", count );

    }
    
    return 0;
}

这样是不太好的我们就利用多线程来解决上面这个问题。

只要accpet接受到一个fd,我们就单独分配一个线程来对这个fd进行处理。这样这个fd的处理就不会影响到其他fd。

客户端断开的话,recv接收到的为0。

void* client_thread(void *arg){
    int clientfd = *(int *)arg;

    while(1){
         char buffer[1024] = {0};
        int count = recv(clientfd, buffer, 1024, 0);
        if (count == 0) { // disconect
            printf("client close\n");
            close(clientfd);
            break;
        }
        printf("recv: %s\n", buffer);
        
        count = send(clientfd, "00000", 5, 0);
        printf( "send: %d\n", count );
    }
   
}

1请求1线程的代码

    while(1){
        printf( "waiting for client...\n" );
        int clientfd = accept(sockfd, (struct sockaddr*)&cli_addr, &len);
        printf("%d\n",clientfd);
        pthread_t tid;
        pthread_create(&tid, NULL, client_thread, &clientfd);
        
    }

优点:逻辑简单

缺点:大并发太消耗资源根本无法支持。

6. io多路复用

epoll 用于大并发 poll小并发(1-2) 无poll才使用select

6.1 select

select的内容:

1.fd_set 结构体类型集合

2.FD_ZERO 清空fd集合

3.FD_SET 设置fd,相当于初始化

4.FD_ISSET FD_ISSET(sockfd, &rset)就是判断sockfd是否在集合(可读,可写,错误三个集合)中

5.select 轮询找到满足条件的fd,它是内核调用会copy fdset, 没满足条件就会阻塞在这里。

fd_set是一个bit位集合,默认1024个:

在这里插入图片描述
在这里插入图片描述

8是一个字节有8个比特 eg:long是4个字节 1024 / (8 * 4)= 32

这个代码就是在算这个类型要多少个才能表示1024个比特位。这个可以改来增加操作fd的个数。

select的流程:

rfds 放置现存的所有io

rset 是根据集合筛选出来的可使用的

select 进行轮询找能用的io集合

   // ======== select =========
   // 1.优点:实现了io多路复用
   // 2.缺点:参数太多而且麻烦

   fd_set rfds, rset;
   FD_ZERO(&rfds); // 清空fd
   FD_SET(sockfd, &rfds); // fd设置, 像初始化

   int maxfd = sockfd; // 记录最大fd

   while(1){
       rset = rfds;
       // rfds是应用层用的用了存的,rset是内核用的是用来判断的
       // 参数
       // 1.max fd作用是用来做循环条件 fd + 1 (因为是从0开始的)
       // 我们关注的条件,加rfds传入,如果满足条件,则返回满足条件的fd,和它的数量nready
       // 这就是为什么io(fd)要设置一个集合,其实就是在集合中找满足条件的最终子集
       // 2.第一个参数,是读集合,也就是fd_set结构体
       // 3.第二个参数,是写集合,也就是fd_set结构体
       // 4.第三个参数,是读集合,也就是fd_set结构体
       // 5.第5个参数,是超时时间,如果为NULL,则一直阻塞
       int nready = select(maxfd+1, &rset, NULL, NULL, NULL); // 阻塞在这里,只要io可读就返回不然就一直阻塞
       // select也是一个系统调用,每一次都要把fd的集合传入内核,然后内核再循环判断io是否就绪

       // 2.SOCKFD是第几位是否已经被设置
       // FD_ISSET(sockfd, &rset)就是判断sockfd是否在集合中
       if (FD_ISSET(sockfd, &rset)) {
           printf("accept\n");
           int clientfd = accept(sockfd, (struct sockaddr*)&cli_addr, &len);
           printf("%d\n",clientfd);
           FD_SET(clientfd, &rfds); // 添加到集合中
           if (maxfd < clientfd) maxfd = clientfd;
       }

       // recv
       int i = 0;
       for (i = sockfd + 1; i <= maxfd; ++i) {
           if (FD_ISSET(i, &rfds)) {
               printf("recv\n");
               char buffer[1024] = {0};
               int count = recv(i, buffer, 1024, 0);
               if (count == 0) { // disconect
                   printf("client close\n");
                   close(i);
                   FD_CLR(i, &rfds);
                   continue;
               } else {
                   printf("recv: %s\n", buffer);
               }

               count = send(i, buffer, count, 0);
           }
       }
   }

6.2 poll

poll的内容:(感觉在poll的时候就多了事件这个概念,这是select所没有的)

1.结构体 pollfd

在这里插入图片描述

events是传入的事件

revents是返回的事件

相关宏定义(用来判断)

在这里插入图片描述
在这里插入图片描述

2.poll是系统调用,把pollfd的fd集合放到到内核。内核循环看io是否就绪,不就绪进行阻塞。

3.poll的底层是还是select那一套,但是参数会少些。

poll的流程:

初始化pollfd结构体,设置好fd和对应事件

poll(fds, maxfd+1, -1); // 阻塞在这里,只要io可读就返回

if (fds[sockfd].revents & POLLIN) // 判断是否是想要的事件

#elif 0
    // ========= poll =========
    
    struct pollfd fds[1024] = {0};
    int n = 0;
    fds[sockfd].fd = sockfd; // 监听fd
    fds[sockfd].events = POLLIN; // 监听的事件

    int maxfd = sockfd;

    while(1){
        // 参数
        // 1.监听的个数
        // 2.监听的集合,也就是pollfd结构体数组
        // 3.超时时间,如果为NULL,则一直阻塞, -1表示一直阻塞
        int nready = poll(fds, maxfd+1, -1); // 阻塞在这里,只要io可读就返回

        if (fds[sockfd].revents & POLLIN){
            printf("accept\n");
            int clientfd = accept(sockfd, (struct sockaddr*)&cli_addr, &len);
            fds[clientfd].fd = clientfd; // 监听fd
            fds[clientfd].events = POLLIN; // 监听的事件
            if (maxfd < clientfd) maxfd = clientfd;
        }

        // recv
        int i = 0;
        for (i = sockfd + 1; i <= maxfd; ++i) {
            if (fds[i].revents & POLLIN) {
                printf("recv\n");
                char buffer[1024] = {0};
                int count = recv(i, buffer, 1024, 0);
                if (count == 0) { // disconect
                    printf("client close\n");
                    close(i);
                    fds[i].fd = -1;
                    fds[i].events = 0;
                    continue;
                } else {
                    printf("recv: %s\n", buffer);
                }

                count = send(i, buffer, count, 0);
                printf("send: %d\n", count);
            }
        }
    }

6.3 epoll

epoll使得大并发连接成为可能。

epoll的实现类比为送快递:

1.epoll实现上就是多了一个中转,原本fd是否可用都是轮询一个个询问(快递员一个个问是否有快递)。

2.而epoll是建立中转站(eg:丰巢),请求只要都往中转站里去放就绪,快递员只要定时来查询中转站就好,不用挨家挨户一个个查询(不用遍历所有fd)

epoll内容

epoll_create: 就是雇佣快递员 建立丰巢盒子。(建立就绪)

快递员就不用去找住户了,直接把快递放丰巢用户(io)自己来拿快递。

快递员只要定时来丰巢取快递和送快递就ok。

住户是整集,丰巢是就绪(用什么数据结构组织?)

(io)住户 --> 丰巢 <–快递员

epoll_ctl:是住户的搬出和搬入,改变了内部的位置

下面是相关操作码

EPOLL_CTL_ADD

EPOLL_CTL_DEL

EPOLL_CTL_MOD

epoll_wait:就是快递员多长时间去一次丰巢

在这里插入图片描述

evennts是取快递的快递箱

maxevents是最多取快递的数量

timeout就是间隔时间

struct epoll_event:
ev.data.fd = sockfd; // 监听fd
ev.events = EPOLLIN; // 监听的事件

epoll相比较select而言,对于大并发的优势在哪里??

1.能支持100w io

2.select(maxfd, 可读集合,可写集合,出错集合, err)100w的io,select要反复搬许多的io

epoll_create 后io是一个个添加,删除是一个个删除。

epoll的100w io是一点点加出来的, 积累

后面有io时间处理丰巢中的就绪就行

3.100w人同时在线不代表他们都同时发消息

每一个客户端对应一个io,100w中可能只要10w在发消息

就绪才是我们需要处理的事件,不用取处理100w更快

epoll流程

    // ========= epoll =========
    int epollfd = epoll_create(1);
    struct epoll_event ev;
    ev.data.fd = sockfd; // 监听fd
    ev.events = EPOLLIN; // 监听的事件
    // epoll_ctl参数
    // 第一个参数,epollfd是epoll_create返回的文件描述符
    // 第二个参数,EPOLL_CTL_ADD表示添加监听,EPOLL_CTL_DEL表示删除监听
    // 第三个参数,监听的fd
    // 第四个参数,epoll_event结构体指针
    epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);
    
    int maxfd = sockfd;
    
    while (1) {
        struct epoll_event events[1024] = {0};
        // 参数
        // 1.epollfd
        // 2.监听的个数
        // 3.监听的集合,也就是epoll_event结构体数组
        // 4.超时时间,如果为NULL,则一直阻塞, -1表示一直阻塞
        int nready = epoll_wait(epollfd, events, maxfd+1, -1); // 阻塞在这里,只要io可读就返回
        
        int i = 0;
        for(i = 0; i < nready; ++i) {
            int connfd = events[i].data.fd;
            if (connfd == sockfd) { // 监听fd
                printf("accept\n");
                int clientfd = accept(sockfd, (struct sockaddr*)&cli_addr, &len);
                printf("accept finished\n");

                ev.data.fd = clientfd; // 监听fd
                ev.events = EPOLLIN; // 监听的事件
                epoll_ctl(epollfd, 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 close\n");
                    close(connfd);
                    epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, NULL);
                    continue;
                }
                
                printf("recv: %s\n", buffer);
                
                count = send(connfd, buffer, count, 0);
                printf("send: %d\n", count);
            }
        }
        // if (events[sockfd].events & EPOLLIN){
        //     printf("accept\n");
        //     int clientfd = accept(sockfd, (struct sockaddr*)&cli_addr, &len);
        //     ev.data.fd = clientfd; // 监听fd
        //     ev.events = EPOLLIN
        // }
    }

io多路复用总结

select,poll,epoll 三种方式,都是对server提供io事件进行触发,处理的是io

代码实现LT/ET的区别

ET和LT的原理和区别-CSDN博客

epoll有两种触发模式

水平触发 (LT):buff中还有数据就一直触发
边沿触发 (ET):收到一次触发一次,so自己实现要加while循环,适合非阻塞的模式

event.events = EPOLLIN | EPOLLET; //ET 边沿触发模式
event.events = EPOLLIN; //默认 LT 水平触发模式 ()

适用场景

边沿触发: 更适合一个包大小不确定的

水平触发: 更适合一个包大小比较确定的

零声链接:https://xxetb.xetslk.com/s/1ooNS8

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值