select 实现类似多线程_linux进程通信--socket套接字(四)--多路IO转实现一个server对应多个client...

先给自己打个广告,本人的微信公众号正式上线了,搜索:张笑生的地盘,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题

de34ffbdd62abed964a39acfbd4162ed.png

一 why

在前面的文章《linux进程间通信—本地socket套接字(三)—多线程实现一个server对应多个client》以及《linux进程间通信—本地socket套接字(二)—多进程程实现一个server对应多个client》中,我们采取的方式都是在server端的应用程序中,调用accept函数阻塞等待客户端的连接。

这么做的缺点是什么呢?我们知道accept函数会发生阻塞,server应用程序效率不高。如果server进程需要做其他工作的话,在这里阻塞等待显然不是一个合适的方式。当然我们在server中,可以采用多线程避免这种情况的发生,将server端的其他工作分门别类,分配给其他的线程去处理;但是有没有别的方式是避免accept函数的阻塞等待造成的效率问题呢?

答案是显然的,这就是我们这篇文章将要介绍的多路IO转接实现的服务器。

二 what

何为多路IO转接?在linux中,有一条准则,是一切皆文件。socket通信也是利用文件的读写来实现,那么我们就可以检测文件是否发生读写事件,来判断是否有socket通信。
应用程序调用select函数来通知内核来实现检测文件读、写、异常事件,示意图如下:

e2e4d4ffd90f19401bcea13532d935b8.png

select函数原型如下

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
参数1: 所监听的所有文件描述符中,最大的文件描述符+1
参数2: 所监听的文件描述符"可读"事件,readfds
参数3: 所监听的文件描述符"可写"事件,writefds
参数4: 所监听的文件描述符"异常"事件,
参数5: 超时
返回值: 成功: 所监听的集合中,满足条件的总数;出错:-1,并设置errno

既然select函数可以分别检测对应文件描述符的读(readfds)、写(writefds)、异常(exceptfds)事件,那么我们就需要首先设置select监测哪些文件描述符的读(readfds)、写(writefds)、异常(exceptfds)事件,linux提供的相关API为:

void FD_ZERO(fd_set *set);
    将set清空为0
void FD_CLR(int fd, fd_set *set);
    将fd从set集合中清除出去,
int  FD_ISSET(int fd, fd_set *set);
    判断fd是否在集合中
void FD_SET(int fd, fd_set *set);
    将fd设置到set集合中去

FD_SET是设置要监听的文件描述符是哪些,以位图形式表示,假设我们需要监听文件描述符分别是lfd,fd1,fd2,fd3,FD_SET设置之后的readfds如下

fbbd02dd0442706e37bb92023606d894.png

select要监听readfds的文件描述符状态,如果lfd和fd3的读事件发生,此时readfds的文件描述符状态如下:

d8e6dac5b30bc902ac4ce5a86a00cee7.png

程序步骤如下

1. 初始化 readfds 集合
    fd_set readfds
    FD_ZERO(&readfds);
2. 将要监听的文件描述符添加到集合中
    FD_SET(fd1, &readfds);
    FD_SET(fd2, &readfds);
    FD_SET(fd3, &readfds);
3. select开始监听对应文件描述符的事件
    select()     ------->  满足条件的总数
4. 判断文件描述符所属事件类型
    for() {
        FD_ISSET(fd1, &readfds)    ----->  返回为1,表示满足
    }

三 how

根据上面的设置步骤,先上server代码,然后根据代码分析流程

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/shm.h>

#define PORT  8890
#define QUEUE_SIZE   10
#define BUFFER_SIZE 1024

int main(int argc, char **argv)
{
    struct sockaddr_in server_sockaddr, client_addr;
    socklen_t length = sizeof(client_addr);
    char str[INET_ADDRSTRLEN];
    pthread_t pid;
    int listenfd, client[FD_SETSIZE], sockfd;
    int maxfd, maxi, connfd;
    int ret = 0, nready;
    int reuse = 1;
    fd_set rset, allset;
    char buf[BUFFER_SIZE] = {0};
    int recvlen, i;

    //定义IPV4的TCP连接的套接字描述符
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        perror("socket() fail!n");
        return -1;
    }

    //使能可以重新使用addr
    ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
        &reuse, sizeof(reuse));
    if (ret < 0) {
        perror("setsockopt error");
        return -1;
    }
    //定义sockaddr_in
    memset(&server_sockaddr, 0, sizeof(server_sockaddr));
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_sockaddr.sin_port = htons(PORT);

    //bind成功返回0,出错返回-1
    ret = bind(listenfd, (struct sockaddr *)&server_sockaddr,
        sizeof(server_sockaddr));
    if(ret < 0) {
        perror("bind");
        return -1;//1为异常退出
    }
    printf("bind success.n");

    //listen成功返回0,出错返回-1,允许同时帧听的连接数为QUEUE_SIZE
    ret = listen(listenfd, QUEUE_SIZE);
    if(ret < 0) {
        perror("listen");
        return -1;
    }
    printf("listen success.n");

    maxfd = listenfd;
    maxi = -1;
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    while(1) {
        rset = allset;
        nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
        if (nready < 0) {
            perror("select error!");
            return -1;
        }

        if (FD_ISSET(listenfd, &rset)) { //监听是否有连接请求
            connfd = accept(listenfd, (struct sockaddr*)&client_addr,&length);
            if(connfd < 0) {
                perror("connect error!");
                return -1;
            }
            printf("new client accepted, ip : %s, port : %d.n",
                inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
                ntohs(client_addr.sin_port));

            for (i = 0; i < FD_SETSIZE; i++) {
                if (client[i] < 0) {
                    client[i] = connfd;
                    break;
                }
            }

            if (i >= FD_SETSIZE) {
                printf("too many clientn");
                return -1;
            }

            FD_SET(connfd, &allset);
            if (connfd > maxfd)
                maxfd = connfd;

            if (i > maxi)
                maxi = i;

            if (--nready == 0)
                continue;
        }

        for (i = 0; i <= maxi; i++) {  //监听已经连接的客户端是否有数据发送过来
            if ((sockfd = client[i]) < 0)
                continue;

            if (FD_ISSET(sockfd, &rset)) {
                memset(buf, 0, sizeof(buf));
                recvlen = read(sockfd, buf, sizeof(buf));
                if (recvlen ==0) {
                    printf("client[%d] exitn", client[i]);
                    close(sockfd);
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;
                } else {
                    printf("now server recv : %sn", buf);
                    write(sockfd, buf, recvlen);
                }
            }

            if (--nready <= 0)
                 continue;
        }
    }

    printf("closed.n");
    close(listenfd);
    return 0;
}

