2022-2-18 IO 多路复用的相关函数的介绍

零、一些基本函数的介绍
1、socket ()

#include<sys/socket.h>

int socket(int domain, int type. int protocol);

参数含义:
   int domain   -->  选择: (只列出比较常见的)
IPV4:AF_INET
IPV6:AF_INET6
本地协议:AF_UNIX
		AF_LOCAT
int type     --> 选择协议:
SOCK_STREAM -->TCP "面向连接的、可靠的、数据完整的基于字节流的连接"
SOCK_DGRAM  -->UDP "面向消息的、不可靠的、无连接的、报文传输"
int protocol    -->0 表示使用默认协议
返回值:
        成功:返回指向新创建的 socket 的文件描述符
        失败:返回 -1 设置errno。

socket 是在 Linux 下完成网络通信的特殊文件的类型,是内核借助缓冲区形成的伪文件。使用文件描述符是为了统一操作文件的接口。

2、bind()函数的介绍
服务器监听的地址和网络的端口号是固定不变的,服务器程序只要知道了服务器监听的端口号和网络地址就可以建立相应的连接。
bind () 的作用是绑定 socket 的文件描述符和服务器所监听的地址和端口号。
sockaddr 可以适配多种网络地址,由于不同的网络地址的长度是不同的,因此需要第三个参数来指定地址的大小。

使用 bind () 函数前需要设置好地址的端口号和网络。

#include <sys/types.h> 
#include <sys/socket.h>
//#include<arpa/inet.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:
 int sockfd ---> socket文件描述符
const struct addr ---> 构造出IP地址加端口号
socklen_t addrlen ---> sizeof(addr)长度
返回值:
	成功返回0,失败返回-1, 设置errno

3、accept()函数的介绍

#include<sys/socket.h>

int accept(int sockfd, struct spckaddr *addr, skcklen_t *sddlen);

参数:
	int sockfd ---> 服务器套接字的文件描述符
	struct spckaddr *addr --->保存发起连接请求的客户端地址信息的变量地址值,也就是返回链接客户端地址信息,含IP地址和端口号
					需要强制转换类型。传入参数
skcklen_t *sddlen ---> 传入sizeof(addr)第二个参数的大小,函数返回时返回真正接收到地址结构体的大小。传入参数

值得一提的是第二个参数和第三个参数:
	第二个参数:传出参数,返回的连接成功的信息,所以我们不需要对这个套接字进行初始化。
	第三个参数:传入传出参数,一开始传入一个参数防止溢出,调用完成之后长度会发生改变。
	因为是类型不是int 所以我们一般会在前面定义一个 skcklen_t 变量来衡量大小。
第三个参数具体使用:
skcklen_t = cliaddr_len;
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);//因为是传入传出所以需要涉及修改值,所以传入地址。
返回值:
		成功:成功返回一个新的socket文件描述符,用于和客户端通信。
		失败:返回-1,设置errno
		如果被其他的信号打断调用所设置的错误号为 EINTR,这个时候再次重启就好了。

accept() 用于连接请求队列当中的客户端的请求,函数调用成功的时候,会创建数据 I/O 套接字,以及返回文件描述符。套接字是自动创建的,并且与发送请求的客户端连接。
在这里插入图片描述
参考文章:socket编程之 accept函数的理解

一、阻塞等待
服务器只能和一个客户端建立起连接。
好处:一个时间只解析响应一个请求。CPU 的压力小。
缺点:同一时间只能连接一个客户端,效率低下。

一对一连接的服务器端

/*
    TCP 的服务器端
*/
#include<stdio.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>

int main(void){
    //1.创建 socket 套接字
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    if (lfd == -1)
    {
        perror("socket");
        exit(0);
    }
    
    //2.绑定
    struct sockaddr_in sadder;
    sadder.sin_family = AF_INET;
    //inet_pton(AF_INET,"127.0.0.1",sadder.sin_addr.s_addr);服务端写的是本机的地址
    sadder.sin_addr.s_addr = INADDR_ANY;//0.0.0.0 表示任意网关的地址,一种简单的写法

    sadder.sin_port= htons(9999);
    
    //sockaddr 和 sockaddr_in 这两种类型的指针可以互相转换?
    int ret = bind(lfd,(struct sockaddr*)&sadder,sizeof(sadder));
    if (ret == -1)
    {
        perror("bind");
        exit(-1);
    }

    //3.监听
    ret = listen(lfd,8);
    if (ret == -1)
    {
        perror("listen");
        exit(-1);
    }

    //4.接收客户端连接
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    int cfd = accept(lfd,(struct sockaddr*)&clientaddr,&len);
    if (cfd == -1)
    {
        perror("accept");
        exit(-1);
    }
    //输出客户端信息
    char clientIP[16];
    inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,clientIP,sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);
    printf("client ip is %s, port is %d.\n",clientIP,clientPort);

    //5.获取客户端的数据
    char recvBuf[1024] = {0}; 
   // int len2 = read(lfd,recvBuf,sizeof(recvBuf));
   int len2 = read(cfd,recvBuf,sizeof(recvBuf));
   //lfd 和 cfd 的区别是什么
   //创建客户端的socket 的fd 和接受客户端的socket有什么区别
    if (len2 == -1)
    {
        perror("read");
        exit(-1);
    }
    else if(len2 > 0){
        printf("recv client data : %s\n",recvBuf);

    }else if(len2 == 0){
        //表示客户端断开连接
        printf("client closed.\n");
    }
    //给客户端发送数据
    char *data = "hello , i am server.";
    write(cfd,data,strlen(data));
    close(lfd);
    close(cfd);
    return 0;
}

