多路I/O复用:select、poll、epoll(一)

IO有两种操作:

  • 同步IO,必须等待IO操作完成后,控制权才返回给用户进程;
  • 异步IO,无需等待IO操作完成,就将控制权返回给用户进程。

当一个网络IO发生时,会涉及两个系统对象,一个是调用此IO的进程,另一个是系统内核。该IO操作会经历两个阶段:

  1. 等待数据准备;

  2. 将数据从内核拷贝到进程中。

多路IO复用模型之select

多路IO复用模型之poll
多路IO复用模型之epoll

slect不断的轮询所负责的所有socket,当有数据到达时,就通知用户进程,其模型的流程图如下:

在这里插入图片描述

当用户进程调用了select,整个进程会被阻塞,同时内核会监听所有select负责的socket,当socket当中有数据准备好时select就会返回。

在应用进程调用了recvfrom这个系统调用后,系统内核就开始准备数据,准备好后将数据从系统内核拷贝到用户内存中,然后系统内核返回结果,用户进程解除阻塞状态。这个时候用户进程调用read将数据从内核拷贝到用户空间。

select模型和阻塞IO的比较

扫描二维码关注公众号,回复: 12160584 查看本文章
  • 相比于阻塞IO(仅调用一个recvfrom),select需要调用select和recvfrom两个系统调用,性能更差;
  • 优势在于可以同时处理多个连接。

因此在处理连接数不是很高的情况下,使用select/epoll不一定优于使用多线程的阻塞IO。值得注意的是,select/epoll的优势并不是对单个连接能处理的更快,而是在于能处理更多的连接

select函数原型

int select(int maxfdp, 
			fd_set * readfds, 
			fd_set * writefds, 
			fd_set * errorfds, 
			struct timeval * timeout);

这里用一张图来解释各个参数的作用

在这里插入图片描述

select实战

server.cpp

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>
#define DEFAULT_PORT 6666

