上文我们采用了IO阻塞+多线程解决了服务器多并发问题,但是,为每一个用户服务都需要开启一个线程,开销仍然是巨大的,那么有没有一种方法,能使一个线程处理多个客户呢?这就需要用到我们的多路复用机制了。
多路复用机制实现一般有select、poll、epoll三种。
对于它们的原理可以概述为将客户端连接的套接字存在各自相对应的容器中,不断对容器轮询查看放入的套接字状态。
(一)select
int select(int nfds, fd_set readfds, fd_set writefds,fd_set exceptfds, struct timeval timeout);
nfds:检测set中套接字的最大数量,应为你所放入的socket max+1;
参数2,3,4所对应的set分别为检测可读可写以及出错的socket集合,
最后一个参数设置检测等待时间,设为0表示立即返回,NULL时表示无限等待直到有套接字状态变化发生。
fd_set结构体实则为一个long数组,采用bit-map技术,对每一个套接字在对应的bit位上赋值。若相应的状态被检测到,则会将对应bit位设为1。下面介绍它的操作函数
FD_SET(int fd,fd_set
set):将某个套接字放入set中(即对应bit位设为1)
FD_ZERO(fd_set
set):初始化set(将set全置为0)
FD_CRL(int fd,fd_set
set):将放入set中的某个套接字删除(相应bit位置为0)
FD_ISSET(int fd,fd_set
set):检测某个套接字是否在set中(相应bit位是否置为1)

服务端
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <poll.h>

unsigned short server_Port = 1234; // 服务器端口
const char *IP = "192.168.80.132"; // 服务器IP

typedef struct client_info
{
    struct sockaddr_in client_Addr;
    int conn_Scoket;
} client_info;

/*字母转大写*/
void upper(char *buf)
{
    char *pointer = buf;
    while (*pointer != '\n')
    {
        if (*pointer >= 'a' && *pointer <= 'z')
            *pointer -= 32;
        pointer++;
    }
    return;
}

int main()
{
    // 创建套接字
    int server_Socket = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_Addr;
    server_Addr.sin_family = AF_INET;
    server_Addr.sin_port = htons(server_Port);
    int flag = inet_aton(IP, &(server_Addr.sin_addr));
    if (!flag)
    {
        perror("IP transfer failed!\n");
    }

    // 绑定服务器IP地址端口号
    bind(server_Socket, (struct sockaddr *)&server_Addr, sizeof(server_Addr));
    listen(server_Socket, 5);
    int MAXSOCKFD = server_Socket;

    fd_set readfds;                  // 设置检测套接字可读的集合
    FD_ZERO(&readfds);               // 初始化全置零
    FD_SET(server_Socket, &readfds); // 将监听套接字放入容器中

    while (1)
    {
        fd_set testfds = readfds;                                    // select每次检测后会使set改变,所以需要拿副本去检测,改变的值放入原集合
        int ret = select(MAXSOCKFD + 1, &testfds, NULL, NULL, NULL); // 非阻塞式检测文件描述符
        if (ret > 0)                                                 //检测到变化时返回变化套接字的数量
        {
            for (int i = 0; i < MAXSOCKFD + 1; i++)                  //逐个遍历set中的套接字查看状态
            {
                if (FD_ISSET(i, &testfds))
                {
                    if (i == server_Socket)
                    {
                        struct sockaddr_in client_Addr;
                        int addrlen = sizeof(client_Addr);
                        int client_Socket = accept(server_Socket, (struct sockaddr *)&client_Addr, &addrlen);
                        if (client_Socket == -1)
                        {
                            perror("accept:");
                            close(server_Socket);
                            exit(0);
                        }

                        FD_SET(client_Socket, &readfds);
                        printf("客户端: %s:%hu已连接!\n", inet_ntoa(client_Addr.sin_addr), ntohs(client_Addr.sin_port));
                        MAXSOCKFD = MAXSOCKFD < client_Socket ? client_Socket : MAXSOCKFD;//每次插入client socket后需要更新MAXFD
                    }
                    else
                    {
                        char buf[100];
                        memset(buf, '\0', 100);
                        int count = read(i, buf, 99);
                        if (count == 0)
                        {
                            printf("有客户端断开连接!\n");
                            FD_CLR(i, &readfds);
                            close(i);
                        }
                        else
                        {
                            printf("客户端消息:%s", buf);
                            upper(buf);
                            write(i, buf, 99);
                        }
                    }
                }
            }
        }
        else if (ret == -1)//负值出错立即终止,无事件发生继续查询
        {
            perror("select error:");
        }
    }
    return 0;
}

