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
// 多路复用select int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); // 接口说明 返回值:成功返回readfds,writefds,exceptfds中状态发生变化的文件描述符数量,失败返回-1 参数nfds:通常填写三个集合中最大的文件描述符值+1,让内核检测多少个文件描述符的状态 参数readfds:监控有读数据到达文件描述符集合 参数writefds:监控有写数据到达文件描述符集合 参数exceptfds:监控有异常发生到达文件描述符集合 参数timeout:设置阻塞等待时间,三种情况 (1)、设置为NULL,一直阻塞等待 (2)、设置timevl,等待固定的时间 (3)、设置timeval里时间为0,在检测完描述符后立即返回
2、集合操作
// 把文件描述符集合里fd清0 void FD_CLR(int fd, fd_set *set); // 把文件描述符集合里fd位置1 void FD_SET(int fd, fd_set *set); // 把文件描述符集合里所有位清0 void FD_ZERO(fd_set *set); // 测试文件描述符集合里fd是否置1 int FD_ISSET(int fd, fd_set *set);
六、案例
1、 采用select函数,完成多路复用TCP服务器的通信演示,用nc命令来模拟客户端
// 多路复用TCP服务器的案例 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #define MAX_LISTEN FD_SETSIZE // 最大能处理的连接数, 1024 #define SERVER_IP "192.168.64.128" // 记得改为自己IP #define SERVER_PORT 20000 // 不能超过65535,也不要低于1000,防止端口误用 // 定义客服端管理类 struct ClientManager { int client[MAX_LISTEN]; // 存储客户端的套接字 char ip[MAX_LISTEN][20]; // 客户端套接字IP uint16_t port[MAX_LISTEN]; // 客户端套接字端口号 }; // 初始化客户端管理类 void client_manager_init(struct ClientManager *manager) { for(int i = 0; i < MAX_LISTEN; i++) { manager->client[i] = -1; manager->port[i] = 0; memset(manager->ip, 0, sizeof(manager->ip)); } } int main(int argc, char *argv[]) { // 1、建立套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd == -1) { perror("socket fail"); return -1; } // 2、设置端口复用(推荐) int optval = 1; // 这里设置为端口复用,所以随便写一个值 int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); if(ret == -1) { perror("setsockopt fail"); close(sockfd); return -1; } // 3、绑定自己的IP地址和端口号 struct sockaddr_in server_addr = {0}; socklen_t addr_len = sizeof(struct sockaddr); server_addr.sin_family = AF_INET; // 指定协议为IPV4地址协议 server_addr.sin_port = htons(SERVER_PORT); // 端口号 // server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // IP地址 ret = bind(sockfd, (struct sockaddr *)&server_addr, addr_len); if(ret == -1) { perror("bind fail"); close(sockfd); return -1; } // 4、设置监听 ret = listen(sockfd, MAX_LISTEN); if(ret == -1) { perror("listen fail"); close(sockfd); return -1; } // 5、多路复用的准备工作 fd_set client_set, active_set; // (1)、清空活跃的文件描述符集合 FD_ZERO(&active_set); // (2)、把服务器的套接字文件描述符加入到活跃的文件描述符集合中 FD_SET(sockfd, &active_set); // (3)、初始化活跃集合中最大的文件描述符 int maxfd = sockfd; // (4)、初始化能接受的活跃客户端管理类 struct ClientManager manager; client_manager_init(&manager); uint16_t port = 0; // 新的客户端端口号 char ip[20] = {0}; // 新的客户端IP struct sockaddr_in client_addr; // 新的客户端地址 char recv_msg[128] = {0}; // 用来接收客户端的数据 printf("wait client...\n"); while(1) { client_set = active_set; // 先备份活跃的集合 // 6、多路复用,同时监听多个文件描述符状态,阻塞等待 int num = select(maxfd+1, &client_set, NULL, NULL, NULL); if(num == -1) { perror("select fail"); close(sockfd); return -1; } // 如果监听文件描述符发生变化,说明一定有新的客户端连接上来 if(FD_ISSET(sockfd, &client_set)) { int new_client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len); if(new_client_fd == -1) { perror("accept fail"); continue; } else { // 打印连接的客服端IP和端口号 memset(ip, 0, sizeof(ip)); strcpy(ip, inet_ntoa(client_addr.sin_addr)); port = ntohs(client_addr.sin_port); printf("[%s:%d] connect\n", ip, port); // 把新的客户端套接字加入到活跃的集合中 FD_SET(new_client_fd, &active_set); // 更新最大活跃文件描述符 if(maxfd < new_client_fd) { maxfd = new_client_fd; } // 把新的套接字加入到空的活跃客户端套接字数组 for(int i = 0; i < MAX_LISTEN; i++) { if(manager.client[i] == -1) { manager.client[i] = new_client_fd; manager.port[i] = port; strcpy(manager.ip[i], ip); break; } } // 如果只有服务器的套接字发生变化,新的套接字没有发送数据 // 那就继续监听,否则需要打印套接字的信息 if(--num == 0) { continue; } } } // 如果客服端发送数据过来 for(int i = 0; i < MAX_LISTEN; i++) { if(manager.client[i] == -1) { continue; } // 如果活跃的客户端有发送数据,注意这里要采用client_set,而不是active_set,否则会读取不了数据 if(FD_ISSET(manager.client[i], &client_set)) { // 接收数据 memset(recv_msg, 0, sizeof(recv_msg)); ret = recv(manager.client[i], recv_msg, sizeof(recv_msg), 0); memset(ip, 0, sizeof(ip)); strcpy(ip, inet_ntoa(client_addr.sin_addr)); port = ntohs(client_addr.sin_port); if(ret == 0) { printf("[%s:%d] disconnect\n", manager.ip[i], manager.port[i]); FD_CLR(manager.client[i], &active_set); // 清空对应活跃集合的套接字 manager.client[i] = -1; manager.port[i] = 0; memset(manager.ip[i], 0, sizeof(ip)); // 需要重新更新活跃集合中最大的文件描述符 maxfd = sockfd; for(int j = 0; j < MAX_LISTEN; j++) { if(manager.client[j] != -1 && maxfd < manager.client[j]) { maxfd = manager.client[j]; } } } else if(ret > 0) { printf("[%s:%d] send data: %s\n", manager.ip[i], manager.port[i], recv_msg); } // 如果所有发生变化的套接字都已经处理完成 if(--num == 0) { break; } } } } close(sockfd); return 0; }
2、 采用select函数,完成多路复用UDP服务器的通信演示,用nc命令来模拟客户端
// 多路复用TCP服务器的案例 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <errno.h> #define MAX_LISTEN FD_SETSIZE // 最大能处理的连接数, 1024 #define SERVER_IP "192.168.64.128" // 记得改为自己IP #define SERVER_PORT 20000 // 不能超过65535,也不要低于1000,防止端口误用 int main(int argc, char *argv[]) { // 1、建立套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if(sockfd == -1) { perror("socket fail"); return -1; } // 2、设置端口复用(推荐) int optval = 1; // 这里设置为端口复用,所以随便写一个值 int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); if(ret == -1) { perror("setsockopt fail"); close(sockfd); return -1; } // 3、绑定自己的IP地址和端口号 struct sockaddr_in server_addr = {0}; socklen_t addr_len = sizeof(struct sockaddr); server_addr.sin_family = AF_INET; // 指定协议为IPV4地址协议 server_addr.sin_port = htons(SERVER_PORT); // 端口号 // server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // IP地址 ret = bind(sockfd, (struct sockaddr *)&server_addr, addr_len); if(ret == -1) { perror("bind fail"); close(sockfd); return -1; } // 4、多路复用的准备工作 fd_set client_set, active_set; // (1)、清空活跃的文件描述符集合 FD_ZERO(&active_set); // (2)、把服务器的套接字文件描述符加入到活跃的文件描述符集合中 FD_SET(sockfd, &active_set); // (3)、初始化活跃集合中最大的文件描述符 int maxfd = MAX_LISTEN; // (4)、初始化能接受的活跃客户端套接字数组 int client[MAX_LISTEN]; for(int i = 0; i < MAX_LISTEN; i++) { client[i] = -1; // 空的置为-1,活跃的置为对应的文件描述符 } uint16_t port = 0; // 新的客户端端口号 char ip[20] = {0}; // 新的客户端IP struct sockaddr_in client_addr; // 新的客户端地址 char recv_msg[128] = {0}; // 用来接收客户端的数据 printf("wait client...\n"); while(1) { client_set = active_set; // 先备份活跃的集合 // 5、多路复用,同时监听多个文件描述符状态,阻塞等待 int num = select(maxfd+1, &client_set, NULL, NULL, NULL); if(num == -1) { perror("select fail"); close(sockfd); return -1; } else { // 接收数据 memset(recv_msg, 0, sizeof(recv_msg)); ret = recvfrom(sockfd, recv_msg, sizeof(recv_msg), 0, (struct sockaddr*)&client_addr, &addr_len); memset(ip, 0, sizeof(ip)); strcpy(ip, inet_ntoa(client_addr.sin_addr)); port = ntohs(client_addr.sin_port); printf("[%s:%d] send data: %s\n", ip, port, recv_msg); } } close(sockfd); return 0; }
注:TCP和UDP的代码有所不同,多路复用监听方式有所不同。
七、总结
多路复用适用于处理连接的客户端的数量小于1024的场景,当然你可以改,让其超过1024限制,这里不做讨论。多路复用模型TCP服务器跟简单的TCP服务器通信流程很像,就是在接收客户端时要采用select要进行操作。一般情况下,不采用多路复用select结合UDP协议使用,但是不代表不行,案例给出了演示。