int main(){
    
    
    int serverfd,acceptfd; //监听socket:serverfd,数据传输socket:acceptfd
    struct sockaddr_in my_addr; //本机地址信息
    struct sockaddr_in their_addr; //客户地址信息
    unsigned int sin_size, myport = 6666, lisnum = 10;
    if ((serverfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    
    
        perror("socket");
        return -1;
    }
    printf("socket ok \n");
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(DEFAULT_PORT);
    my_addr.sin_addr.s_addr = INADDR_ANY;
    bzero(&(my_addr.sin_zero), 0);
    if (bind(serverfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) {
    
    
        perror("bind");
        return -2;
    }
    printf("bind ok \n");
    if (listen(serverfd, lisnum) == -1) {
    
    
        perror("listen");
        return -3;
    }
    printf("listen ok \n");
    
    fd_set client_fdset; //监听文件描述符集合
    int maxsock; //监听文件描述符中最大的文件号
    struct timeval tv; //超时返回时间
    int client_sockfd[5]; //存放活动的sockfd
    bzero((void*)client_sockfd, sizeof(client_sockfd));
    int conn_amount = 0; //用来记录描述符数量
    maxsock = serverfd;
    char buffer[1024];
    int ret = 0;
    while (1) {
    
    
        FD_ZERO(&client_fdset); //初始化文件描述符号到集合
        FD_SET(serverfd, &client_fdset); //加入服务器描述符
        tv.tv_sec = 30; //设置超时时间30s
        tv.tv_usec = 0;
        for (int i = 0; i < 5; i++) {
    
     //根据listen参数设置,i要小于5
            if (client_sockfd[i] != 0) {
    
    
                FD_SET(client_sockfd[i], &client_fdset); //将活动的句柄放入文件描述符中
            }
        }
        ret = select(maxsock+1, &client_fdset, nullptr, nullptr, &tv); //select函数,参见前面的图
        if (ret < 0) {
    
    
            perror("select error! \n");
            break;
        }
        else if(ret == 0){
    
    
            printf("timeout!\n");
            continue;
        }
        for (int i = 0; i < conn_amount; i++) {
    
     //轮询各个文件描述符
            if (FD_ISSET(client_sockfd[i], &client_fdset)) {
    
     // FD_ISSET检查client_sockfd是否可读写,大于0则可读写
                printf("start recv from client[%d]:\n",i);
                ret = recv(client_sockfd[i], buffer, 1024, 0);
                if (ret <= 0) {
    
    
                    printf("client[%d] close\n",i);
                    close(client_sockfd[i]);
                    FD_CLR(client_sockfd[i], &client_fdset);
                    client_sockfd[i] = 0;
                }
                else{
    
    
                    printf("recv from client[%d] : %s\n", i, buffer);
                }
            }
        }
        if (FD_ISSET(serverfd, &client_fdset)) {
    
     //检查是否有新连接,若有则加入到client_sockfd中
            struct sockaddr_in client_addr;
            size_t size = sizeof(struct sockaddr_in);
            int sock_client = accept(serverfd, (struct sockaddr*)(&client_addr), (unsigned int*)(&size));
            if (sock_client < 0) {
    
    
                perror("accept error! \n");
                continue;
            }
            if (conn_amount < 5) {
    
     //将连接加入到文件描述符中
                client_sockfd[conn_amount++] = sock_client;
                bzero(buffer, 1024);
                strcpy(buffer, "this is server! welcome!\n");
                send(sock_client, buffer, 1024, 0);
                printf("new connection client[&d] %s:%d\n", conn_amount, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
                bzero(buffer, sizeof(buffer));
                ret = recv(sock_client, buffer, 1024, 0);
                if (ret < 0) {
    
    
                    perror("recv error!\n");
                    close(serverfd);
                    return -1;
                }
                printf("recv : %s\n",buffer);
                if (sock_client > maxsock) {
    
    
                    maxsock = sock_client;
                }
                else{
    
    
                    printf("max connections!!quit!\n");
                    break;
                }
            }
        }
    }
    for (int i = 0; i < 5; i++) {
    
    
        if (client_sockfd[i] != 0) {
    
    
            close(client_sockfd[i]);
        }
    }
    close(serverfd);
    return 0;
}

client.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define DEFAULT_PORT 6666
int main(int argc, char* argv[]){
    
    
    int connfd = 0;
    int cLen = 0;
    struct sockaddr_in client;
//    if (argc < 2) {
    
    
//        printf("Uasge : clientent[server IP address]\n");
//        return -1;
//    }
    client.sin_family = AF_INET;
    client.sin_port = htons(DEFAULT_PORT);
    client.sin_addr.s_addr = inet_addr("这里填写服务器的ip地址");
    connfd = socket(AF_INET, SOCK_STREAM, 0);
    if (connfd < 0) {
    
    
        perror("socket");
        return -1;
    }
    if (connect(connfd, (struct sockaddr*)&client, sizeof(client)) < 0) {
    
    
        perror("connect");
        return -1;
    }
    char buffer[1024];
    bzero(buffer, sizeof(buffer));
    recv(connfd, buffer, 1024, 0);
    printf("recv : %s\n",buffer);
    bzero(buffer, sizeof(buffer));
    strcpy(buffer, "this is client!\n");
    send(connfd, buffer, 1024, 0);
    while (1) {
    
    
        bzero(buffer, sizeof(buffer));
        scanf("%s",buffer);
        int p = strlen(buffer);
        buffer[p] = '\0';
        send(connfd, buffer, 1024, 0);
        printf("i have send buffer\n");
    }
    close(connfd);
    return 0;
}

运行上述代码前请注意客户端的inet_addr()中填写服务器的ip地址,并确保端口号可用。

总结

  • 由于select对socket采用轮询,因此相对效率较低;

  • 单个进程监听端口个数有上限,32位linux系统中为1024个,64位linux系统中为2048个;

  • 其需要从内核空间拷贝数据到用户内存,需要复制大量的句柄,非常消耗资源。

猜你喜欢

转载自blog.csdn.net/zxxkkkk/article/details/111033613
今日推荐