(二)poll机制
select解决了多用户处理时线程开销的问题,但由于检测的对象set自身使一个有限长度的long数组(1024bit),在用户连接的套接字数量达到上限后,难以处理庞大的用户集群。因此采用poll机制可以解决用户连接数量存在上限这一问题。
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
struct pollfd {
int fd; /文件描述符/
short events; /* 等待的需要测试事件 /
short revents; /
实际发生了的事件,也就是返回结果 */
};
前两个参数表示检测的pollfd数量,最后的参数设置同select.可以看出,poll机制采用的容器为数组存放socket,开发者可以自由设置检测的数量,理想情况下没有一定的数量上线。

服务端
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <poll.h>

unsigned short server_Port = 1234; // 服务器端口
const char *IP = "192.168.80.132"; // 服务器IP

typedef struct client_info
{
    struct sockaddr_in client_Addr;
    int conn_Scoket;
} client_info;

/*字母转大写*/
void upper(char *buf)
{
    char *pointer = buf;
    while (*pointer != '\n')
    {
        if (*pointer >= 'a' && *pointer <= 'z')
            *pointer -= 32;
        pointer++;
    }
    return;
}

int main()
{
    // 创建套接字
    int server_Socket = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_Addr;
    server_Addr.sin_family = AF_INET;
    server_Addr.sin_port = htons(server_Port);
    int flag = inet_aton(IP, &(server_Addr.sin_addr));
    if (!flag)
    {
        perror("IP transfer failed!\n");
    }

    // 绑定服务器IP地址端口号
    bind(server_Socket, (struct sockaddr *)&server_Addr, sizeof(server_Addr));
    listen(server_Socket, 5);

    struct pollfd fds[1024]; // 设置最大的检测数量
    int count = 0;           // 已放置socket检测数量
    fds[count].fd = server_Socket;
    fds[count++].events = POLLIN;
    while (1)
    {
        int ret = poll(fds, count, 0); // 非阻塞检测
        if (ret > 0)
        {
            for (int i = 0; i < count; i++)
            {
                if (fds[i].fd == server_Socket)
                {
                    if (fds[i].revents == POLLIN)
                    {
                        printf("listen entry!\n");
                        struct sockaddr_in client_Addr;
                        int addrlen = sizeof(client_Addr);
                        int client_Socket = accept(server_Socket, (struct sockaddr *)&client_Addr, &addrlen);
                        if (client_Socket == -1)
                        {
                            perror("accept:");
                            close(server_Socket);
                            exit(0);
                        }

                        fds[count].fd = client_Socket;
                        fds[count++].events = POLLIN;
                        printf("客户端: %s:%hu已连接!\n", inet_ntoa(client_Addr.sin_addr), ntohs(client_Addr.sin_port));
                    }
                }
                else
                {
                    if (fds[i].revents == POLLIN)
                    {
                        printf("用户进入读写!\n");
                        char buf[100];
                        memset(buf, '\0', 100);
                        int count = read(fds[i].fd, buf, 99);
                        if (count == 0)
                        {
                            printf("有客户端断开连接!\n");
                            close(fds[i].fd);
                        }
                        else
                        {
                            printf("客户端消息:%s", buf);
                            upper(buf);
                            write(fds[i].fd, buf, 99);
                        }
                    }
                }
            }
        }
        else if (ret == -1)
        {
            perror("poll error:");
            close(server_Socket);
            exit(0);
        }
    }
    return 0;
}

