I/O复用之select服务器

本文通过具体代码实例解析了select的工作原理及应用,包括如何利用select进行I/O多路复用、处理客户端连接请求以及数据交互等关键操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  学习了select之后,也有好一段时间了,但是一直没有提起写一篇关于select的博客,大概也是因为自己那会还没搞懂吧,这段时间在看《linux高性能服务器编程》时,又看到I/O复用对于select,poll,epoll的用法实例和比较,又从头看了一次之前写的代码,虽然是在老师的指导下写的,但是印象还是很深刻的,现在看来,当时一头雾水的写,顶多记得一个写代码的步骤,而并没有明白为什么和原理,甚至当初还因为文件描述符集绕了半天。
  如果你只是想看看代码,github链接在这:https://github.com/NICK-DUAN/Three-U/tree/master/select_server
  如果一味的讲解原理的话,我自己也会睡着,所以我选择从代码入手,原理其实都隐藏在代码里,一行行的看代码,看到疑惑或者不懂的地方再去看看书,搜搜资料,才会发现自己的想法究竟是贴合了大牛的思想还是根本很愚蠢,一味的看原理就像是“站在岸上学不会游泳”,而一味的敲代码那不就是一个码农了吗,有何意义。
  下面是我的代码,我会从头到尾,详细解释代码,和对应的原理:
  

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <time.h>

int fds[1024];

为什么要定义一个全局变量的fds数组?这是因为需要在select里面的文件描述符集中与之对应,将外界有效的文件描述符(套接字)set进select的文件描述符集中,相当于select的一份保存。这个解释也不是很好,往下看或许你就明白了

static void usage(const char* str)
{
    printf("Please Enter# ");
    printf("%s [local-ip]--[local-port]\n",str);
}

int startup(const char* _ip,const char* _port)
{
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        exit(2);
    }

AF_INET代表协议族,SOCK_STREAM表示以字节流传递数据,即代表使用TCP协议,最后一个参数代表协议类型,但因为前两个参数已经确定了一个协议,所以默认为0。

    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(atoi(_port));
    local.sin_addr.s_addr=inet_addr(_ip);
    if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
    {
        perror("bind");
        exit(3);
    }
    if(listen(sock,5)<0)
    {
        perror("listen");
        exit(4);
    }