客户端

//tcp 通信客户端
#include<stdio.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>

int main(void){
    //1.创建套接字
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd == -1){
        perror("socket");
        exit(0);
    }

    //2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET,"192.168.131.136",&serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
    if(ret == -1){
        perror("connect");
        exit(0);
    }

    //3.通信
    char *data = "hello , i am client.";
    write(fd,data,strlen(data));

     char recvBuf[1024] = {0}; 
    int len2 = read(fd,recvBuf,sizeof(recvBuf));
    if (len2 == -1)
    {
        perror("read");
        exit(-1);
    }
    else if(len2 > 0){
        printf("recv client data : %s\n",recvBuf);

    }else if(len2 == 0){
        //表示服务端断开连接
        printf("server closed.\n");
    }
    close(fd);

    return 0;
}

在这里插入图片描述
二、BIO 模型
由独立的 accept() 来监听客户端的信息,为每一个客户端创建一个线程来处理客户端的请求。
当客户端无限多的时候,服务端的压力很大。
在这里插入图片描述

使用 多进程 / 多线程 来确保服务器来连接多个客户端。
优点:能够连接多个客户端
缺点:进程 / 线程消耗资源,进程 / 线程的调度消耗 CPU 资源

使用多进程的服务端
注意点:

  • 使用信号来回收死掉的进程。
  • 使用 waitpid ()在非阻塞情况下循环回收
  • 要对 accept 设置 关于错误号 EINTR 的信号的处理
#include<stdio.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>
#include<wait.h>
#include<errno.h>
//#include <sys/types.h>

void recycleChild(int arg){
    while (1)
    {
        int ret = waitpid(-1,NULL,WNOHANG);
        if(ret == -1){
            //所有的子进程都回收完了,等待下一次调用
            break;
        }else if (ret > 0){
            printf("child process %d is recycled.",ret);
        }else if (ret == 0){
            break;
            //还有子进程活着等待下一次调用

        }
    }
    
}

int main(void){
    //注册信号
    struct sigaction act;
   // sigaction(SIGCHLD,&act,NULL);
   //放错位置了,导致信号根本就没有注册好
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recycleChild;
    sigaction(SIGCHLD,&act,NULL);

    //创建socket
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    //绑定
    struct sockaddr_in serveraddr;
    serveraddr.sin_addr.s_addr = INADDR_ANY;
    serveraddr.sin_port = htons(9999);
    serveraddr.sin_family = AF_INET;
    int ret = bind(lfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
    if(ret == -1){
        perror("bind");
        exit(0);
    }

    //监听
    ret = listen(lfd,8);
    if(ret == -1){
        
        perror("listen");
        exit(0);
    }

    //不断循环等待客户端连接
    while (1)
    {
        struct sockaddr_in cliaddr;
        socklen_t len = sizeof(cliaddr);

        //建立连接
        //是连接的时候将客户端的信息写入结构体当中
        int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
        if(cfd == -1){
            if(errno == EINTR){
            continue;
            //能够保证客户端断开了之后依然能够连接
            //连接的时候出现了 EINTR 说明了什么?
            // EINTR  The  system  call was interrupted by a signal that was caught before a valid connection arrived.
            //accept()是阻塞函数,被信息打断后会产生返回-1,并设置 errno == EINTR。这个时候再重启一次就好
              
        }
            perror("accept");
            exit(0);
        }
        //每一个连接进来,创建一个子进程与客户端进行通信
        pid_t pid = fork();
        if(pid == 0){
            //子进程
            //获取客户端的信息
            char clienIP[16];
            inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,clienIP,sizeof(clienIP));
            unsigned short cliPort = ntohs(cliaddr.sin_port);
            printf("client ip is %s, client port is %d.\n",clienIP,cliPort);

            //接受客户端发来的数据
          //  char recvBuf[1024] = {0};
          char recvBuf[1024];//没有初始化会不会导致奇奇怪怪的数据。
            while (1)
            {
                int lenr = read(cfd,recvBuf,sizeof(recvBuf));
                if(lenr == -1){
                    perror("read");
                    exit(0);
                }else if(lenr > 0){
                    printf("recv client data: %s.\n",recvBuf);
                }
                else if(lenr == 0){
                    printf("client closed....\n");
                    break;

                }
                write(cfd,recvBuf,sizeof(recvBuf));
                //回射服务器,表示将客户端发送的数据再发回去
            }
            close(cfd);
           exit(0);//退出当前子进程
        }

    }
    close(lfd);
    
    
return 0;
}