(三)epoll机制
epoll_create(int size);
创建一个文件描述符,指向存放socket的实例,可供通过该文件描述符检测容器内的socket状态。
epoll_ctl(int epoll_fd,int op,int fd,struct epoll_event* event);
epoll_fd:存放容器的文件描述符
op:对容器的操作(增删改)
fd:操作的文件描述符
event:期待的文件描述的状态
epoll_wait(int epoll_fd,struct epoll_event* events,int max,int time_out)
events:检测返回的socket存放容器
max:容器events能存放检测数
time_out:检测超时时间

服务端
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <poll.h>
#include <sys/epoll.h>

#define MAX_EVENT 20

unsigned short server_Port = 1234; // 服务器端口
const char *IP = "192.168.80.132"; // 服务器IP

typedef struct client_info
{
    struct sockaddr_in client_Addr;
    int conn_Scoket;
} client_info;

/*字母转大写*/
void upper(char *buf)
{
    char *pointer = buf;
    while (*pointer != '\n')
    {
        if (*pointer >= 'a' && *pointer <= 'z')
            *pointer -= 32;
        pointer++;
    }
    return;
}

int main()
{
    // 创建套接字
    int server_Socket = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_Addr;
    server_Addr.sin_family = AF_INET;
    server_Addr.sin_port = htons(server_Port);
    int flag = inet_aton(IP, &(server_Addr.sin_addr));
    if (!flag)
    {
        perror("IP transfer failed!\n");
    }

    // 绑定服务器IP地址端口号
    bind(server_Socket, (struct sockaddr *)&server_Addr, sizeof(server_Addr));
    listen(server_Socket, 5);

    int epoll_fd = epoll_create(1);

    struct epoll_event evep;
    evep.data.fd = server_Socket;
    evep.events = EPOLLIN | EPOLLET;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_Socket, &evep);

    struct epoll_event events[MAX_EVENT];

    while (1)
    {
        int ret = epoll_wait(epoll_fd,events,MAX_EVENT,0); // 非阻塞检测
        if (ret > 0)
        {
            for (int i = 0; i < ret; i++)
            {
                if (events[i].data.fd == server_Socket)
                {
                    if (events[i].events & POLLIN)
                    {
                        struct sockaddr_in client_Addr;
                        int addrlen = sizeof(client_Addr);
                        int client_Socket = accept(server_Socket, (struct sockaddr *)&client_Addr, &addrlen);
                        if (client_Socket == -1)
                        {
                            perror("accept:");
                            close(server_Socket);
                            exit(0);
                        }

                        evep.data.fd = client_Socket;
                        evep.events = EPOLLIN | EPOLLET;
                        epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_Socket, &evep);
                        printf("客户端: %s:%hu已连接!\n", inet_ntoa(client_Addr.sin_addr), ntohs(client_Addr.sin_port));
                    }
                }
                else
                {
                    if (events[i].events & POLLIN)
                    {
                        char buf[100];
                        memset(buf, '\0', 100);
                        int count = read(events[i].data.fd, buf, 99);
                        if (count == 0)
                        {
                            printf("有客户端断开连接!\n");
                            close(events[i].data.fd);
                            epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                        }
                        else
                        {
                            printf("客户端消息:%s", buf);
                            upper(buf);
                            write(events[i].data.fd, buf, 99);
                        }
                    }
                }
            }
        }
        else if (ret == -1)
        {
            perror("epoll error:");
            close(server_Socket);
            close(epoll_fd);
            exit(0);
        }
    }

    return 0;
}
posted on 2023-04-30 23:13  Aspirants  阅读(219)  评论(0)    收藏  举报