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

cpp 复制代码
// 多路复用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、集合操作

cpp 复制代码
// 把文件描述符集合里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命令来模拟客户端

cpp 复制代码
// 多路复用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命令来模拟客户端

cpp 复制代码
// 多路复用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协议使用,但是不代表不行,案例给出了演示。

相关推荐
虾..8 小时前
Linux 软硬链接和动静态库
linux·运维·服务器
Evan芙8 小时前
Linux常见的日志服务管理的常见日志服务
linux·运维·服务器
晨晖29 小时前
单链表逆转,c语言
c语言·数据结构·算法
hkhkhkhkh12310 小时前
Linux设备节点基础知识
linux·服务器·驱动开发
HZero.chen11 小时前
Linux字符串处理
linux·string
张童瑶11 小时前
Linux SSH隧道代理转发及多层转发
linux·运维·ssh
汪汪队立大功12311 小时前
什么是SELinux
linux
石小千11 小时前
Linux安装OpenProject
linux·运维
柏木乃一11 小时前
进程(2)进程概念与基本操作
linux·服务器·开发语言·性能优化·shell·进程
Lime-309011 小时前
制作Ubuntu 24.04-GPU服务器测试系统盘
linux·运维·ubuntu