【Linux】多路IO复用技术②——poll详解&如何使用poll模型在本地主机实现简易的一对多服务器(附图解与代码实现)

在阅读本篇博客之前,建议大家先去看一下我之前写的这篇博客,否则你很可能会一头雾水

【Linux】多路IO复用技术①------select详解&如何使用select模型在本地主机实现简易的一对多服务器(附图解与代码实现)http://t.csdnimg.cn/kPjvk

如果你看完了上面这篇博客,或者对select的原理和网络通信方面有一定了解的话,就可以开始阅读下面的内容了。那么废话不多说,我们正式开始了。

这篇博客为大家讲解的是多路IO复用技术②------poll

PS:由于poll模型的原理和select模型的实现原理基本一致,并且select的原理已经在上述的博客中详细讲过,所以这里不再浪费篇幅再讲一遍了

首先,我们先来了解一下poll模型的优缺点

poll模型的优缺点

优点:

  1. 在内网场景下,poll也算是不错的IO复用模式

  2. 对监听集合进行了传入传出分离设置,不需要用户再自己设置传入集合(监听集合)和传出集合(就绪集合)

  3. 相比select,poll可以监听的事件种类更加丰富(具体可见下面的博客接口部分------struct pollfd)

  4. 可以为不同的套接字设置不同的监听事件,不像select模型只能批量设置监听事件

  5. poll模型可以监听的socket数量不受1024的硬限制,允许用户自定义数组作为监听集合,数组想设置多大就设置多大(其实这也不能完全算优点,下面讲缺点时会讲为什么)
    缺点:

  6. poll模型的兼容性极差,甚至部分linux系统都不认识poll模型,更别说windows系统了

  7. 随着select的持续使用,会产生大量的拷贝开销和挂载开销(原因和select模型一样)

  8. 与select模型一样,poll的监听也是通过一次次的遍历实现的,非常消耗CPU,会导致服务器吞吐能力会非常差。更可怕的是,select遍历的大小仅为1024,而poll模型遍历的大小是由用户决定的,如果用户设置的监听集合大小为100000,就意味着poll遍历的大小就是100000,服务器很可能会直接瘫痪

在了解了poll模型的优缺点后,我们来了解一下poll模型的相关函数

poll模型的相关接口

以下接口的头文件都是 #include <poll.h>

监听结构体struct pollfd

先来介绍一下poll模型中的监听集合是什么样的

poll中的监听集合是一个结构体数组,这里我们将变量名设为listen_array[size],写法如下所示:

cpp 复制代码
#define SIZE 10000

struct pollfd listen_array[SIZE];//用户自定义的监听集合

pollfd结构体中的成员
struct pollfd
{
    int fd; //目标套接字的文件描述符,取消监听就设为-1
    short events; //想要监听什么事件
    short revetns;
};

我们来讲解一下revents这个成员的作用

当套接字就绪时触发某相关事件时,系统会将其设置为对应事件的宏定义,用户可以使用该成员判断套接字是否就绪

比如套接字中有数据来了,需要读取处理,触发读事件,系统就会自动将revents设置为读事件对应的常量POLLIN,用户就可以去通过判断revents是不是等于POLLIN来判断是否读事件就绪

events 和 revents 可取的值及对应事件如下图所示:

监听函数poll

先来介绍一下一会会用到的参数:

  • #define SIZE 10000; //监听集合的大小
  • struct pollfd listen_array[SIZE]; //自定义监听集合
  • nfds_t nfds; //最大监听套接字的数量,一般传监听数组的大小
  • int timeout; //工作模式

我们来介绍一下这个timeout,timeout有以下几种设置方式:

  1. timeout = -1:poll 调用后阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
  2. timeout = 0:poll 调用后非阻塞等待,无论被监视的文件描述符上的事件是否就绪,poll 检测后都会立即返回。
  3. timeout = 特定的时间值:poll 调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在经过长度为timeout的时间后, poll 进行超时返回。(以毫秒为级别)

|------------------------------------------|---------------|------------------------------------------------------------------------------------|
| 函数 | 功能 | 返回值 |
| int poll(listen_array , nfds , timeout); | 监听集合中是否有套接字就绪 | 1.函数调用成功,则返回就绪的套接字个数 2.如果 timeout 时间耗尽,但没有套接字就绪,则返回 0。 3.如果函数调用失败,则返回 -1,同时会传出错误码。 |

常见的错误码有以下四种:

  • EFAULT:数组listen_array不包含在调用程序的地址空间中。
  • EINTR:此调用被信号所中断。
  • EINVAL:nfds值的大小超过RLIMIT_NOFILE。
  • ENOMEM:核心内存不足。

poll函数使用时的注意事项