listen的第二个参数代表最大完全连接的数量参数的值,但是最大的半连接的数量则由内核决定。
我自己的理解是一次只能同时处理backlog个连接,再多的话你先等着。
PS:最大完全连接数为:backlog+1,因为计算机的下标从0开始。backlog表示listen的第二个参数

    return sock;
}
int main(int argc,char *argv[])
{
    if(argc!=3)
    {   
        usage(argv[0]);
        exit(1);
    }
    int listen_sock = startup(argv[1],argv[2]);

    int i=1;
    int maxfd=-1;

maxfd表示当前最大的有效socket,在遍历整个socket数组时,只需要遍历到这个位置就ok了。
后续的socket因为是无效的socket,所以不用进行判断

    int nums=sizeof(fds)/sizeof(fds[0]);
    for(i=1;i<nums;i++)
    {
        fds[i]=-1;        
    }
    fds[0]=listen_sock;

将全局数组的所有元素全部初始化为-1,便于在后续判断中不出错,并且把listen_sock(监听套接字)放入数组的第一个位置中,因为监听套接字已经处理号,所以后续并不需要对监听套接字再做什么别的操作。

while(1)
{
    struct timeval time={3,0};
    fd_set readfds;//读 文件描述符集
    FD_ZERO(&readfds);//将"读"文件描述符集中的所有文件描述符设为0
    maxfd=-1;

更新文件描述符集的最大值,防止多出来的被无效访问。
例如:第一次while循环,maxfd=1000,此时需要遍历到1000,
第二次时,maxfd=10,表示10后面的socket全部无效,没有读写事件,若不重新设定的话,就浪费时间。

    for(i=1;i<nums;i++)//循环检测fds中的文件描述符中的文件描述符,如果有效,则设置入"读"
    {
        if(fds[i]>0)//如果有效,则设置入"读"文件描述符集中
        {
            FD_SET(fds[i],&readfds);
            if(maxfd<fds[i])//如果存在比maxfd大的文件描述符,则更新maxfd
            {
               maxfd=fds[i]; 
            }
        }
    }

遍历所有的fds[i],每个fd[i]表示一个socket,因为我们已经把所有的文件描述符全部初始化为-1,所以如果当前的文件描述符大于0,则必定是监听套接字或者建立号的连接。

    switch(select(maxfd+1,&readfds,NULL,NULL,&time))//用switch判断select的等待结果

select的第一个参数为最大有效的maxfd+1,原理也是因为计算机的下表从0开始,所以多一个。第二个参数是readfd读文件描述符集(我们只讨论读事件),第三个参数为写文件描述符集,第四个参数为错误文件描述符集,第五个参数表示等待时间,设置时间表示当超过等待时间后,会返回timeout表示等待时间已到,设置为NULL表示阻塞式,设置为0时,表示立即返回。


        {
            case -1:
                perror("select");
                break;

表示select出错

            case  0:
                printf("select timeout,waitting...\n");
                break;

表示等待的time时间内没有一个有效的socket

            default:
            {
                for(i=0;i<nums;i++)//从头到尾遍历fds
                { 
                    if(fds[i]<0)
                    {
                        continue;
                    }  

小于0的,表示无效,继续遍历下一个,直到遍历到有效的文件描述符,才进行操作


                    if(i==0&&FD_ISSET(listen_sock,&readfds))

listen_sock就绪,tcp三次握手完成,此时可以开始进行连接
FD_ISSET表示判断第一个参数是否在第二个参数的集合中

                    {
                        struct sockaddr_in client;
                        socklen_t len=sizeof(client);
                        int new_fd=accept(listen_sock,(struct sockaddr*)&client,&len);//接收到的文件描述符new_fd
                        if(new_fd<0)
                        {
                            perror("accept");
                            continue;
                        }

使用accept函数将得到一个连接的socket,再fds数组中找到一个可用的位置,因为有效的socket必大于0,所以原先初始化的数组中为-1的则表示可用的位置,插入此socket,等待下次循环时,进入对连接请求处理的模块


                        //运行到此处,说明已经有一个client连接
                        printf("client[%s]--[%d]# ",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
                        fflush(stdout);
                        int j=1;
                        for(j=1;j<nums;j++)
                        {
                            if(fds[j]==-1)//找到一个可用的文件描述符
                            {
                                break;
                            }
                        }
                        if(j==nums)

文件描述符集已满,没有可用的文件描述符,关闭new_fd,即关闭此client的链接

                         { 
                             printf("fd_set full...");
                             fflush(stdout);
                             sleep(2);
                             printf("connect broken...\n");
                             close(new_fd);
                         }

到此处,有两种情况:
1.从for循环中跳出,表示找到一个可用的位置,也表示当前的服务器还有资源再处理一个连接的请求
2.或因为j==nums,表示资源全部分配,没有“余力”处理其他连接,拒绝接收此连接,不过这种已经返回,不会进入else逻辑。

                        else
                        {
                            fds[j]=new_fd;

将链接此client的文件描述符设置到全局的文件描述符集中,在下一次循环进入后,进行操作

                        }
                    }
                    else if(i>0&&FD_ISSET(fds[i],&readfds))

除listen_sock就绪外的其他文件描述符就绪

                    {
                        char buff[1024];
                        ssize_t s=read(fds[i],buff,sizeof(buff)-1);

从标准输入(键盘)接收数据

                        if(s>0)
                        {
                            buff[s]=0;
                            printf("client say# %s",buff);
                            printf("Send to client# ");
                            fflush(stdout);
                            ssize_t _s=read(0,buff,sizeof(buff)-1);
                            if(_s>0)
                            {
                                write(fds[i],buff,strlen(buff));
                            }
                            else{
                            }
                        }
                        else if(s==0)

s==0表示客户端断开连接

                        {
                            printf("client quit...\n");
                            FD_CLR(fds[i],&readfds);
                            close(fds[i]);
                            fds[i]=-1;
                        }
                        else
                        {
                            perror("read");
                        }
                    }else{ 
                    }
                 }
            }
            break;
        }
    }
}

  至此,关于select的代码已经写完,建议读者在看这篇博客时自己了解过select的架构,因为我并没有很详细的分析select服务器,只是从代码层分析,希望在看完之后,自己动手敲一遍代码,然后对不熟悉的地方或查手册,或从网上找点针对性的博客看看。


                我是分割线


上次忘记说在select中何种情况下算作可读,何种情况下算作可写:

下列情况下socket可读:

1,socket内核接收缓冲区中的字节数大于或等于其低水位标记SO_RCVLOWAT,此时可读;
2,socket通信的对方关闭,会返回0;
3,监听socket上有新的连接到来;
4,socket上有未处理的错误,此时可以用getsockopt来读取和清除该错误;

下列情况下socket剋可写:

1,socket内核发送缓冲区中的字节数大于或等于其低水位标记SO_SNDLOWAT,此时可写;
2,socket的写操作被关闭,对写操作被关闭的socket执行写操作将会返回SIGPIPE;
3,socket使用非阻塞connect连接成功或失败之后;
4,socket上有未处理的错误,此时可以用getsockopt来读取和清除该错误。

  在select中还可以接收带外数据(即紧急数据),这个带外数据是TCP标志位中URG有效时的数据,而不是PSH有效时的数据,如果还是分不清,可以看看这篇博客:http://blog.youkuaiyun.com/sinat_36118270/article/details/73927628
  并且,URG的带外数据有点奇怪,并不是想象中的一串数据,而是偏移量的最后一个数据,假如TCP报头中的URG标志位有效,则必然紧急指针的位置会有一个有效的地址偏移量,然后接收方根据紧急指针解读出带外数据,如下图的,往TCP的缓冲区中写入N个普通数据后,又紧接着写了三个带外数据“a,b,c”,系统将紧急指针指向带外数据的最后一个字节的下一位置,此时在接收端只有c被解读为带外数据,其他数据都被解释为普通数据,所以,不难发现,带外数据一次只能发一个。这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值