• Linux网络编程系列之服务器编程——多路复用模型


    Linux网络编程系列  (够吃,管饱)

            1、Linux网络编程系列之网络编程基础

            2、Linux网络编程系列之TCP协议编程

            3、Linux网络编程系列之UDP协议编程

            4、Linux网络编程系列之UDP广播

            5、Linux网络编程系列之UDP组播

            6、Linux网络编程系列之服务器编程——阻塞IO模型

            7、Linux网络编程系列之服务器编程——非阻塞IO模型

            8、Linux网络编程系列之服务器编程——多路复用模型

            9、Linux网络编程系列之服务器编程——信号驱动模型

    一、什么是多路复用模型

            服务器的多路复用模型指的是利用操作系统提供的多路复用机制,同时处理多个客户端连接请求的能力。在服务器端,常见的多路复用技术包括select、poll和epoll等。这些技术允许服务器同时监听多个客户端连接请求,当有请求到达时,会通知服务器进行处理。通过使用多路复用技术,可以避免一个线程只处理一个客户端连接的情况,提高服务器的并发性能和响应速度。在实际应用中,多路复用技术被广泛地应用于Web服务器、游戏服务器、消息队列等领域。

            注:下面案例演示采用select结合TCP协议,一般不结合UDP协议使用,案例也演示了select结合UDP协议。

    二、特性

            1、支持大量并发连接

            多路复用技术可以同时监听多个客户端连接请求,避免了一个线程只处理一个客户端连接的情况,从而可以支持更多的并发连接。

            2、减少系统开销

            采用多路复用技术可以减少系统开销,因为不需要为每个连接开启一个线程或进程,避免了系统资源浪费。

            3、提高响应速度

            采用多路复用技术可以提高服务器的响应速度,因为多个连接可以同时处理,避免了连接排队的情况。

            4、更好的可扩展性

            多路复用技术可以更好的支持服务器的可扩展性,因为它可以动态地管理和调度连接,方便服务器的扩展和升级。

    三、使用场景

            1、高并发的Web服务器

            对于高并发的Web服务器,采用多路复用技术可以同时监听多个客户端连接请求,避免了一个线程只处理一个客户端连接的情况,从而可以支持更多的并发连接。

            2、实时通信服务器

            对于实时通信服务器,采用多路复用技术可以同时监听多个客户端连接请求,可以处理多种类型的通信,包括即时通讯、实时游戏等。

            3、TCP/IP服务器

            对于TCP/IP服务器,采用多路复用技术可以提高服务器的性能和可靠性,因为多个连接可以同时处理,避免了连接排队的情况。

            4、网络监控工具

            对于网络监控工具,采用多路复用技术可以同时处理多个客户端的请求,并对网络数据进行监控和分析。

    四、模型框架(通信流程)

            1、建立套接字。使用socket()

            2、设置端口复用。使用setsockopt()

            3、绑定自己的IP地址和端口号。使用bind()

            4、设置监听。使用listen()

            5、多路复用准备工作。使用文件描述符集合操作

            6、循环监听,开始多路复用。使用select()

            7、处理客户端连接或者数据接收。使用accept()或者recv()

            8、关闭套接字。使用close()

    五、相关函数API接口

            TCP通信流程常规的API那些在本系列的TCP协议里有大量展示,这里省略,详情可以点击本文开头的链接查看

            1、多路复用select

    1. // 多路复用select
    2. int select(int nfds,
    3. fd_set *readfds,
    4. fd_set *writefds,
    5. fd_set *exceptfds,
    6. struct timeval *timeout);
    7. // 接口说明
    8. 返回值:成功返回readfds,writefds,exceptfds中状态发生变化的文件描述符数量,失败返回-1
    9. 参数nfds:通常填写三个集合中最大的文件描述符值+1,让内核检测多少个文件描述符的状态
    10. 参数readfds:监控有读数据到达文件描述符集合
    11. 参数writefds:监控有写数据到达文件描述符集合
    12. 参数exceptfds:监控有异常发生到达文件描述符集合
    13. 参数timeout:设置阻塞等待时间,三种情况
    14. 1)、设置为NULL,一直阻塞等待
    15. 2)、设置timevl,等待固定的时间
    16. 3)、设置timeval里时间为0,在检测完描述符后立即返回

            2、集合操作

    1. // 把文件描述符集合里fd清0
    2. void FD_CLR(int fd, fd_set *set);
    3. // 把文件描述符集合里fd位置1
    4. void FD_SET(int fd, fd_set *set);
    5. // 把文件描述符集合里所有位清0
    6. void FD_ZERO(fd_set *set);
    7. // 测试文件描述符集合里fd是否置1
    8. int FD_ISSET(int fd, fd_set *set);

    六、案例

           1、 采用select函数,完成多路复用TCP服务器的通信演示,用nc命令来模拟客户端

    1. // 多路复用TCP服务器的案例
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #define MAX_LISTEN FD_SETSIZE // 最大能处理的连接数, 1024
    12. #define SERVER_IP "192.168.64.128" // 记得改为自己IP
    13. #define SERVER_PORT 20000 // 不能超过65535,也不要低于1000,防止端口误用
    14. // 定义客服端管理类
    15. struct ClientManager
    16. {
    17. int client[MAX_LISTEN]; // 存储客户端的套接字
    18. char ip[MAX_LISTEN][20]; // 客户端套接字IP
    19. uint16_t port[MAX_LISTEN]; // 客户端套接字端口号
    20. };
    21. // 初始化客户端管理类
    22. void client_manager_init(struct ClientManager *manager)
    23. {
    24. for(int i = 0; i < MAX_LISTEN; i++)
    25. {
    26. manager->client[i] = -1;
    27. manager->port[i] = 0;
    28. memset(manager->ip, 0, sizeof(manager->ip));
    29. }
    30. }
    31. int main(int argc, char *argv[])
    32. {
    33. // 1、建立套接字
    34. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    35. if(sockfd == -1)
    36. {
    37. perror("socket fail");
    38. return -1;
    39. }
    40. // 2、设置端口复用(推荐)
    41. int optval = 1; // 这里设置为端口复用,所以随便写一个值
    42. int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
    43. if(ret == -1)
    44. {
    45. perror("setsockopt fail");
    46. close(sockfd);
    47. return -1;
    48. }
    49. // 3、绑定自己的IP地址和端口号
    50. struct sockaddr_in server_addr = {0};
    51. socklen_t addr_len = sizeof(struct sockaddr);
    52. server_addr.sin_family = AF_INET; // 指定协议为IPV4地址协议
    53. server_addr.sin_port = htons(SERVER_PORT); // 端口号
    54. // server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    55. server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // IP地址
    56. ret = bind(sockfd, (struct sockaddr *)&server_addr, addr_len);
    57. if(ret == -1)
    58. {
    59. perror("bind fail");
    60. close(sockfd);
    61. return -1;
    62. }
    63. // 4、设置监听
    64. ret = listen(sockfd, MAX_LISTEN);
    65. if(ret == -1)
    66. {
    67. perror("listen fail");
    68. close(sockfd);
    69. return -1;
    70. }
    71. // 5、多路复用的准备工作
    72. fd_set client_set, active_set;
    73. // (1)、清空活跃的文件描述符集合
    74. FD_ZERO(&active_set);
    75. // (2)、把服务器的套接字文件描述符加入到活跃的文件描述符集合中
    76. FD_SET(sockfd, &active_set);
    77. // (3)、初始化活跃集合中最大的文件描述符
    78. int maxfd = sockfd;
    79. // (4)、初始化能接受的活跃客户端管理类
    80. struct ClientManager manager;
    81. client_manager_init(&manager);
    82. uint16_t port = 0; // 新的客户端端口号
    83. char ip[20] = {0}; // 新的客户端IP
    84. struct sockaddr_in client_addr; // 新的客户端地址
    85. char recv_msg[128] = {0}; // 用来接收客户端的数据
    86. printf("wait client...\n");
    87. while(1)
    88. {
    89. client_set = active_set; // 先备份活跃的集合
    90. // 6、多路复用,同时监听多个文件描述符状态,阻塞等待
    91. int num = select(maxfd+1, &client_set, NULL, NULL, NULL);
    92. if(num == -1)
    93. {
    94. perror("select fail");
    95. close(sockfd);
    96. return -1;
    97. }
    98. // 如果监听文件描述符发生变化,说明一定有新的客户端连接上来
    99. if(FD_ISSET(sockfd, &client_set))
    100. {
    101. int new_client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
    102. if(new_client_fd == -1)
    103. {
    104. perror("accept fail");
    105. continue;
    106. }
    107. else
    108. {
    109. // 打印连接的客服端IP和端口号
    110. memset(ip, 0, sizeof(ip));
    111. strcpy(ip, inet_ntoa(client_addr.sin_addr));
    112. port = ntohs(client_addr.sin_port);
    113. printf("[%s:%d] connect\n", ip, port);
    114. // 把新的客户端套接字加入到活跃的集合中
    115. FD_SET(new_client_fd, &active_set);
    116. // 更新最大活跃文件描述符
    117. if(maxfd < new_client_fd)
    118. {
    119. maxfd = new_client_fd;
    120. }
    121. // 把新的套接字加入到空的活跃客户端套接字数组
    122. for(int i = 0; i < MAX_LISTEN; i++)
    123. {
    124. if(manager.client[i] == -1)
    125. {
    126. manager.client[i] = new_client_fd;
    127. manager.port[i] = port;
    128. strcpy(manager.ip[i], ip);
    129. break;
    130. }
    131. }
    132. // 如果只有服务器的套接字发生变化,新的套接字没有发送数据
    133. // 那就继续监听,否则需要打印套接字的信息
    134. if(--num == 0)
    135. {
    136. continue;
    137. }
    138. }
    139. }
    140. // 如果客服端发送数据过来
    141. for(int i = 0; i < MAX_LISTEN; i++)
    142. {
    143. if(manager.client[i] == -1)
    144. {
    145. continue;
    146. }
    147. // 如果活跃的客户端有发送数据,注意这里要采用client_set,而不是active_set,否则会读取不了数据
    148. if(FD_ISSET(manager.client[i], &client_set))
    149. {
    150. // 接收数据
    151. memset(recv_msg, 0, sizeof(recv_msg));
    152. ret = recv(manager.client[i], recv_msg, sizeof(recv_msg), 0);
    153. memset(ip, 0, sizeof(ip));
    154. strcpy(ip, inet_ntoa(client_addr.sin_addr));
    155. port = ntohs(client_addr.sin_port);
    156. if(ret == 0)
    157. {
    158. printf("[%s:%d] disconnect\n", manager.ip[i], manager.port[i]);
    159. FD_CLR(manager.client[i], &active_set); // 清空对应活跃集合的套接字
    160. manager.client[i] = -1;
    161. manager.port[i] = 0;
    162. memset(manager.ip[i], 0, sizeof(ip));
    163. // 需要重新更新活跃集合中最大的文件描述符
    164. maxfd = sockfd;
    165. for(int j = 0; j < MAX_LISTEN; j++)
    166. {
    167. if(manager.client[j] != -1 && maxfd < manager.client[j])
    168. {
    169. maxfd = manager.client[j];
    170. }
    171. }
    172. }
    173. else if(ret > 0)
    174. {
    175. printf("[%s:%d] send data: %s\n", manager.ip[i], manager.port[i], recv_msg);
    176. }
    177. // 如果所有发生变化的套接字都已经处理完成
    178. if(--num == 0)
    179. {
    180. break;
    181. }
    182. }
    183. }
    184. }
    185. close(sockfd);
    186. return 0;
    187. }

               2、 采用select函数,完成多路复用UDP服务器的通信演示,用nc命令来模拟客户端

    1. // 多路复用TCP服务器的案例
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #define MAX_LISTEN FD_SETSIZE // 最大能处理的连接数, 1024
    13. #define SERVER_IP "192.168.64.128" // 记得改为自己IP
    14. #define SERVER_PORT 20000 // 不能超过65535,也不要低于1000,防止端口误用
    15. int main(int argc, char *argv[])
    16. {
    17. // 1、建立套接字
    18. int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    19. if(sockfd == -1)
    20. {
    21. perror("socket fail");
    22. return -1;
    23. }
    24. // 2、设置端口复用(推荐)
    25. int optval = 1; // 这里设置为端口复用,所以随便写一个值
    26. int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
    27. if(ret == -1)
    28. {
    29. perror("setsockopt fail");
    30. close(sockfd);
    31. return -1;
    32. }
    33. // 3、绑定自己的IP地址和端口号
    34. struct sockaddr_in server_addr = {0};
    35. socklen_t addr_len = sizeof(struct sockaddr);
    36. server_addr.sin_family = AF_INET; // 指定协议为IPV4地址协议
    37. server_addr.sin_port = htons(SERVER_PORT); // 端口号
    38. // server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    39. server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // IP地址
    40. ret = bind(sockfd, (struct sockaddr *)&server_addr, addr_len);
    41. if(ret == -1)
    42. {
    43. perror("bind fail");
    44. close(sockfd);
    45. return -1;
    46. }
    47. // 4、多路复用的准备工作
    48. fd_set client_set, active_set;
    49. // (1)、清空活跃的文件描述符集合
    50. FD_ZERO(&active_set);
    51. // (2)、把服务器的套接字文件描述符加入到活跃的文件描述符集合中
    52. FD_SET(sockfd, &active_set);
    53. // (3)、初始化活跃集合中最大的文件描述符
    54. int maxfd = MAX_LISTEN;
    55. // (4)、初始化能接受的活跃客户端套接字数组
    56. int client[MAX_LISTEN];
    57. for(int i = 0; i < MAX_LISTEN; i++)
    58. {
    59. client[i] = -1; // 空的置为-1,活跃的置为对应的文件描述符
    60. }
    61. uint16_t port = 0; // 新的客户端端口号
    62. char ip[20] = {0}; // 新的客户端IP
    63. struct sockaddr_in client_addr; // 新的客户端地址
    64. char recv_msg[128] = {0}; // 用来接收客户端的数据
    65. printf("wait client...\n");
    66. while(1)
    67. {
    68. client_set = active_set; // 先备份活跃的集合
    69. // 5、多路复用,同时监听多个文件描述符状态,阻塞等待
    70. int num = select(maxfd+1, &client_set, NULL, NULL, NULL);
    71. if(num == -1)
    72. {
    73. perror("select fail");
    74. close(sockfd);
    75. return -1;
    76. }
    77. else
    78. {
    79. // 接收数据
    80. memset(recv_msg, 0, sizeof(recv_msg));
    81. ret = recvfrom(sockfd, recv_msg, sizeof(recv_msg), 0, (struct sockaddr*)&client_addr, &addr_len);
    82. memset(ip, 0, sizeof(ip));
    83. strcpy(ip, inet_ntoa(client_addr.sin_addr));
    84. port = ntohs(client_addr.sin_port);
    85. printf("[%s:%d] send data: %s\n", ip, port, recv_msg);
    86. }
    87. }
    88. close(sockfd);
    89. return 0;
    90. }

            注:TCP和UDP的代码有所不同,多路复用监听方式有所不同。

    七、总结

            多路复用适用于处理连接的客户端的数量小于1024的场景,当然你可以改,让其超过1024限制,这里不做讨论。多路复用模型TCP服务器跟简单的TCP服务器通信流程很像,就是在接收客户端时要采用select要进行操作。一般情况下,不采用多路复用select结合UDP协议使用,但是不代表不行,案例给出了演示。

  • 相关阅读:
    PAT甲级 A1057 Stack
    python获取剪切板内容并打印出来
    并发编程中的原子性,可见性,有序性问题
    CSDN竞赛 - 第四期 总结
    安装Ubuntu系统并搭建C语言环境(超详细教程)
    大数据平台搭建2024(一)
    2016款奔驰C200车COMAND显示屏黑屏
    计算机网络——应用层协议(1)
    什么是同步容器和并发容器的实现?
    UI自动化
  • 原文地址:https://blog.csdn.net/AABond/article/details/133419182