Linux c 多路IO转接服务器(select函数使用)

本文介绍了如何使用select函数来实现一个跨平台的多客户端服务器,详细讲解了select函数的参数和返回值,并提供了相应的错误处理封装。示例代码展示了如何监听连接请求,接受新连接,并处理多个客户端的读写操作。该方法限制了监听文件描述符的最大数量,但避免了过多的线程或进程创建。

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

1、select函数介绍

int select(int nfds, fd_set *readfds, fd_set *writefds,
            fd_set *exceptfds, struct timeval *timeout);
参数:
    nfds 监听所有文件描述符中,最大文件描述符+1
    readfds:读 文件描述符监听集合(这三个参数都是传入传出参数)
    writefds:写 文件描述符监听集合
    exceptfds 异常 文件描述符监听集合
    timeout:
        >0 设置监听超时长
        NULL 阻塞监听
        0 非阻塞监听,轮询

返回值:
    >0 所有监听集合中, 满足对应事件的总数
    0 没有满足监听条件的文件描述符
    -1 设置errno


void FD_ZERO(fd_set* set);  // 清空一个文件描述符集合
    fd_set rset;
    FD_ZERO(rset);

void FD_SET(int fd,fd_set* set);    // 将待监听的文件描述符,添加到监听集合中
    FD_SET(3,&rset);

void FD_CLR(int fd,fd_set* set);    // 将一个文件描述符从监听中移除
    FD_CLR(4,&rset);

int FD_ISSET(int fd,fd_set* set);   // 判断一个文件描述符是否在监听集合中
    FD_ISSET(4,&rset);  
    返回值:在1,不在0

在这里插入图片描述

之前写的多线程和多进程服务器,都是通过,accept进行阻塞监听,来一个客户端(子进程、线程)就给他创建一个通信文件描述符,进行读写
要是客户端太多,就会创建太多线程或进程

在这里插入图片描述

select:先将建立连接的文件描述符添加到集合中,监听select 的读事件,确定是建立连接,就调用accept,此时不会阻塞,将通信的文件描述符添加到数组中,并且也添加到集合中,要是select返回数大于1表示,还有其他读事件,则去一一处理

2、select代码

#include <unistd.h>
#include <sys/wait.h>
#include <strings.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/time.h>
#include <signal.h>
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>

#include "socket_wrap.h"

int main()
{
    int listenfd,connfd,sockfd;
    char buf[BUFSIZ];
    bzero(buf,sizeof(buf));
    struct sockaddr_in clie_addr,serv_addr;
    socklen_t clie_addr_len = sizeof(clie_addr);
    listenfd = Socket(AF_INET,SOCK_STREAM,0);   // 创建套接字
    int opt = 1;
    setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));  // 端口复用函数
    bzero(&serv_addr,sizeof(serv_addr));

    serv_addr.sin_family  = AF_INET;
    serv_addr.sin_port = htons(9999);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    Bind(listenfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));  // 绑定ip port
    Listen(listenfd,128);   // 设置连接上限

    fd_set rset,allset; // all监听的,rset是传出的
    FD_ZERO(&allset);   //  将allset集合全部置零
    FD_SET(listenfd,&allset);      // 将用于连接文件描述符添加到集合中
    int ret,maxfd=listenfd; // maxfd表示当前集合中最大的文件描述符术
    int n,i,j,maxi=-1;      // maxi存储最后一个下标
    char client[FD_SETSIZE],str[INET_ADDRSTRLEN];   // FD_SETSIZE 1024 INET_ADDRSTRLEN 16
    for(i=0;i<FD_SETSIZE;i++)   // 将用于存有时间的文件描述符数组初始化
        client[i] = -1;
    
    while(1)
    {   
        rset = allset;  // rset待会用来做传出参数
        ret = select(maxfd+1,&rset,NULL,NULL,NULL); 
        if(ret<0)  
        {
            perr_exit("select error");
        }

        if(FD_ISSET(listenfd,&rset))    // 表示返回的集合中有listenfd事件,
        {   // 客户端连接请求
            // 此时不需等待,直接建立连接
            connfd = Accept(listenfd,(struct sockaddr*)&clie_addr,&clie_addr_len);    
            printf("客户端:%s:%d\n",inet_ntop(AF_INET,&clie_addr.sin_addr.s_addr,str,sizeof(str)),
            ntohs(clie_addr.sin_port));

            for(i=0;i<FD_SETSIZE;i++)
            {
                if(client[i]<0)
                {
                    client[i] = connfd; // 将用于通信的文件描述符存入数组中
                    break;
                }
            }
            if(i == FD_SETSIZE)     // i超过1024,就会越界
            {
                fputs("too many cliens\n",stderr);
                exit(1);
            }
            if(i>maxi)   // 保证maxi中存储最后一个下标
                maxi = i;

            FD_SET(connfd,&allset); // 将通信文件描述符添加到集合中
            if(maxfd < connfd)  // max中存储最大的文件描述符
                maxfd = connfd;
            if(ret == 1)    // 说明select 只返回了一个并且listenfd
                continue;
        }

        for(i=0;i<=maxi;i++)
        {
            if((sockfd=client[i])<0)
                continue;
            if(FD_ISSET(sockfd,&rset))
            {
                if((n=Read(sockfd,buf,sizeof(buf)))==0)
                {
                    Close(sockfd);
                    FD_CLR(sockfd,&allset);
                    client[i] = -1;
                    printf("客户端:%s:%d已经断开\n",inet_ntop(AF_INET,&clie_addr.sin_addr.s_addr,str,sizeof(str)),
                    ntohs(clie_addr.sin_port));
                }
                else if(n>0)
                {
                    for(j=0;j<n;j++)
                        buf[j] = toupper(buf[j]);
                    Write(STDOUT_FILENO,buf,n);
                    Write(sockfd,buf,n);
                }
                if(--ret == 0)
                    break;
            }
        }
    }
    Close(listenfd);

    return 0;
}

