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


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

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

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

    目录

    poll模型的优缺点

    poll模型的相关接口

    监听结构体struct pollfd

    监听函数poll

    poll函数使用时的注意事项

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

    程序构成        结果图示


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

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

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

    poll模型的优缺点

    优点:

    1. 在内网场景下,poll也算是不错的IO复用模式
    2. 对监听集合进行了传入传出分离设置,不需要用户再自己设置传入集合(监听集合)和传出集合(就绪集合)
    3. 相比select,poll可以监听的事件种类更加丰富(具体可见下面的博客接口部分——struct pollfd)
    4. 可以为不同的套接字设置不同的监听事件,不像select模型只能批量设置监听事件
    5. poll模型可以监听的socket数量不受1024的硬限制,允许用户自定义数组作为监听集合,数组想设置多大就设置多大(其实这也不能完全算优点,下面讲缺点时会讲为什么)

    缺点:

    1. poll模型的兼容性极差,甚至部分linux系统都不认识poll模型,更别说windows系统了
    2. 随着select的持续使用,会产生大量的拷贝开销和挂载开销(原因和select模型一样)
    3. 与select模型一样,poll的监听也是通过一次次的遍历实现的,非常消耗CPU,会导致服务器吞吐能力会非常差。更可怕的是,select遍历的大小仅为1024,而poll模型遍历的大小是由用户决定的,如果用户设置的监听集合大小为100000,就意味着poll遍历的大小就是100000,服务器很可能会直接瘫痪

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

    poll模型的相关接口

    以下接口的头文件都是 #include

    监听结构体struct pollfd

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

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

    1. #define SIZE 10000
    2. struct pollfd listen_array[SIZE];//用户自定义的监听集合
    3. pollfd结构体中的成员
    4. struct pollfd
    5. {
    6. int fd; //目标套接字的文件描述符,取消监听就设为-1
    7. short events; //想要监听什么事件
    8. short revetns;
    9. };

    我们来讲解一下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的硬限制,允许用户自定义数组作为监听集合,数组想设置多大就设置多大

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

    1. int ready_num;
    2. //阻塞监听socket相关事件
    3. if((ready_num = poll(client_sockfd_array , 4096 , -1)) == -1)
    4. {
    5. perror("poll call failed\n");
    6. exit(0);
    7. }
    8. else
    9. {
    10. printf("1\n");
    11. }

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

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

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

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

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

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

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

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

    程序构成

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

    • func_2th_parcel.h:定义二次包裹的函数名
    • func_2th_parcel.c:对网络初始化相关的函数进行二次包裹
    • poll_server.c:使用poll模型的服务器程序
    • client.c:客户端程序
    1. /*************************************************************************
    2. > File Name: func_2th_parcel.h
    3. > Author: Nan
    4. > Mail: **@qq.com
    5. > Created Time: 2023年10月18日 星期三 18时32分22秒
    6. ************************************************************************/
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include
    13. #include
    14. #include
    15. #include
    16. #include
    17. #include
    18. #include
    19. #include
    20. #include
    21. #include
    22. #include
    23. #include
    24. #include
    25. #include
    26. //socket函数的二次包裹
    27. int SOCKET(int domain , int type , int protocol);
    28. //bind函数的二次包裹
    29. int BIND(int sockfd , const struct sockaddr* addr , socklen_t addrlen);
    30. //listen函数的二次包裹
    31. int LISTEN(int sockfd , int backlog);
    32. //send函数的二次包裹
    33. ssize_t SEND(int sockfd , const void* buf , size_t len , int flags);
    34. //recv函数的二次包裹
    35. ssize_t RECV(int sockfd , void* buf , size_t len , int flags);
    36. //connect函数的二次包裹
    37. int CONNECT(int sockfd , const struct sockaddr* addr , socklen_t addrlen);
    38. //accept函数的二次包裹
    39. int ACCEPT(int sockfd , struct sockaddr* addr , socklen_t addrlen);
    40. //网络初始化函数
    41. int SOCKET_NET_CREATE(const char* ip , int port);
    42. //服务端与客户端建立连接并返回客户端套接字文件描述符
    43. int SERVER_ACCEPTING(int server_fd);
    1. /*************************************************************************
    2. > File Name: func_2th_parcel.c
    3. > Author: Nan
    4. > Mail: **@qq.com
    5. > Created Time: 2023年10月18日 星期三 18时32分42秒
    6. ************************************************************************/
    7. #include
    8. int SOCKET(int domain , int type , int protocol){
    9. int return_value;
    10. if((return_value = socket(domain , type , protocol)) == -1){
    11. perror("socket call failed!\n");
    12. return return_value;
    13. }
    14. return return_value;
    15. }
    16. int BIND(int sockfd , const struct sockaddr* addr , socklen_t addrlen){
    17. int return_value;
    18. if((return_value = bind(sockfd , addr , addrlen)) == -1){
    19. perror("bind call failed!\n");
    20. return return_value;
    21. }
    22. return return_value;
    23. }
    24. int LISTEN(int sockfd , int backlog){
    25. int return_value;
    26. if((return_value = listen(sockfd , backlog)) == -1){
    27. perror("listen call failed!\n");
    28. return return_value;
    29. }
    30. return return_value;
    31. }
    32. ssize_t SEND(int sockfd , const void* buf , size_t len , int flags){
    33. ssize_t return_value;
    34. if((return_value = send(sockfd , buf , len , flags)) == -1){
    35. perror("send call failed!\n");
    36. return return_value;
    37. }
    38. return return_value;
    39. }
    40. ssize_t RECV(int sockfd , void* buf , size_t len , int flags){
    41. ssize_t return_value;
    42. if((return_value = recv(sockfd , buf , len , flags)) == -1){
    43. perror("recv call failed!\n");
    44. return return_value;
    45. }
    46. return return_value;
    47. }
    48. int CONNECT(int sockfd , const struct sockaddr* addr , socklen_t addrlen){
    49. int return_value;
    50. if((return_value = connect(sockfd , addr , addrlen)) == -1){
    51. perror("connect call failed!\n");
    52. return return_value;
    53. }
    54. return return_value;
    55. }
    56. int ACCEPT(int sockfd , struct sockaddr* addr , socklen_t addrlen){
    57. int return_value;
    58. if((return_value = accept(sockfd , addr , &addrlen)) == -1){
    59. perror("accept call failed!\n");
    60. return return_value;
    61. }
    62. return return_value;
    63. }
    64. int SOCKET_NET_CREATE(const char* ip , int port){
    65. int sockfd;
    66. struct sockaddr_in addr;
    67. addr.sin_family = AF_INET;
    68. addr.sin_port = htons(port);
    69. inet_pton(AF_INET , ip , &addr.sin_addr.s_addr);
    70. sockfd = SOCKET(AF_INET , SOCK_STREAM , 0);
    71. BIND(sockfd , (struct sockaddr*)&addr , sizeof(addr));
    72. LISTEN(sockfd , 128);
    73. return sockfd;
    74. }
    75. int SERVER_ACCEPTING(int server_fd)
    76. {
    77. int client_sockfd;
    78. struct sockaddr_in client_addr;
    79. char client_ip[16];
    80. char buffer[1500];
    81. bzero(buffer , sizeof(buffer));
    82. bzero(&client_addr , sizeof(client_addr));
    83. socklen_t addrlen = sizeof(client_addr);
    84. client_sockfd = ACCEPT(server_fd , (struct sockaddr*)&client_addr , addrlen);
    85. bzero(client_ip , 16);
    86. //将客户端的IP地址转成CPU可以识别的序列并存储到client_ip数组中
    87. inet_ntop(AF_INET , &client_addr.sin_addr.s_addr , client_ip , 16);
    88. sprintf(buffer , "Hi , %s welcome tcp test server service..\n" , client_ip);
    89. printf("client %s , %d , connection success , client sockfd is %d\n" , client_ip , ntohs(client_addr.sin_port) , client_sockfd);
    90. SEND(client_sockfd , buffer , strlen(buffer) , MSG_NOSIGNAL);
    91. return client_sockfd;
    92. }
    1. /*************************************************************************
    2. > File Name: poll_server.c
    3. > Author: Nan
    4. > Mail: **@qq.com
    5. > Created Time: 2023年10月25日 星期三 18时53分30秒
    6. ************************************************************************/
    7. #include
    8. int main(void)
    9. {
    10. //一、进行网络初始化
    11. int server_sockfd;//服务器套接字文件描述符
    12. struct pollfd client_sockfd_array[1024];//存放客户端套接字相关结构体的数组
    13. int client_sockfd;//客户端套接字文件描述符
    14. int ready_num;//获取处于就绪状态的套接字数目
    15. char rw_buffer[1500];//读写缓冲区
    16. int flag;
    17. int recv_len;//客户端发来的数据长度
    18. //将结构体数组中对应套接字文件描述符的那一位置为-1,方便后面查找就绪套接字
    19. for(int i = 1 ; i < 1024 ; i++)
    20. {
    21. //从1开始初始化是因为,0那一位要留给服务器套接字
    22. client_sockfd_array[i].fd = -1;
    23. client_sockfd_array[i].events = POLLIN;//都设置为监听读事件
    24. }
    25. bzero(rw_buffer , sizeof(rw_buffer));
    26. server_sockfd = SOCKET_NET_CREATE("192.168.79.128" , 6060);//初始化服务器套接字网络信息结构体
    27. //将服务器套接字结构体初始化一下
    28. client_sockfd_array[0].fd = server_sockfd;
    29. client_sockfd_array[0].events = POLLIN;
    30. printf("poll_server wait TCP connect\n");
    31. //二、启动监听,等待socket相关事件
    32. while(1)
    33. {
    34. //阻塞等待socket读相关事件
    35. if((ready_num = poll(client_sockfd_array , 1024 , -1)) == -1)
    36. {
    37. perror("poll call failed\n");
    38. exit(0);
    39. }
    40. //printf("readynum = %d\n" , ready_num);
    41. while(ready_num)
    42. {
    43. //辨别就绪,如果是服务端套接字就绪
    44. if(client_sockfd_array[0].revents == POLLIN)
    45. {
    46. client_sockfd = SERVER_ACCEPTING(client_sockfd_array[0].fd);//与客户端建立TCP链接
    47. for(int i = 1 ; i < 1024 ; i++)
    48. {
    49. //将该客户端套接字,放到数组中有空缺的地方
    50. if(client_sockfd_array[i].fd == -1)
    51. {
    52. client_sockfd_array[i].fd = client_sockfd;
    53. break;
    54. }
    55. }
    56. client_sockfd_array[0].revents = 0;//清0,防止ready_num > 1时会多次误判断就绪套接字为服务器套接字
    57. }
    58. //如果是客户端套接字就绪
    59. else
    60. {
    61. for(int i = 1 ; i < 1024 ; i++)
    62. {
    63. //检测数组中下标位为i的地方是否存放的有客户端套接字文件描述符
    64. if(client_sockfd_array[i].fd != -1)
    65. {
    66. //如果存放的有客户端套接字文件描述符,且该套接字处于就绪状态
    67. if(client_sockfd_array[i].revents == POLLIN)
    68. {
    69. recv_len = RECV(client_sockfd_array[i].fd , rw_buffer , sizeof(rw_buffer) , 0);//获取数据长度
    70. printf("客户端%d 发来数据 : %s , 现在进行处理\n" , client_sockfd_array[i].fd , rw_buffer);
    71. flag = 0;
    72. //如果recv_len = 0,就说明与客户端套接字对应的客户端退出了,将对应客户端套接字移出监听集合
    73. if(recv_len == 0)
    74. {
    75. printf("客户端%d 已下线\n" , client_sockfd_array[i].fd);
    76. close(client_sockfd_array[i].fd);
    77. client_sockfd_array[i].fd = -1;
    78. break;
    79. }
    80. //如果recv_len > 0,说明需要进行业务处理:小写字母转大写字母
    81. while(recv_len > flag)
    82. {
    83. rw_buffer[flag] = toupper(rw_buffer[flag]);
    84. flag++;
    85. }
    86. printf("已向客户端%d 发送处理后的数据 : %s\n" , client_sockfd_array[i].fd , rw_buffer);
    87. SEND(client_sockfd_array[i].fd , rw_buffer , recv_len , MSG_NOSIGNAL);//发送处理后的数据给客户端
    88. bzero(rw_buffer , sizeof(rw_buffer));//清空读写缓冲区
    89. recv_len = 0;//重置数据长度
    90. client_sockfd_array[i].revents = 0;//清0,防止ready_num > 1时会多次误判断就绪套接字为该客户端套接字
    91. break;
    92. }
    93. }
    94. }
    95. }
    96. ready_num--;//已经处理一个,就绪套接字数量-1
    97. }
    98. }
    99. close(server_sockfd);
    100. printf("server shutdown\n");
    101. return 0;
    102. }
    1. /*************************************************************************
    2. > File Name: client.c
    3. > Author: Nan
    4. > Mail: **@qq.com
    5. > Created Time: 2023年10月19日 星期四 18时29分12秒
    6. ************************************************************************/
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include
    13. #include
    14. #include
    15. #include
    16. #include
    17. #include
    18. #include
    19. #include
    20. #include
    21. //服务器实现大小写转换业务
    22. int main()
    23. {
    24. //1.定义网络信息结构体与读写缓冲区并初始化
    25. struct sockaddr_in dest_addr;
    26. char buffer[1500];
    27. bzero(&dest_addr , sizeof(dest_addr));
    28. bzero(buffer , sizeof(buffer));
    29. dest_addr.sin_family = AF_INET;
    30. dest_addr.sin_port = htons(6060);
    31. //字符串ip转大端序列
    32. inet_pton(AF_INET , "192.168.79.128" , &dest_addr.sin_addr.s_addr);
    33. int sockfd = socket(AF_INET , SOCK_STREAM , 0);
    34. int i;
    35. //2.判断连接是否成功
    36. if((connect(sockfd , (struct sockaddr*) &dest_addr , sizeof(dest_addr))) == -1)
    37. {
    38. perror("connect failed!\n");
    39. exit(0);
    40. }
    41. recv(sockfd , buffer , sizeof(buffer) , 0);
    42. printf("%s" , buffer);
    43. bzero(buffer , sizeof(buffer));
    44. //3.循环读取终端输入的数据
    45. while( (fgets(buffer , sizeof(buffer) , stdin) ) != NULL)
    46. {
    47. i = strlen(buffer);
    48. buffer[i-1] = '\0';
    49. //向服务端发送消息
    50. send(sockfd , buffer , strlen(buffer) , MSG_NOSIGNAL);
    51. //接收服务端发来的消息
    52. recv(sockfd , buffer , sizeof(buffer) , 0);
    53. //打印服务端发来的信息
    54. printf("response : %s\n" , buffer);
    55. //清空读写缓冲区,以便下一次放入数据
    56. bzero(buffer , sizeof(buffer));
    57. }
    58. //4.关闭套接字,断开连接
    59. close(sockfd);
    60. return 0;
    61. }

    结果图示

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

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

  • 相关阅读:
    数据库之元数据
    【深度学习实验】图像处理(二):PIL 和 PyTorch(transforms)中的图像处理与随机图片增强
    JavaScript反爬虫技巧详细攻略
    中介子方程二十二
    关于电影的HTML网页设计—— 电影小黄人6页 HTML+CSS+JavaScript
    jQuery 在图片和文字中插入内容(多种情况考虑)
    稳定性实践:限流降级
    Node.js项目(二)
    【蜂鸟E203的FPGA验证】Chap.5 基于E203内核的处理器验证+ 基于FPGA的处理器设计与验证流程
    sh脚本工具集锦(文件批量操作、音视频相关)持续更新
  • 原文地址:https://blog.csdn.net/m0_53133879/article/details/134127549