既然前面都说了:poll模型可以监听的socket数量不受1024的硬限制,允许用户自定义数组作为监听集合,数组想设置多大就设置多大

那我现在写一段小代码,大家看一看这个程序对不对,系统会不会报错(假设已经完成了网络初始化并已经设置了服务器套接字监听)

cpp 复制代码
int ready_num;

//阻塞监听socket相关事件
if((ready_num = poll(client_sockfd_array , 4096 , -1)) == -1)
{
    perror("poll call failed\n");
    exit(0);
}
else
{
    printf("1\n");
}

怎么样?有自己的结果了吗?接下来,我们公布正确答案

这个程序是错的!!!!!

为什么呢?这就要牵涉到进程相关的知识了

这是因为一个进程默认打开的最大文件描述符个数就是1024,我们可以通过ulimit -a命令在终端下查看,如下图所示

所以,当我们在poll函数中的最大监听数那个位置,填入比1024更大的数值的话,系统就会报错,警告Invalid argument------无效的参数

想要填入比1024更大的数值,我们就需要去修改默认的文件描述符数量,由于每个系统,甚至每个版本改动文件描述符数量的操作方式不一定一样,所以永久修改文件描述符数量的方式,这里就不多作介绍了,感兴趣的同学可以去查一下对应自己系统、对应自己版本的修改方式

我们这里简单介绍一下只对当前终端生效的修改方式,如下图所示

本地主机使用poll模型实现简易一对多服务器的程序实现

程序构成

该服务器与客户端由以下几个程序共同组成:

  • func_2th_parcel.h:定义二次包裹的函数名
  • func_2th_parcel.c:对网络初始化相关的函数进行二次包裹
  • poll_server.c:使用poll模型的服务器程序
  • client.c:客户端程序
cpp 复制代码
/*************************************************************************
        > File Name: func_2th_parcel.h
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月18日 星期三 18时32分22秒
 ************************************************************************/
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <signal.h>
#include <pthread.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/mman.h>
#include <time.h>
#include <ctype.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <poll.h>
#include <sys/epoll.h>
 
//socket函数的二次包裹
int SOCKET(int domain , int type , int protocol);
 
//bind函数的二次包裹
int BIND(int sockfd , const struct sockaddr* addr , socklen_t  addrlen);
 
//listen函数的二次包裹
int LISTEN(int sockfd , int backlog);
 
//send函数的二次包裹
ssize_t SEND(int sockfd , const void* buf , size_t len , int flags);
 
//recv函数的二次包裹
ssize_t RECV(int sockfd , void* buf , size_t len , int flags);
 
//connect函数的二次包裹
int CONNECT(int sockfd , const struct sockaddr* addr , socklen_t addrlen);
 
//accept函数的二次包裹
int ACCEPT(int sockfd , struct sockaddr* addr , socklen_t addrlen);
 
//网络初始化函数
int SOCKET_NET_CREATE(const char* ip , int port);
 
//服务端与客户端建立连接并返回客户端套接字文件描述符
int SERVER_ACCEPTING(int server_fd);
cpp 复制代码
/*************************************************************************
        > File Name: func_2th_parcel.c
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月18日 星期三 18时32分42秒
 ************************************************************************/
 
#include <func_2th_parcel.h>
 
int SOCKET(int domain , int type , int protocol){
    int return_value;
    if((return_value = socket(domain , type , protocol)) == -1){
        perror("socket call failed!\n");
        return return_value;
    }
    return return_value;
}
 
int BIND(int sockfd , const struct sockaddr* addr , socklen_t addrlen){
    int return_value;   
    if((return_value = bind(sockfd , addr , addrlen)) == -1){
        perror("bind call failed!\n");
        return return_value;
    }                      
    return return_value;   
}
 
int LISTEN(int sockfd , int backlog){
    int return_value;   
    if((return_value = listen(sockfd , backlog)) == -1){
        perror("listen call failed!\n");
        return return_value;
    }                      
    return return_value;   
}
 
ssize_t SEND(int sockfd , const void* buf , size_t len , int flags){
    ssize_t return_value;
    if((return_value = send(sockfd , buf , len , flags)) == -1){
        perror("send call failed!\n");
        return return_value;
    }
    return return_value;
}
 
ssize_t RECV(int sockfd , void* buf , size_t len , int flags){
    ssize_t return_value;   
    if((return_value = recv(sockfd , buf , len , flags)) == -1){
        perror("recv call failed!\n");
        return return_value;
    }                      
    return return_value;   
}
 
int CONNECT(int sockfd , const struct sockaddr* addr , socklen_t addrlen){
    int return_value;   
    if((return_value = connect(sockfd , addr , addrlen)) == -1){
        perror("connect call failed!\n");
        return return_value;
    }                      
    return return_value;   
}
 
