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

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 
#include 
#include 
#include 
#include 
#include        
#include 
#include 
#include 
#include 
#include 

#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;
}

Linux网络编程系列之服务器编程——非阻塞IO模型_第1张图片

七、总结

        非阻塞模型适用于资源有限的,需要高并发,低延迟的场景。非阻塞模型TCP服务器的通信流程跟普通的TCP服务器通信流程大致相同,区别在于不仅要设置服务器监听的套接字设置为非阻塞,而且要把客户端的套接字也设置为非阻塞。可以结合案例加深理解。

你可能感兴趣的:(C语言程序设计,Linux,c语言,linux)