在这里插入图片描述


 缺点:监听上限文件描述符限制,最大1024
    检测满足条件fd,自己添加业务逻辑提高小,提高了编码难度

    优点:
        跨平台

3、socket常用函数出错处理封装

socket_wrap.h

#ifndef __WRAP_H_
#define __WRAP_H_

void perr_exit(const char* s);
int Accept(int fd,struct sockaddr* sa,socklen_t* salenptr);
int Bind(int fd,const struct sockaddr* sa,socklen_t salen);
int Connect(int fd,const struct sockaddr* sa,socklen_t salen);
int Listen(int fd,int backlog);
int Socket(int family,int type,int protocol);
ssize_t Read(int fd,void* ptr,size_t nbytes);
ssize_t Write(int fd,const void* ptr,size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd,void* vptr,size_t n);
ssize_t Writen(int fd,const void* vptr,size_t n);
static ssize_t my_read(int fd,char* ptr);
ssize_t Readline(int fd,void* vptr,size_t maxlen);

#endif // !__WRAP_H_

socket_wrap.c

#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <ctype.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <arpa/inet.h>
#include "socket_wrap.h"

void perr_exit(const char* s)
{
    perror(s);
    exit(1);
}

int Accept(int fd,struct sockaddr* sa,socklen_t* salenptr)
{
    int n;
again:
    if((n=accept(fd,sa,salenptr))<0)
    {
        if(errno == ECONNABORTED || (errno==EINTR))
            goto again;
        else
            perr_exit("accept error");
    }
    return n;
}

int Bind(int fd,const struct sockaddr* sa,socklen_t salen)
{
    int n;
    if((n=bind(fd,sa,salen))<0)
    {
        perr_exit("bind error");
    }
    return n;
}

int Connect(int fd,const struct sockaddr* sa,socklen_t salen)
{
    int n;
    if((n=connect(fd,sa,salen))<0)
        perr_exit("connect error");
    return n;

}

int Listen(int fd,int backlog)
{
    int n;
    if((n=listen(fd,n))<0)
    {
        perr_exit("listen error");
    }
    return n;
}


int Socket(int family,int type,int protocol)
{
    int n;
    if((n=socket(family,type,protocol))<0)
        perr_exit("socket error");
    return n;
}

ssize_t Read(int fd,void* ptr,size_t nbytes)
{
    ssize_t n;
again:
    if((n=read(fd,ptr,nbytes))<0)
    {
        if(errno==EINTR)
            goto again;
        else
            return -1;
    }
    return n;
}

ssize_t Write(int fd,const void* ptr,size_t nbytes)
{
    ssize_t n;
    if((n=write(fd,ptr,nbytes))<0)
        perr_exit("write error");
    return n;
}

int Close(int fd)
{
    int n;
    if((n=close(fd))<0)
        perr_exit("close error");
    return n;
}

ssize_t Readn(int fd,void* vptr,size_t n)
{
    size_t nleft;   // unsigned int 剩余未读取字节数
    ssize_t nread; // int 实际读到的字节数
    char* ptr;
    ptr = vptr;
    nleft = n;
    while (nleft>0)
    {
        if((nread = read(fd,ptr,nleft))<0)
        {
            if(errno == EINTR)        
                nread = 0;
            else
             return -1;
        }
        else if(nread == 0)
            break;
        nleft -= nread;
        ptr+=nread;
    }
    return n-nread;
    
}

ssize_t Writen(int fd,const void* vptr,size_t n)
{
    size_t nleft;
    ssize_t nwritten;
    const char* ptr;
    ptr = vptr;
    nleft = n;
    while(nleft>0)
    {
        if((nwritten=write(fd,ptr,nleft))<=0)
        {
            if(nwritten<0 && errno==EINTR)
                nwritten = 0;
            else
                return -1;
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n;
}

static ssize_t my_read(int fd,char* ptr)
{
    static int read_cnt;
    static char* read_ptr;
    static char read_buf[100];
    if(read_cnt<=0)
    {
    again:
        if((read_cnt=read(fd,read_buf,sizeof(read_buf)))<0)
        {
            if(errno == EINTR)
                goto again;
            return -1;
        }
        else if(read_cnt == 0)
            return 0;
        read_ptr = read_buf;
    }
    read_cnt--;
    *ptr = *read_ptr++;
    return 1;
}

// 传出参数 vptr
ssize_t Readline(int fd,void* vptr,size_t maxlen)
{
    ssize_t n,rc;
    char c,*ptr;
    ptr = vptr;
    for(n=1;n<maxlen;n++)
    {
        if((rc=my_read(fd,&c))==1)
        {
            *ptr++ = c;
            if(c=='\n')
                break;
        }
        else if(rc == 0)
        {
            *ptr = 0;
            return n-1;
        }
        else
            return -1;
    }
    *ptr = 0;
    return n;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讳疾忌医丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值