int ACCEPT(int sockfd , struct sockaddr* addr , socklen_t addrlen){
    int return_value;   
    if((return_value = accept(sockfd , addr , &addrlen)) == -1){
        perror("accept call failed!\n");
        return return_value;
    }                      
    return return_value;   
}
 
int SOCKET_NET_CREATE(const char* ip , int port){
    int sockfd;
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    inet_pton(AF_INET , ip , &addr.sin_addr.s_addr);
    sockfd = SOCKET(AF_INET , SOCK_STREAM , 0);
    BIND(sockfd , (struct sockaddr*)&addr , sizeof(addr));
    LISTEN(sockfd , 128);
    return sockfd;
}
 
int SERVER_ACCEPTING(int server_fd)
{
    int client_sockfd;
    struct sockaddr_in client_addr;
    char client_ip[16];
    char buffer[1500];
    bzero(buffer , sizeof(buffer));
    bzero(&client_addr , sizeof(client_addr));
    socklen_t addrlen = sizeof(client_addr);
    client_sockfd = ACCEPT(server_fd , (struct sockaddr*)&client_addr , addrlen);
    bzero(client_ip , 16);
    //将客户端的IP地址转成CPU可以识别的序列并存储到client_ip数组中
    inet_ntop(AF_INET , &client_addr.sin_addr.s_addr , client_ip , 16);
    sprintf(buffer , "Hi , %s welcome tcp test server service..\n" , client_ip);
    printf("client %s , %d , connection success , client sockfd is %d\n" , client_ip , ntohs(client_addr.sin_port) , client_sockfd);
    SEND(client_sockfd , buffer , strlen(buffer) , MSG_NOSIGNAL);
    return client_sockfd;
}
 
cpp 复制代码
/*************************************************************************
        > File Name: poll_server.c
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月25日 星期三 18时53分30秒
 ************************************************************************/

#include <func_2th_parcel.h>

int main(void)
{
    //一、进行网络初始化
    int server_sockfd;//服务器套接字文件描述符
    struct pollfd client_sockfd_array[1024];//存放客户端套接字相关结构体的数组  
    int client_sockfd;//客户端套接字文件描述符
    int ready_num;//获取处于就绪状态的套接字数目
    char rw_buffer[1500];//读写缓冲区
    int flag;
    int recv_len;//客户端发来的数据长度

    //将结构体数组中对应套接字文件描述符的那一位置为-1,方便后面查找就绪套接字
    for(int i = 1 ; i < 1024 ; i++)
    {
        //从1开始初始化是因为,0那一位要留给服务器套接字
        client_sockfd_array[i].fd = -1;
        client_sockfd_array[i].events = POLLIN;//都设置为监听读事件
    }
    bzero(rw_buffer , sizeof(rw_buffer));

    server_sockfd = SOCKET_NET_CREATE("192.168.79.128" , 6060);//初始化服务器套接字网络信息结构体
    
    //将服务器套接字结构体初始化一下
    client_sockfd_array[0].fd = server_sockfd;
    client_sockfd_array[0].events = POLLIN;
    printf("poll_server wait TCP connect\n");

    //二、启动监听,等待socket相关事件
    while(1)
    {
        //阻塞等待socket读相关事件
        if((ready_num = poll(client_sockfd_array , 1024 , -1)) == -1)
        {
            perror("poll call failed\n");
            exit(0);
        }
        //printf("readynum = %d\n" , ready_num);

        while(ready_num)
        {
            //辨别就绪,如果是服务端套接字就绪
            if(client_sockfd_array[0].revents == POLLIN)
            {
                client_sockfd = SERVER_ACCEPTING(client_sockfd_array[0].fd);//与客户端建立TCP链接
                
                for(int i = 1 ; i < 1024 ; i++)
                {
                    //将该客户端套接字,放到数组中有空缺的地方
                    if(client_sockfd_array[i].fd == -1)
                    {
                        client_sockfd_array[i].fd = client_sockfd;
                        break;
                    }
                }
                client_sockfd_array[0].revents = 0;//清0,防止ready_num > 1时会多次误判断就绪套接字为服务器套接字
            }
            //如果是客户端套接字就绪
            else
            {
                for(int i = 1 ; i < 1024 ; i++)
                {
                    //检测数组中下标位为i的地方是否存放的有客户端套接字文件描述符
                    if(client_sockfd_array[i].fd != -1)
                    {
                        //如果存放的有客户端套接字文件描述符,且该套接字处于就绪状态
                        if(client_sockfd_array[i].revents == POLLIN)
                        {
                            recv_len = RECV(client_sockfd_array[i].fd , rw_buffer , sizeof(rw_buffer) , 0);//获取数据长度
                            printf("客户端%d 发来数据 : %s , 现在进行处理\n" , client_sockfd_array[i].fd , rw_buffer);
                            flag = 0;
                            //如果recv_len = 0,就说明与客户端套接字对应的客户端退出了,将对应客户端套接字移出监听集合
                            if(recv_len == 0)
                            {
                                printf("客户端%d 已下线\n" , client_sockfd_array[i].fd);
                                close(client_sockfd_array[i].fd);
                                client_sockfd_array[i].fd = -1;
                                break;
                            }
                            //如果recv_len > 0,说明需要进行业务处理:小写字母转大写字母
                            while(recv_len > flag)
                            {
                                rw_buffer[flag] = toupper(rw_buffer[flag]);
                                flag++;
                            }
                            printf("已向客户端%d 发送处理后的数据 : %s\n" , client_sockfd_array[i].fd , rw_buffer);
                            SEND(client_sockfd_array[i].fd , rw_buffer , recv_len , MSG_NOSIGNAL);//发送处理后的数据给客户端
                            bzero(rw_buffer , sizeof(rw_buffer));//清空读写缓冲区
                            recv_len = 0;//重置数据长度
                            client_sockfd_array[i].revents = 0;//清0,防止ready_num > 1时会多次误判断就绪套接字为该客户端套接字
                            break;
                        }
                    }
                }
            }
            ready_num--;//已经处理一个,就绪套接字数量-1
        }
    }
    close(server_sockfd);
    printf("server shutdown\n");
    return 0;  
}
cpp 复制代码
/*************************************************************************
        > File Name: client.c
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月19日 星期四 18时29分12秒
 ************************************************************************/
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <signal.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <time.h>
 