为什么需要在 accept () 的返回值的检测中加上一个对 错误号 EINTR 的检测呢?
因为在子进程退出的时候会产生一个信号 SIGCLD,这个信号会造成 accept()的终止。
在这里插入图片描述
使用多线程的客户端
注意点:

  • 线程只能传入一个参数,所以如果要在线程内使用多个参数的情况下,要将需要传入的参数全部打包进一个结构体。
  • 将客户端的信息保存在结构体当中,但是结构体不能存放在线程当中,这样线程结束后这些数据也将会消失。
  • 使用结构体数组来限制最多可以创建的线程的大小。每个线程必然要处理一个含有客户端的信息的结构体。因此线程的最大的数量和结构体的最大的数量相等。
#include<stdio.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>

struct sockinfo
{
    int fd;//通信的文件描述符
    pthread_t tid;//线程号
    struct sockaddr_in addr;
};

struct sockinfo infos[128];

void*working(void * arg){
    //子线程和客户端通信 cfd 客户端的信息 线程号
    //由于arg 只是一块地址,无法传递多个参数,
    //因此需要将所有需要的参数封装成结构体,
    //再将结构体的地址作为参数传入参数当中。
    //获取客户端的信息
    struct sockinfo* pinfo = (struct sockinfo*)arg;

            char clienIP[16];
            inet_ntop(AF_INET,&pinfo->addr.sin_addr.s_addr,clienIP,sizeof(clienIP));
            unsigned short cliPort = ntohs(pinfo->addr.sin_port);
            printf("client ip is %s, client port is %d.\n",clienIP,cliPort);

            //接受客户端发来的数据
          //  char recvBuf[1024] = {0};
          char recvBuf[1024];//没有初始化会不会导致奇奇怪怪的数据。
            while (1)
            {
                int lenr = read(pinfo->fd,recvBuf,sizeof(recvBuf));
                if(lenr == -1){
                    perror("read");
                    exit(0);
                }else if(lenr > 0){
                    printf("recv client data: %s.\n",recvBuf);
                }
                else if(lenr == 0){
                    printf("client closed....\n");
                    break;

                }
                write(pinfo->fd,recvBuf,sizeof(recvBuf));
                //回射服务器,表示将客户端发送的数据再发回去
            }
            close(pinfo->fd);

            pinfo->fd = -1;
            pinfo->tid = -1;

            return  NULL;
}
int main(void){
     //创建socket
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    //绑定
    struct sockaddr_in serveraddr;
    serveraddr.sin_addr.s_addr = INADDR_ANY;
    serveraddr.sin_port = htons(9999);
    serveraddr.sin_family = AF_INET;
    int ret = bind(lfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
    if(ret == -1){
        perror("bind");
        exit(0);
    }

    //监听
    ret = listen(lfd,8);
    if(ret == -1){
        
        perror("listen");
        exit(0);
    }
    //初始化用来保存线程数据的结构体函数
    int max = sizeof(infos) / sizeof(infos[0]);
    for(int i = 0;i < max;i++){
        bzero(&infos[i],sizeof(infos[i]));
        infos[i].fd =  -1;
        //-1是一个无效的文件描述符,不可用,设置成任意的非负数表示有效可以用
        infos[i].tid = -1;

    }

    //循环等待客户端连接,一旦一个客户端连接进来,就创建一个子进程进行通信
    while (1)
    {
        struct sockaddr_in cliaddr;
        socklen_t len = sizeof(cliaddr);

        //建立连接
        //是连接的时候将客户端的信息写入结构体当中
        int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
        if(cfd == -1){
            perror("accept");
            exit(0);
        }
        //封装客户端的信息传递给线程
        struct sockinfo* pinfo;
        for(int i = 0;i < max;i++){
            //从这个数组中找到第一个可用的结构体信息标识符
            if(infos[i].fd == -1){
                pinfo = &infos[i];
                //但是如果都用完了怎么办?
                //猜测在线程断开连接的时候需要对使用的数据模块进行回收
                break;
            }
            if(i == max-1){
                sleep(1);
                i--;
                //如果找不到怎么办?暂停一秒看是否还有其他的可以使用
                //i--是为了防止跳出循环,否则后面直接非法访问申请数组
            }
        }
        pinfo->fd = cfd;
        memcpy(&pinfo->addr,&cliaddr,len);


        // pinfo.addr.sin_addr.s_addr = cliaddr.sin_addr.s_addr;
        // pinfo
        //结构体不能够直接复制,要么分别复制每个成员,要么使用memcopy
        //pinfo.fd = cfd;

        //创建子线程
       // pthread_t tid;
        pthread_create(&pinfo->tid,NULL,working,NULL);

       // pthread_join(pinfo->tid,NULL);
       //不能够使用 join 函数,因为这个函数是阻塞的
       pthread_detach(pinfo->tid);



    }
    //有以下几个问题需要解决:
    //1.新创建的线程只能传递一个参数,而子进程有多个信息需要传递。
    //2.tid 是在线程创建好了之后才会有的值,所以一开始需要直接给结构体赋值
    //3.保存所有客户端信息的结构体是一个局部变量,只有在本次循环中才有效,
    //本次循环结束后才会回收,回收之后线程无法继续使用。
    //如果使用 malloc 进行分配的话,之后需要在子进程当中进行回收,空间管理的难度大
    //如果有多个客户端来连接,堆空间占用太大
    //问题3的解决方法,定义全局变量结构体数据,和规定最多能够连接进来的客户端的上限
    close(lfd);
    return 0;
}

三、非阻塞、忙轮询
忙轮询是指,数据没有来的情况下,服务端会主动不断的检测是否有数据到达。
优点:提高了程序的执行的效率
缺点:占用了更过的 CPU 系统

四、NIO
原先的阻塞 IO 存在着什么问题?
1、大量的客户端请求下,为每一个客户端创建一个线程处理业务不合适。
2、在没有接收到数据的时候线程会被阻塞挂起,CPU 的利用率很低。
3、如果网络路线传输的数据较少,整个线程有故障,会显得比较不可靠。

通过使用 NIO 可以使得线程非阻塞,在数据没有到达的时候去做其他的事情。

1、select()函数的使用
可以委托内核检测一个或多个事件,当内核检测到其中第一个或者多个事情发生了,内核会将检测结果返回给它。
(有点像信号注册,先注册好处理的方法,检测到了再返回。)

  • 构造一个文件描述符的列表,列表里面包含了需要委托内核检测的文件描述符的信息。
  • 调用一个系统函数,该函数会负责监听传递的文件描述符列表中有哪些发生了变化。如果检测到变化发生,会将检测到的变化的个数返回。
    • 该系统调用函数是阻塞的。(有种委托别人阻塞等待,自己不阻塞等待的感觉。
    • 交由内核检测,检测到变化后返回。

缺点:
1.调用一次 select 需要将 fd 从用户态拷贝到内核态。
2、每次调用都需要遍历一遍 fd,开销大
3、fd 的上限是 1024个,比较小也不够灵活
4、fds 的集合不能重用,每次都得重新设置一次

// sizeof(fd_set) = 128 1024
 #include <sys/time.h>
  #include <sys/types.h> 
  #include <unistd.h> 
  #include <sys/select.h> 
  int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 
 - 参数: 
 - nfds : 委托内核检测的最大文件描述符的值 + 1 
 (检测范围为 [0,nfds + 1)左闭右开区间
 - readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性 
 - 一般检测读操作 
 - 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区 
 - 是一个传入传出参数 
 - writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
 - 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写) - -- exceptfds : 检测发生异常的文件描述符的集合 
 - timeout : 设置的超时时间 
 - struct timeval { 
 - long tv_sec; /* seconds */ 
 - long tv_usec; /* microseconds */ 
 - };
 - NULL : 永久阻塞,直到检测到了文件描述符有变化。(永久等待) 
 - tv_sec = 0 tv_usec = 0, 不阻塞 。检测到结果后立刻返回,无论是否发生变化,和轮询相当。
- tv_sec > 0 tv_usec > 0, 阻塞对应的时间 (等待一定时间)
-(其中,永久等待和等待一定时间这两种方式会被等待期间所捕获的函数中断,此时 select 会返回 -1,并且设置错误号为 EINTR)
 - 返回值 :
  -   -1 : 失败 
 - - >0(n) : 检测的集合中有n个文件描述符发生了变化

select()使用描述符的集合。描述符集合可以设置为一个数组单元。实现的原理大概是:假设使用 32位整数作为集合数组里面的元素,这个数组单元的第一个元素有 32 个二进制位,表示 0 ~ 31 位上的文件描述符,第二个元素则表示 32 ~ 63 位置上的文件描述符的有无。
在这里插入图片描述

// 将参数文件描述符fd对应的标志位设置为0 
void FD_CLR(int fd, fd_set *set); 
// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1 
int FD_ISSET(int fd, fd_set *set); 
// 将参数文件描述符fd 对应的标志位,设置为1 
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0 
void FD_ZERO(fd_set *set);

通过下面这些宏来测试和设置 文件描述符的集合。
在这里插入图片描述
在这里插入图片描述
使用select 来实现 IO 复用

#include<stdio.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>

int main(void){
    //创建 socket
    int lfd = socket(PF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    //socket 是一块缓冲区?用来接收网络中发送来的信息?
    //On success, a file descriptor for the new socket is returned.  
    //On error, -1 is returned, and errno is set appropriately.
    //程序通过缓冲区对这个文件进行操作吗?


    //绑定
    bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));

    //监听
    listen(lfd,8);

    //之前写的代码是什么模型?为什么那个效率是低的?为什么这个效率搞?

    //创建一个 fd_set 的集合,存放的是需要检测的文件描述符。
    //fd_set rdset;
    //需要定义两个集合,因为内核在检测的时候,也将原来文件描述符上的1修改成了0
    fd_set rdset,tmp;
    //一个由程序操作,一个交给内存修改
    FD_ZERO(&rdset);
    FD_SET(lfd,&rdset);
    int maxfd = lfd;
   //设置检测文件描述符的最大的范围

    while (1)
    {
        //tmp = rdset;
        memcpy(&tmp,&rdset,sizeof(rdset));
        //备份原来的文件描述符

        //调用 select函数,让内核帮忙检测哪些文件描述符有数据。
       // int ret = select(maxfd + 1,&rdset,NULL,NULL,NULL);
       int ret = select(maxfd + 1,&tmp,NULL,NULL,NULL);
        if(ret == -1){
            perror("select");
            exit(0);
        }else if(ret == 0){
            continue;
            //设置为永远等待不可能返回0
        }else if(ret > 0){
            //说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            //为啥有客户端连接,读文件描述符会发生变化
            
         //   if(FD_ISSET(lfd,&rdset)){
             if(FD_ISSET(lfd,&tmp)){
                //监听的文件描述符发生了变化,说明有客户端连接进来
                struct sockaddr_in cliaddr;
                socklen_t len = sizeof(cliaddr);
                int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
                if(cfd == -1){
                    perror("accept");
                    exit(0);
                }
                //将新的文件描述符加入到集合当中
                FD_SET(cfd,&rdset);

                //更新最大的文件描述符
                maxfd = maxfd > cfd ? maxfd:cfd;
            }
            for(int i = lfd + 1;i <= maxfd;i++){
              //  if(FD_ISSET(i,&rdset)){
                  if(FD_ISSET(i,&tmp)){
                    //说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len1 = read(i,buf,sizeof(buf));
                    if(len1 == -1){
                        perror("read");
                        exit(0);
                    }else if(len1 == 0){
                        //表示客户端断开了连接
                        printf("client closed....\n");
                        close(i);
                        FD_CLR(i,&rdset);
                    }else if(len1 > 0){
                        printf("read buf = %s\n",buf);
                        write(i,buf,sizeof(buf));
                    }
                }
            }

        }
       //其实返回值不大可能是0,因为select 的最后一个参数设置为 NULL,
       //意味着没有设置 timeout,表示这个 select函数会阻塞直到写文件描述符发生变化。
       //而返回值是 0 ,意味着超时,所以不可能超时。

    }
    close(lfd);
    return 0;
}

没有解决的疑惑,为啥有客户端的连接会造成读文件描述符发生变化。
select () 的文件描述符集合交由内核检测后会被置为 0 ,怀疑是防止重复检测。

2、poll函数
检测针对特定的文件描述符的某个事件,传入的是文件描述符的集合。

相比于 select()功能更加强大了。

  • select 能够检测 0 ~ 1023 一共 1024 个文件描述符,poll能够检测的个数比这个多。而且 select采用循环遍历的模式中间会遍历经过无效的文件描述符。
  • poll 对于每个文件描述符能够检测到的事件的范围更大。
#include <poll.h> 
struct pollfd { 
int fd; /* 委托内核检测的文件描述符 */ 
short events; /* 委托内核检测文件描述符的什么事件 */ 
short revents; /* 文件描述符实际发生的事件 */ 
};
struct pollfd myfd; 
myfd.fd = 5; 
myfd.events = POLLIN | POLLOUT; 
int poll(struct pollfd *fds, nfds_t nfds, int timeout); 
- 参数:
- fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合 
- nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1 
- timeout : 阻塞时长 
 0 : 不阻塞 
 -1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞 
 >0 : 阻塞的时长 
 - 返回值: 
-1 : 失败 
 >0(n) : 成功,n表示检测到集合中有n个文件描述符发生变化

在这里插入图片描述

示例函数:

#include<stdio.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<poll.h>

int main(void){
    //创建 socket
    int lfd = socket(PF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    //绑定
    bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));

    //监听
    listen(lfd,8);

    //初始化检测的文件描述符数组
    struct pollfd fds[1024];
    for(int i = 0;i < 1024;i++){
        fds[i].fd = -1;
        fds[i].events = POLLIN;
        //检测文件描述符需要的事件,POLLIN
    }

    //为什么要开一个数组和 fds 绑定
    fds[0].fd = lfd;
   
   //这个是干啥来着?
    int nfds = 0;
    //界定集合中检测的范围

   
    while (1)
    {
       int ret = poll(fds,nfds+1,-1);
        if(ret == -1){
            perror("poll");
            exit(0);
        }else if(ret == 0){
        //设置成阻塞,返回值不可能等于0
            continue;
        }else if(ret > 0){
            //说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
             if(fds[0].revents & POLLIN){
                //监听的文件描述符发生了变化,说明有客户端连接进来
                //我有点明白了,客户端向服务端发送信息,相当于向服务端和网络通信的的文件缓冲区中写入数据,只要有数据写入,文件描述符的事件就发生了变化。
                struct sockaddr_in cliaddr;
                socklen_t len = sizeof(cliaddr);
                int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
                //获取客户端地址,和为连接进来的客户端分配套接字。
                if(cfd == -1){
                    perror("accept");
                    exit(0);
                }
                //将新的文件描述符加入到集合当中
                //找到第一空位加入套接字
                for(int i = 1;i < 1024;i++){
                    if(fds[i].fd == -1){
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                        break;
                    }
                }

                //更新最大的文件描述符的索引
                //老是感觉不是这样更新的,而是用上面循环中加入的 i 的值作为更新的值。
                //nfds = nfds > i ? nfds:i;
                nfds = nfds > cfd ? nfds:cfd;
            }
            for(int i = 1;i <= nfds;i++){
                  if(fds[i].revents & POLLIN){
                    //说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len1 = read(fds[i].fd,buf,sizeof(buf));
                    if(len1 == -1){
                        perror("read");
                        exit(0);
                    }else if(len1 == 0){
                        //表示客户端断开了连接
                        printf("client closed....\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    }else if(len1 > 0){
                        printf("read buf = %s\n",buf);
                        write(fds[i].fd,buf,sizeof(buf));
                    }
                }
            }
        }
    }
    close(lfd);
    return 0;
}

3、epoll ()函数

#include <sys/epoll.h> 
// 创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检 测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向 链表)。 
/*epoll_create()  creates  a  new epoll(7) instance.  Since Linux 2.6.8, the size argument is ignored, but must be greater than zero; see NOTES below.
epoll_create() returns a file descriptor referring to the new epoll  instance.   This  file descriptor is used for all the subsequent calls to the epoll interface.  When no longer required, the file descriptor returned by epoll_create() should be closed by using  close(2).
       When  all  file descriptors referring to an epoll instance have been closed, the kernel de‐
       stroys the instance and releases the associated resources for reuse.
int epoll_create(int size);*/
 - 参数:size : 目前没有意义了。随便写一个数,必须大于0
 - 返回值:
 -1 : 失败
 -  > 0 : 文件描述符,操作epoll实例的

typedef union epoll_data {
 void *ptr; //可以传递想要传递的所有参数,相当于万能接口,可以将参数打包成一个结构体传入进去
 //*ptr不会带来epoll性能损失,这是必然的,因为是一个固定的指针,并不影响epoll本身,但用户在在扩展epoll高并发多业务服务的时候,定义指针的方式,自己实现上面还是会有一定的性能问题,即便优化成哈希存储,epoll返回也还要进行二次寻址。
 //(这一段不明白)
 
 int fd; //目前知道这个是传入 socketfd 的值,
 //其他的参数存在是为了用户自定义的参数的使用。
 uint32_t u32; 
 uint64_t u64; 
 } epoll_data_t; 
 
 struct epoll_event { 
 uint32_t events; /* Epoll events */ 
 epoll_data_t data; /* User data variable */ 
 };
 /* The events member is a bit mask composed by ORing together zero or more of the following available event types:
EPOLLIN 读数据,要缓冲区里有数据
The associated file is available for read(2) operations.
 EPOLLOUT 写数据
 The associated file is available for write(2) operations.
EPOLLRDHUP (since Linux 2.6.17) 用于报告一段断开请求。
在没有这个标志之前:明明是对方断开请求,系统却报告一个查询失败的错误,但从用户角度来看请求的结果正常返回,没有任何问题。
(会造成服务端和客户端对于传输错误的认识不一致)
Stream socket peer closed connection, or shut down writing  half  of  connection.(This  flag  is especially useful for writing simple code to detect peer shutdown when using Edge Triggered monitoring.)*/
 
 常见的Epoll检测事件: 
 - EPOLLIN 
 - EPOLLOUT 
 - EPOLLERR 

// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息 
 - int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 
 - 参数: 
 - epfd : epoll实例对应的文件描述符 (实例用于存放要检测的数据。相当于要检测的文件描述符在内核中的报备)
 - op : 要进行什么操作
 -  EPOLL_CTL_ADD: 添加 
 - EPOLL_CTL_MOD: 修改 
 - EPOLL_CTL_DEL: 删除 
 - fd : 要检测的文件描述符 
 - event : 检测文件描述符什么事情 
 
  // 检测函数 
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 
 - 参数:
 - epfd : epoll实例对应的文件描述符 
 - events : 传出参数,保存了发送了变化的文件描述符的信息 
 - maxevents : 第二个参数结构体数组的大小 
 - timeout : 阻塞时间 
 - 0 : 不阻塞 
 -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞 
 -> 0 : 阻塞的时长(毫秒) 
 - 返回值: 
 - 成功,返回发送变化的文件描述符的个数 > 0 
 - 失败 -1

示例函数:

#include<stdio.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/epoll.h>

int main(void){
     //创建 socket
    int lfd = socket(PF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    //绑定
    bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));

    //监听
    listen(lfd,8);

    //调用 epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    //将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN | EPOLLOUT;
    epev.data.fd = lfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);

