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网络编程系列之服务器编程——信号驱动模型
一、什么是非阻塞IO模型
服务器非阻塞IO模型是一种服务器处理客户端连接请求的方式。在这种模型下,服务器会采用异步IO方式,即在一个线程中进行非阻塞IO操作,以此来处理多个客户端连接请求。当一个连接请求到来时,服务器会采用非阻塞的方式进行IO操作,这样就不会阻塞其他请求的处理,从而提高服务器的并发处理能力。另外,非阻塞IO模型可以避免复杂的多线程或多进程并发模型,降低服务器编程的复杂度。
二、特性
1、异步IO方式
采用异步IO方式进行IO操作,不会阻塞其他客户端连接请求的处理。
2、单线程处理
整个服务器只使用一个线程进行客户端连接请求处理,降低了线程切换和上下文切换的开销。
3、事件驱动
采用事件驱动的方式,只有在有客户端连接请求到来时才进行处理,节省了CPU资源消耗。
4、高并发
由于使用了非阻塞IO模型,使得服务器能够同时处理大量的客户端连接请求,提高了服务器的并发处理能力。
5、低延迟
使用非阻塞IO模型可以减少等待IO操作完成的时间,降低了请求的延迟。
6、简单易用
非阻塞IO模型可以避免复杂的多线程或多进程并发模型,降低服务器编程的复杂度。
三、使用场景
1、高并发场景
服务器需要处理大量的客户端连接请求,需要提高服务器的并发处理能力。
2、低延迟场景
对于需要快速响应的应用场景,比如实时通信、游戏等,可以采用非阻塞IO模型以减少请求的延迟。
3、资源受限场景
对于资源受限的服务器,比如嵌入式设备、单片机等,采用非阻塞IO模型可以节省CPU和内存资源,提高服务器的性价比。
4、长连接场景
对于需要维持长时间连接的应用,比如推送消息、物联网等,采用非阻塞IO模型可以减少连接的等待时间。
5、单机多线程场景
对于使用多线程模型的服务器应用,由于线程切换和上下文切换的开销,可能会导致性能瓶颈,采用非阻塞IO模型可以降低这种开销。
四、模型框架(通信流程)
1、建立套接字。使用socket()
2、设置端口复用。使用setsockopt()
3、绑定自己的IP和端口号。使用bind()
4、设置监听。使用listen()
5、设置套接字为非阻塞状态。使用fcntl()
6、轮询,接收连接请求。使用accept()
7、轮询,查看活跃的客户端是否有数据到达。使用recv()
8、关闭套接字。使用close()
五、相关函数API接口
TCP通信流程常规的API那些在本系列的TCP协议里有大量展示,这里省略,详情可以点击本文开头的链接查看
1、设置套接字为非阻塞状态
// 5、设置监听套接字为非阻塞 int status = fcntl(sockfd, F_GETFL); // 获取文件描述符状态 status |= O_NONBLOCK; // 添加非阻塞状态 fcntl(sockfd, F_SETFL, status); // 设置文件描述符状态
六、案例
完成非阻塞IO模型结合TCP协议完成服务器通信演示,使用nc命令模拟客户端
// 服务器非阻塞IO的案例 #include <stdio.h> #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 <fcntl.h> #define MAX_LISTEN 50 // 最大能处理的连接数 #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]; // 客户端套接字端口号 int active_client_number; // 活跃的客户端数量 }; // 初始化客户端管理类 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)); } manager->active_client_number = 0; } int main(int argc, char *argv[]) { // 1、建立套接字,指定IPV4网络地址,TCP协议 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 = 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、设置监听套接字为非阻塞 int status = fcntl(sockfd, F_GETFL); // 获取文件描述符状态 status |= O_NONBLOCK; // 添加非阻塞状态 fcntl(sockfd, F_SETFL, status); // 设置文件描述符状态 int i; uint16_t port = 0; // 新的客户端的端口号 char ip[20] = {0}; // 新的客户端的IP char recv_msg[128] = {0}; // 接收数据缓冲区 struct sockaddr_in client_addr; // 客户端的地址 struct ClientManager manager; client_manager_init(&manager); // 初始化一个管理类 printf("wait client connect...\n"); while(1) { // 6、接受连接请求 int new_client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len); if(new_client_fd != -1) { 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); // 新连接的套接字也要设置为非阻塞状态 status = fcntl(new_client_fd, F_GETFL); // 获取文件描述符状态 status |= O_NONBLOCK; // 添加非阻塞状态 fcntl(new_client_fd, F_SETFL, status); // 设置文件描述符状态 // 把连接上来的客户端套接字加入管理类 manager.client[manager.active_client_number] = new_client_fd; manager.port[manager.active_client_number] = port; strcpy(manager.ip[manager.active_client_number], ip); manager.active_client_number++; } // 7、轮询方式查看连接上来的客户端是否有数据到达,非阻塞方式,不会等待 for(i = 0; i < manager.active_client_number;) { // 尝试接收数据 memset(recv_msg, 0, sizeof(recv_msg)); ret = recv(manager.client[i], recv_msg, sizeof(recv_msg), 0); // 客户端断开 if(ret == 0) { printf("[%s:%d] disconnet\n", manager.ip[i], manager.port[i]); for(int j = i + 1; j < manager.active_client_number; j++) { // 把活跃的套接字往前移一位 if(manager.client[j] != -1) { manager.client[j-1] = manager.client[j]; manager.port[j-1] = manager.port[j]; strcpy(manager.ip[j-1], manager.ip[j]); } } // 更新活跃的套接字数量,注意不需要i++ manager.active_client_number--; // 最后一个要清空 manager.client[manager.active_client_number] = -1; manager.port[manager.active_client_number] = 0; memset(manager.ip[manager.active_client_number], 0, sizeof(ip)); } else if(ret > 0) { printf("[%s:%d] send data: %s\n", manager.ip[i], manager.port[i], recv_msg); i++; // 这需要i++;,上面不用 } else { i++; // 这里也需要i++,没有数据时,需要询问下一个 } } } // 7、关闭套接字 close(sockfd); return 0; }
七、总结
非阻塞模型适用于资源有限的,需要高并发,低延迟的场景。非阻塞模型TCP服务器的通信流程跟普通的TCP服务器通信流程大致相同,区别在于不仅要设置服务器监听的套接字设置为非阻塞,而且要把客户端的套接字也设置为非阻塞。可以结合案例加深理解。