IO复用之select系统调用

一.IO复用
io复用使得程序能够监听多个文件描述符。
以下情况下需要使用io复用:
1.客户端同时处理多个socket(如:非阻塞的connect技术);
2.客户端程序需要同时处理用户输入和网络连接(如:聊天室程序);
3.TCP服务器要同时处理监听socket和连接socket;
4.服务器要同时处理TCP请求和UDP请求(如:回射服务器);
5.服务器同时监听多个端口,或者处理多种服务(xinetd服务器);
二.select函数
select函数原型如下:

#include<sys/select.h>
int select(int fd,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
//第一个参数:fd,指被监听的文件描述符的总数
//第二个参数:readfds,指向可读事件对应的文件描述符的集合
//第三个参数:writefds,指向可写事件对应的文件描述符的集合
//第四个参数:exceptfds,指向异常事件对应的文件描述符的集合
//第五个参数:timeout,指定select函数的超时时间

(1).通过修改2,3,4参数传入自己感兴趣的wenjianmiaoshuf,select函数调用返回时,内核修改,它们来通知应用程序那些文件描述符已经就绪。
(2).fd_set是一个结构体指针,结构体体内包含一个整形数组,该数组的每一个元素的每一位都会标记一个文件描述符,fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制select能同时处理的文件描述符的总量。FD_ISSET是用来测某一位是否被标记为1.
(3).用以下宏来访问fd_set结构体中的位:

#include<sys/select.h>
FD_SETint (fd,fd_set *fdset);//设置fdset的位fd
FD_CLR(int fd,fd_set *fdset);//清除fdset的位fd
FD_ISSET(int fd,fd_set *fdset);//测试fdset的位fd是否被设置
FD_ZERO(fd_set *fdset);//清除fdset中的所有值

(4).timeout是一个timeval结构类型的指针,timeval结构体的定义如下:

struct timeval
{
    long tv_sec;//秒数
    long tv_usec;//微秒数
}

(5 ). select成功时返回就绪(可读,可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0.select失败时返回-1.
三.文件描述符就绪的条件
以下情况下socket可读:
1.socket内核接收缓存区中的字节数大于或等于其低水位标志SO_RCVLOWAT。
(重要)2.监听socket上有新的连接请求。
3.socket上有未处理的错误。
4.socket通信的双方关闭连接。
以下情况下socket可写:
1.socket内核接收缓存区中的字节数大于或等于其低水位标志SO_SNDLOWAT。
2.socket的写操作关闭。
3.socket使用非阻塞connect连接成功或者失败(超时)之后。
4.socket上有未处理的错误。
四.select的缺点
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024
五.代码实现
服务器端:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<string.h>
#include<sys/time.h>
#include<assert.h>

#define MAX 10
int create_socket()//创建套接字
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);//tcp字节流式服务
    if(sockfd==-1)//等于-1,创建失败
    {
        return -1;
    }

    struct sockaddr_in saddr;//专用的地址结构
    memset(&saddr,0,sizeof(saddr));//将saddr置为0

    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6666);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");

    bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));//命名函数,将saddr的地址分配给未命名的sockfd文件描述符

    listen(sockfd,5);//监听sockfd,5指内核监听的最大长度

    return sockfd;
}

int fds_init(int fds[])//初始化文件描述符集合
{
     int i=0;
     for(;i<MAX;i++)
     {
        fds[i]=-1;//将集合内的值都置为-1
     }
}

void fds_add(int fds[],int fd)//添加文件描述符
{
    int i=0;
    for(;i<MAX;i++)
    {
        if(fds[i]==-1)
        {
            fds[i]=fd;
            break;
        }
    }
}

void fds_del(int fds[],int fd)//删除文件描述符
{
    int i=0;
    for(;i<MAX;i++)
    {
        if(fds[i]==fd)
        {
            fds[i]=-1;
            break;
        }
    }

}
int main()
{
        int sockfd=create_socket();//创建sockfd

        int fds[MAX];//设置一个数组fds

        fds_init(fds);//初始化数组fds
        fds_add(fds,sockfd);//将socffd添加到数组中

        fd_set fdset;
        while(1)
        {
            FD_ZERO(&fdset);//清除fdset的所有位

            int maxfd=-1;
            int i=0;

            for(;i<MAX;i++)//循环遍历找到最大的文件描述符
            {
                if(fds[i]!=-1)//遍历找到fds数组中不是初始值-1的位
                {
                    FD_SET(fds[i],&fdset);//设置fdset的fds[i]位
                    if(maxfd<fds[i])
                    {
                        maxfd=fds[i];
                    }
                }
            }

            struct timeval tv={5,0};//设置超时时间为5秒
            int n=select(maxfd+1,&fdset,NULL,NULL,&tv);//用n来接收select成功时返回就绪文件描述符的总数
            if(n==-1)//返回失败
            {
                perror("select error");
                break;
            }
            else if(n==0)//超时时间内没有文件描述符就绪
            {
                printf("timeout\n");
                continue;
            }
            else
            {
                int i=0;
                for(;i<MAX;i++)//循环遍历
                {
                    if(fds[i]==-1)
                    {
                        continue;
                    }
                    if(FD_ISSET(fds[i],&fdset))//fdset的fds[i]位已经被测试
                    {
                        if(fds[i]==sockfd)
                        {
                            struct sockaddr_in caddr;
                            int len=sizeof(caddr);
                            int c=accept(sockfd,(struct sockaddr*)&caddr,&len);//接收一个套接字已建立的连接
                            if(c<0)
                            {
                                continue;
                            }

                            printf("accept :%d\n",c);
                            fds_add(fds,c);
                        }   
                        else
                        {
                            char  buffer[128]={0};
                            if(recv(fds[i],buffer,127,0)<=0)//接收服务器端的数据是小于等于零,说明客户端已经关闭
                            {
                                close(fds[i]);  
                                fds_del(fds,fds[i]);//清除fds数组
                                printf("一个客户端关闭\n");
                                continue;
                            }

                            printf("buffer %d  =%s\n",fds[i],buffer);
                            send(fds[i],"ok",2,0);
                        }
                    }
                }
            }
        }
}

客户端:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建套接字(tcp流式服务)
    assert(sockfd != -1);//断言创建不成功

    struct sockaddr_in saddr;//专用的地址结构
    memset(&saddr,0,sizeof(saddr));//将saddr置为0
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6666);//端口要与服务器端口一致
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//本地ip

    int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//connect触发tcp三次握手发起连接
    //connect成功时返回0
    assert(res != -1);

    while(1)//循环传输
    {
        char buff[128] = {0};
        printf("input:\n");
        fgets(buff,128,stdin);//从键盘接收数据

        if(strncmp(buff,"end",3) == 0)//如果接收到的是end,就跳出循环,close sockfd
        {
            break;
        }
        send(sockfd,buff,strlen(buff),0);//向已连接的sockfd发送数据

        memset(buff,0,128);//将数组buff的128位置为0
        recv(sockfd,buff,127,0);//从服务器端接收数据
        printf("buff = %s\n",buff);//打印数据
    }
    close(sockfd);//close sockfd
}

六.结果展示
这里写图片描述

猜你喜欢

转载自blog.csdn.net/qq_39110766/article/details/80041451