//创建 epoll_event 数组用来存放和客户端建立连接的套接字,和对每个套接字需要监听的事件。
    struct epoll_event epevs[1024];

    while (1)
    {   
        //-1表示让它阻塞
        int ret = epoll_wait(epfd,epevs,1024,-1);
        if(ret == -1){
            perror("epoll_wait");
            exit(0);
        }
        printf("ret = %d.\n",ret);

        for(int i = 0;i < ret;i++){
            int curfd = epevs[i].data.fd;
            if(curfd == lfd){
                //监听的文件描述符有数据达到,有客户端连接
                //表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                socklen_t len = sizeof(cliaddr);
                int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
                if(cfd == -1){
                    perror("accept");
                    exit(0);
                }

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);
                //为和客户端建立的连接的套接字加入文件描述符
            }
            else{
                if(epevs[i].events & EPOLLOUT){
                    continue;
                }
                //为啥写事件需要继续监听?
                //因为上面检测的是读事件和写事件。检测到写事件不能够走这条逻辑。做判断是为了避免读事件,而引发不必要的逻辑。
                //如果监听了很多的事件 ,每种事件都需要特殊的处理。
                //有数据达到需要通信
                char buf[1024] = {0};
                int len1 = read(curfd,buf,sizeof(buf));
                if(len1 == -1){
                    perror("read");
                    exit(0);
                }else if(len1 == 0){
                //表示客户端断开了连接
                    printf("client closed....\n");
                    //epoll_ctl(curfd,EPOLL_CTL_ADD,curfd,NULL);
                    //感觉这句完全是错的
                    //我的感觉是对哒
         epoll_ctl(epfd,EPOLL_CTL_DEL,curfd,NULL);
         //关闭连接的时候需要从保存所有的连接的信息当中删除关闭的连接的信息
                    close(curfd);
                }else if(len1 > 0){
                    printf("read buf = %s\n",buf);
                    write(curfd,buf,sizeof(buf));
                }
            }

        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

epoll 的两种工作方式:

  • LT 模式(水平触发模式)
    以委托内核进行读事件的检测为例。
    缓冲区没有数据内核不通知,缓冲区有数据内核通知。内核会一直通知直到缓冲区当中的数据被读取完。
  • ET 模式(边沿触发模式)
    以委托内核进行读事件的检测为例。
    缓冲区收到数据内核通知,但是只在检测到收到数据的那一刻通知,之后都不通知。无论数据是否读完。

(这个就是负责任的通知方式 和 佛系通知方式的区别)。

边沿触发模式示例,常与非阻塞读一起使用

epoll_et.c

#include<stdio.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/epoll.h>
#include <fcntl.h>
#include<errno.h>

int main(void){
     //创建 socket
    int lfd = socket(PF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    //绑定
    bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));

    //监听
    listen(lfd,8);

    //调用 epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    //将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN | EPOLLOUT;
    epev.data.fd = lfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);

    struct epoll_event epevs[1024];

    while (1)
    {   
        //-1表示让它阻塞
        int ret = epoll_wait(epfd,epevs,1024,-1);
        if(ret == -1){
            perror("epoll_wait");
            exit(0);
        }
        printf("ret = %d.\n",ret);

        for(int i = 0;i < ret;i++){
            int curfd = epevs[i].data.fd;
            if(curfd == lfd){
                //监听的文件描述符有数据达到,有客户端连接
                //表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                socklen_t len = sizeof(cliaddr);
                int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
                //设置 cfd 的属性非阻塞
                //因为一旦 read 读完了会导致阻塞而无法继续执行循环
                int flag = fcntl(cfd,F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd,F_SETFL,flag);

                
                if(cfd == -1){
                    perror("accept");
                    exit(0);
                }

                epev.events = EPOLLIN | EPOLLET;//设置边沿触发
                epev.data.fd = cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);
            }else{
                if(epevs[i].events & EPOLLOUT){
                    continue;
                }
                /*下面这种能够循环读完*/
                //一次性读取所有数据(循环)
                //但是读完缓冲区有个问题就是read可能会导致阻塞,无法跳出循环。
                //read()的阻塞行为由 文件描述符 fd 来控制
                char buf[5] = {0};
                int len1 = 0;
                while ((len1 =read(curfd,buf,sizeof(buf)) )>0 )
                {
                    //答应数据
                    printf("recv data : %s.\n",buf);
                    write(curfd,buf,len1);

                }
                if(len1 == 0){
                    printf("client closed....");
                    epoll_ctl(epfd,EPOLL_CTL_ADD,curfd,NULL);
                    close(curfd);
                }
                else if(len1 == -1)
                {
                    if(errno == EAGAIN){
                        printf("data over...\n");
                    }else{
                        perror(read);
                        exit(0);
                    }
                //返回值为-1,1)出现了错误,
                //2)非阻塞的情况下读完了再读了一次没有读到数据
                    // perror(read);
                    // exit(0);
                }
                
                



                /*下面这种方式不能一次性读完*/
               /* //如果监听了很多的事件 ,每种事件都需要特殊的处理。
                //有数据达到需要通信
                char buf[5] = {0};
                int len1 = read(curfd,buf,sizeof(buf));
                if(len1 == -1){
                    perror("read");
                    exit(0);
                }else if(len1 == 0){
                //表示客户端断开了连接
                    printf("client closed....\n");
                    epoll_ctl(curfd,EPOLL_CTL_ADD,curfd,NULL);
                    close(curfd);
                }else if(len1 > 0){
                    printf("read buf = %s\n",buf);
                    write(curfd,buf,sizeof(buf));
                }*/
            }

        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

在这里插入图片描述
缓冲区还有数据,但是不会再通知了。
要读一次性把数据读完需要使用 while 循环通过循环来读取处所有的数据。

水平触发模式示例,默认水平触发
epoll_lt.c

#include<stdio.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/epoll.h>

int main(void){
     //创建 socket
    int lfd = socket(PF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    //绑定
    bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));

    //监听
    listen(lfd,8);

    //调用 epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    //将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN | EPOLLOUT;
    epev.data.fd = lfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);

    struct epoll_event epevs[1024];

    while (1)
    {   
        //-1表示让它阻塞
        int ret = epoll_wait(epfd,epevs,1024,-1);
        if(ret == -1){
            perror("epoll_wait");
            exit(0);
        }
        printf("ret = %d.\n",ret);

        for(int i = 0;i < ret;i++){
            int curfd = epevs[i].data.fd;
            if(curfd == lfd){
                //监听的文件描述符有数据达到,有客户端连接
                //表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                socklen_t len = sizeof(cliaddr);
                int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
                if(cfd == -1){
                    perror("accept");
                    exit(0);
                }

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);
            }else{
                if(epevs[i].events & EPOLLOUT){
                    continue;
                }
                //如果监听了很多的事件 ,每种事件都需要特殊的处理。
                //有数据达到需要通信
                char buf[5] = {0};
                int len1 = read(curfd,buf,sizeof(buf));
                if(len1 == -1){
                    perror("read");
                    exit(0);
                }else if(len1 == 0){
                //表示客户端断开了连接
                    printf("client closed....\n");
                    epoll_ctl(curfd,EPOLL_CTL_ADD,curfd,NULL);
                    close(curfd);
                }else if(len1 > 0){
                    printf("read buf = %s\n",buf);
                    write(curfd,buf,sizeof(buf));
                }
            }

        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

客户端
在这里插入图片描述
服务端
在这里插入图片描述

client.c

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(void){
    //创建 socket
    int fd = socket(PF_INET,SOCK_STREAM,0);
    if(fd == -1){
        perror("socket");
        exit(0);
    }
    //连接服务器端,需要知道服务器的地址
    struct sockaddr_in seraddr;
    inet_pton(AF_INET,"127.0.0.1",&seraddr.sin_addr.s_addr);
    seraddr.sin_port = htons(9999);
    seraddr.sin_family = AF_INET;
    //p-presentation表达,n-numeric数值
    //p的格式是 ASCII 字符串,
    //而 numeric 的表达格式是放到套接字地址结构中的二进制值
    int ret = connect(fd,(struct sockaddr*)&seraddr,sizeof(seraddr));
    if(ret == -1){
        perror("connect");
        exit(0);
    }

    int num = 0;
    while (1)
    {
        char sendbuf[1024] = {0};
        //sprintf(sendbuf,"send data %d.\n",num++);
        fgets(sendbuf,sizeof(sendbuf),stdin);
        write(fd,sendbuf,strlen(sendbuf)+1);
        //接收数据
        int len = read(fd,sendbuf,sizeof(sendbuf));
        if(len == -1){
            perror("read");
            exit(0);
        }else if(len == 0){
            printf("client closed....\n");
            break;
        }else if(len > 0){
            printf("read buf = %s\n",sendbuf);
        }
       // usleep(1000);
    }
    close(fd);

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值