能进行一对多连接的服务器(1994年Linux 1.0支持select)

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
 
#define PORT 39002
#define MAX_FD_NUM 3
#define BUF_SIZE 512
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)
 
int main()
{
    //创建套接字
    int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (m_sockfd < 0)
    {
        ERR_EXIT("create socket fail");
    }
 
    //初始化socket元素
    struct sockaddr_in server_addr;
    int server_len = sizeof(server_addr);
    memset(&server_addr, 0, server_len);
 
    server_addr.sin_family = AF_INET;
    //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用这个写法也可以
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
 
    //绑定文件描述符和服务器的ip和端口号
    int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len);
    if (m_bindfd < 0)
    {
        ERR_EXIT("bind ip and port fail");
    }
 
    //进入监听状态,等待用户发起请求
    int m_listenfd = listen(m_sockfd, MAX_FD_NUM);
    if (m_listenfd < 0)
    {
        ERR_EXIT("listen client fail");
    }
 
    //定义客户端的套接字,这里返回一个新的套接字,后面通信时,就用这个m_connfd进行通信
    //struct sockaddr_in client_addr;
    //socklen_t client_len = sizeof(client_addr);
    //int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
 
    printf("client accept success\n");
 
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
 
    //接收客户端数据,并相应
    char buffer[BUF_SIZE];
    int array_fd[MAX_FD_NUM];
    //客户端连接数量
    int client_count = 0;

    fd_set tmpfd;
    int max_fd = m_sockfd;
    struct timeval timeout;
 
    for (int i = 0; i < MAX_FD_NUM; i++)
    {
        array_fd[i] = -1;
    }
    //array_fd[0] = m_sockfd;
 
    while (1)
    {
        FD_ZERO(&tmpfd);
        FD_SET(m_sockfd, &tmpfd);   // m_sockfd是服务器的socket,也加入到fd_set中,后面交给select进行监听。
        int i;
 
        //所有在线的客户端加入到fd中,并找出最大的socket
        for (i = 0; i < MAX_FD_NUM; i++)
        {
            if (array_fd[i] > 0)
            {
                FD_SET(array_fd[i], &tmpfd); //set array_fd in red_set
                if (max_fd < array_fd[i])
                {
                    max_fd = array_fd[i]; //get max_fd
                }
            }
        }
 
        int ret = select(max_fd + 1, &tmpfd, NULL, NULL, NULL);
        if (ret < 0)
        {
            ERR_EXIT("select fail");
        }
        else if (ret == 0)
        {
            //ERR_EXIT("select timeout"); //超时不是错误,不可断掉连接
            printf("select timeout\n");
            continue;
        }
 
        //表示有客户端连接
        if (FD_ISSET(m_sockfd, &tmpfd))
        {
            int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
            if (m_connfd < 0)
            {
                ERR_EXIT("server accept fail");
            }
 
            //客户端连接数已满
            if (client_count >= MAX_FD_NUM)
            {
                printf("max connections arrive!!!\n");
                // char buff[]="max connections arrive!!!";
                // send(m_connfd, buff, sizeof(buff) - 1, 0);
                close(m_connfd);
                continue;
            }
 
            //客户端数量加1
            client_count++;
            printf("we got a new connection, client_socket=%d, client_count=%d, ip=%s, port=%d\n", m_connfd, client_count, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
 
            for (i = 0; i < MAX_FD_NUM; i++)
            {
                if (array_fd[i] == -1)
                {
                    array_fd[i] = m_connfd;
                    break;
                }
            }
        }
 
        //遍历所有的客户端连接,找到发送数据的那个客户端描述符
        for (i = 0; i < MAX_FD_NUM; i++)
        {
            if (array_fd[i] < 0)
            {
                continue;
            }
            //有客户端发送过来的数据
            else
            {
                if (FD_ISSET(array_fd[i], &tmpfd))
                {
                    memset(buffer, 0, sizeof(buffer)); //重置缓冲区
                    int recv_len = recv(array_fd[i], buffer, sizeof(buffer) - 1, 0);
                    if (recv_len < 0)
                    {
                        ERR_EXIT("recv data fail");
                    }
                    //客户端断开连接
                    else if (recv_len == 0)
                    {
                        client_count--;
                        //打印断开的客户端数据
                        printf("client_socket=[%d] close, client_count=[%d], ip=%s, port=%d\n\n", array_fd[i], client_count, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
                        close(array_fd[i]);
                        FD_CLR(array_fd[i], &tmpfd);
                        array_fd[i] = -1;
                    }
                    else
                    {
                        printf("server recv:%s\n", buffer);
                        strcat(buffer, "+ACK");
                        send(array_fd[i], buffer, sizeof(buffer) - 1, 0);
                    }
                }
            }
        }
    }
 
    //关闭套接字
    close(m_sockfd);
 
    printf("server socket closed!!!\n");
 
    return 0;
}

上面也是一个简单的echo服务器,它通过select实现I/O复用。下面我简单介绍这个过程的思想(如果你在读这篇文章时觉得写的不清楚,欢迎留言或私信交流)。

首先,要进行I/O多路复用,则意味着一个服务器可以同时连接多个客户端,那么就需要用数组来记录这些客户端,上面的代码中通过int array_fd[MAX_FD_NUM];来记录。每当服务器用accept接受一个用户连接,就将accept返回这个客户端连接的文件描述符m_connfd并放入到array_fd中。

但是accept、recv、send函数都是阻塞的,如果没有用户发送连接请求或者发送数据,那整个服务器照样会被阻塞,无法实现一对多的连接。这个时候select就发挥作用了。

select能够检测到句柄上是否有事件发生(比如客户连接请求、客户的数据到达服务器)。这样,程序就只需要加上一个判断语句,根据select的结果来决定是否调用accept或者recv。

那么select是怎么做的呢?select有一个配套的数据结构fd_set,它里面定义了一个数组可以记录文件描述符。服务器中通过FD_SET(m_sockfd, &tmpfd)tmpfd中的第m_sockfd(它对应服务器对外开放的套接字)标记为感兴趣的文件描述符(将对应的位置1)。如果没有新的连接请求,select会将m_sockfdtmpfd中的记号置为0。select的思想是这样的:遍历tmpfd中被标记为感兴趣的文件描述符,遍历完后,把有事件产生的文件描述符在tmpfd中的值保持为1,否则置为0。

所以tempfd在被select函数处理前后表示的意义是不一样的,在传入前,表示的是需要检查的感兴趣的文件描述符集合,在处理后,表示的是感兴趣的文件描述符集合中有事件发生的文件描述符。

正是由于这个过程中会改变tempfd表示的感兴趣的文件描述符集合,所以在程序中创建了数组array_fd来表示感兴趣的文件描述符集合。然后每次select后,通过array_fd重新设定tempfd上感兴趣的文件描述符集合。

由于服务器需要频繁的监听是否有新的用户连接请求,所以每次循环都会执行FD_SET(m_sockfd, &tmpfd);,表示对m_sockfd这个文件描述符感兴趣。

另外,服务器还需要知道是否收到客户端发送的数据,所以在建立服务器-客户端连接后,还需要通过FD_SET(m_connfd, &tmpfd);将用户连接加入到感兴趣的文件描述符集合中。

每当通过select后,tempfd变为感兴趣的文件描述符集合中有事件发生的文件描述符集合。再通过一个循环函数遍历tempfd,根据FD_ISSET(array_fd[i], &tmpfd)找到有事件发生的文件描述符。并对这个文件描述符执行recv函数,这个时候,recv函数可以直接获得收到的数据,因而不会被阻塞。

需要注意的是,服务器对外开放的文件描述符sock_fd并不在array_fd中,每次单独的通过FD_SET(m_sockfd, &tmpfd);把在tempfd上的标记置为1。再单独的通过FD_ISSET(m_sockfd, &tmpfd)检查这个套接字上是否有新的连接请求事件。如果有,则由accept产生客户端对应的文件描述符,并加入到感兴趣的文件描述符集合array_fd中。此时由于客户端连接请求已经存在,所以accept函数不会被阻塞。

这个服务器程序可以同时处理多个用户连接而不会被阻塞,我觉得它最重要的思想是:不直接去执行会导致阻塞的函数(accept、recv、send),而是在执行前先进行判断,如果有对应的事件发生才去执行阻塞函数,否则继续循环监听。这种方式称为事件驱动

为什么说这种方式实现了I/O多路复用呢?在网上找了一段解释:

IO 多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;
多路是指网络连接,复用指的是同一个线程

这样实现方式存在一些缺点:

  • 首先是fd_set中定义的数组大小是由FD_SETSIZE决定的,最大为1024,所以select的实现方式由连接数量的限制。

  • select会将感兴趣的文件描述符标记集tempfd中有事件发生的文件描述符标记为1,但是并不能直接告诉程序到底是哪些文件描述符上有事件发生,实现时还得通过FD_ISSET函数一个个判断tempfd中的标记,时间复杂度为O(N)

  • select每次处理完tempfd后会改变tempfd中设定的感兴趣的文件描述符标记,程序还得创建一个array_fd来保存这些感兴趣的文件描述符集合,在select之后再通过for循环依次根据array_fd重新设置tempfd,操作繁琐,接口很不友好(不仔细琢磨不容易理解这个设计,差点被select函数挡在了网络编程的门外)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值