client代码我们还是采用之前的代码,如下

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

#define PORT  8890
#define BUFFER_SIZE 1024
#define LOCAL_LOOP_BACK_IP "127.0.0.1"

int main(int argc, char **argv)
{
    struct sockaddr_in servaddr;
    char sendbuf[BUFFER_SIZE] = {0};
    char recvbuf[BUFFER_SIZE] = {0};
    int client_fd;

    //定义IPV4的TCP连接的套接字描述符
    client_fd = socket(AF_INET,SOCK_STREAM, 0);

    //set sockaddr_in
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr(LOCAL_LOOP_BACK_IP);
    servaddr.sin_port = htons(PORT);  //服务器端口

    //连接服务器,成功返回0,错误返回-1
    if (connect(client_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        perror("connect");
        exit(1);
    }
    printf("connect server(IP:%s).n",LOCAL_LOOP_BACK_IP);

    //客户端将控制台输入的信息发送给服务器端,服务器原样返回信息
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        send(client_fd, sendbuf, strlen(sendbuf),0); ///发送
        if(strcmp(sendbuf,"exitn")==0)
        {
            printf("client exited.n");
            break;
        }
        recv(client_fd, recvbuf, sizeof(recvbuf),0); ///接收
        printf("client receive: %sn", recvbuf);

        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }

    close(client_fd);
    return 0;
}

(1)我们将各个文件描述符状态以及一些参数标示如下,初始时,我们要一直监听连接事件,监听连接事件是通过server_listen_fd来实现的,所以初始状态如下:

85e3277f02b4e54abeb2185673b221f5.png

(2)假设此时只有一个client连接事件请求,如下,此时监听到一个连接,会将连接到的文件描述符加入进来,以监听将来是否发生读写数据事件

31909aee5d077c7b246081f1d4e7b4f1.png

(3)假设此时有fd2,fd3,fd4都发生连接事件,如下。因为都检测到了连接事件,所以要将这些事件描述符添加进来,以便监测将来可能会发生的读写数据请求

5b7f173534c18c0066fcde7154e14d56.png

(4)假设此时fd2,fd4发生了数据通信(对客户端而言是发送数据,对服务器端是接收数据,请注意,我们这里的fd1,fd2,fd3,fd4都是server端的文件描述符,因此它们都表示读数据事件发生了),并且同时有fd5的连接请求

a994a6168ca13189e815f498b40a67f6.png

四 Else

有些读者拿到上面的示例代码,回去验证后,发现server还是阻塞等待,这是因为在select函数中,我们没有设置超时等待机制,默认还是阻塞的方式。实际上select函数是可以设置为超时等待的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SelectSocket编程中还是比较重要的,可是对于初学Socket的人来说都不太爱用Select写程序,他们只是习惯写诸如connect、accept、recv或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。下面详细介绍一下! Select的函数格式(我所说的是Unix系统下的伯克利socket编程,和windows下的有区别,一会儿说明): int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout); 先说明两个结构体: 第一,struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作,比如清空集合FD_ZERO(fd_set *),将一个给定的文件描述符加入集合之中FD_SET(int ,fd_set *),将一个给定的文件描述符从集合中删除FD_CLR(int ,fd_set*),检查集合中指定的文件描述符是否可以读写FD_ISSET(int ,fd_set* )。一会儿举例说明。 第二,struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。 具体解释select的参数: int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。 fd_set *readfds是指向fd_set结构的指针,这个集合中应该括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。 fd_set *writefds是指向fd_set结构的指针,这个集合中应该括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。 fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常。 struct timeval* timeoutselect的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即selecttimeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回返回值同上述。 返回值: 负值:select错误 正值:某些文件可读写或出错 0:等待超时,没有可读写或错误的文件

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值