//服务器实现大小写转换业务
 
int main()
{
    //1.定义网络信息结构体与读写缓冲区并初始化
    struct sockaddr_in dest_addr;
    char buffer[1500];
    bzero(&dest_addr , sizeof(dest_addr));
    bzero(buffer , sizeof(buffer));
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(6060);
    //字符串ip转大端序列
    inet_pton(AF_INET , "192.168.79.128" , &dest_addr.sin_addr.s_addr);
    int sockfd = socket(AF_INET , SOCK_STREAM , 0);
    int i;
    //2.判断连接是否成功
    if((connect(sockfd , (struct sockaddr*) &dest_addr , sizeof(dest_addr))) == -1)
    {
        perror("connect failed!\n");
        exit(0);
    }
    recv(sockfd , buffer , sizeof(buffer) , 0);
    printf("%s" , buffer);
    bzero(buffer , sizeof(buffer));
    //3.循环读取终端输入的数据
    while( (fgets(buffer , sizeof(buffer) , stdin) ) != NULL)
    {
        i = strlen(buffer);
        buffer[i-1] = '\0';
        //向服务端发送消息
        send(sockfd , buffer , strlen(buffer) , MSG_NOSIGNAL);
        //接收服务端发来的消息
        recv(sockfd , buffer , sizeof(buffer) , 0);
        //打印服务端发来的信息
        printf("response : %s\n" , buffer);
        //清空读写缓冲区,以便下一次放入数据
        bzero(buffer , sizeof(buffer));
    }
    //4.关闭套接字,断开连接
    close(sockfd);
    return 0;
}

结果图示:

以上就是本篇博客的全部内容了,大家有什么地方没有看懂的话,可以在评论区留言给我,咱要力所能及的话就帮大家解答解答

今天的学习记录到此结束啦,咱们下篇文章见,ByeBye!

相关推荐
Chef_Chen6 分钟前
从0开始学习机器学习--Day22--优化总结以及误差作业(上)
人工智能·学习·机器学习
raysync88810 分钟前
如何保障医院内部的隔离网安全跨网文件交换?
网络·安全
想学习java初学者16 分钟前
Docker compose部署elasticsearch(单机版)
运维·docker·容器
虾球xz22 分钟前
游戏引擎学习第11天
stm32·学习·游戏引擎
落落落sss23 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
我救我自己23 分钟前
UE5运行时创建slate窗口
java·服务器·ue5
心怀梦想的咸鱼25 分钟前
Ue5 umg学习(三)文本控件
学习·ue5
心怀梦想的咸鱼26 分钟前
Ue5 umg学习(二)图像控件,锚点
学习·ue5
人类群星闪耀时43 分钟前
未来运维的发展趋势:运维领域的新技术与趋势
运维
御控物联_姜44 分钟前
IP、网关、子网掩码的验证逻辑及程序(.Net)
网络