【Linux】多路IO复用技术①——select详解&如何使用select在本地主机实现简易的一对多服务器(附图解与代码实现)

这一篇的篇幅可能有点长,但真心希望大家能够静下心来看完,相信一定会有不小的收获。那么话不多说,我们这就开始啦!!!

目录

一对一服务器中的BUG

如何实现简易的一对多服务器

实现简易一对多服务器的大体步骤

每个步骤的具体流程

1.网络初始化

2.启动监听,等待socket相关事件

多路IO复用技术之select模型

3.监听到相关事件 , 辨别是服务器socket还是客户端socket并进行处理

本地主机实现简易一对多服务器的项目实现

项目构成

结果演示


一对一服务器中的BUG

我们先来看一段我之前写的程序——功能:实现一对一的单进程服务器

大家可以直接看该程序的while循环,来看一下该部分有哪些BUG

/*************************************************************************
        > File Name: nan_server.c
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月20日 星期五 13时59分10秒
 ************************************************************************/
 
#include
#include
#include
#include
#include
#include
#include
 
//定义一个开关,用于决定服务器是否开启,默认为开启状态
#define SERVER_SWITCH 1
 
 
int main()
{
    //1.分别定义服务端与客户端的网络信息结构体
    struct sockaddr_in server_addr , client_addr;
    bzero(&server_addr , sizeof(server_addr));
    bzero(&client_addr , sizeof(client_addr));
    //定义一个读写缓冲区与一个存放客户端IP的缓冲区
    char rw_buffer[1500];
    char client_IP[16];
    bzero(rw_buffer , sizeof(rw_buffer));
    bzero(client_IP , sizeof(client_IP));
    //2.对服务端网络信息结构体进行初始化
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(6060);
    //server_addr.sin_addr.s_addr = inet_addr("192.0.0.1");
    server_addr.sin_addr.s_addr = inet_addr("本地主机IPV4地址");
    //3.创建套接字,该套接字起到监听与传输信息的作用
    int server_sockfd = socket(AF_INET , SOCK_STREAM , 0);
    if(server_sockfd == -1)
    {
        perror("server socket call failed!\n");
        exit(-1);
    }
    //4.将IP地址与端口号绑定到监听套接字上
    int bind_result = bind(server_sockfd , (struct sockaddr*)&server_addr , sizeof(server_addr));
    if(bind_result == -1)
    {
        perror("server bind call failed!\n");
        exit(-1);
    }
    printf("server wait connect!\n");//日志打印,可帮助理解程序执行逻辑
    //5.监听是否有TCP链接
    int backlog = 128;
    listen(server_sockfd , backlog);
    socklen_t addrlen;
    int client_sockfd;
    while(SERVER_SWITCH)
    {
        printf("已进入循环!\n");//日志打印,可帮助理解程序执行逻辑
        addrlen = sizeof(client_addr);
        //6.如果接收成功,返回对应的文件描述符,并执行以下程序
        if( (client_sockfd = accept(server_sockfd , (struct sockaddr*)&client_addr , &addrlen)) > 0)
        {   
            printf("accept call success!\n");
            //将网络信息结构体中的大端序IP转为字符串IP并放到读写缓冲区中
            inet_ntop(AF_INET , &(client_addr.sin_addr.s_addr) , client_IP , sizeof(client_IP));
            printf("client_IP = %s\n" , client_IP);//日志打印,帮助检测是否写入IP地址
            sprintf(rw_buffer , "Hello , %s , welcome connect nan_server\n" , client_IP);
            printf("读写缓冲区中内容为 %s\n" , rw_buffer);//日志打印,帮助检测是否写入要发送的数据
            //将读写缓冲区中的内容发送到服务端的套接字中,由套接字向客户端发送数据
            send(client_sockfd , rw_buffer , sizeof(rw_buffer) , MSG_NOSIGNAL);
            //清空读写缓冲区与存放IP的缓冲区,以供下一次使用
            bzero(rw_buffer , sizeof(rw_buffer));                                      
            bzero(client_IP , sizeof(client_IP));
            //读取客户端发来的数据
            recv(client_sockfd , rw_buffer , sizeof(rw_buffer) , 0);
            printf("client_message : %s\n" , rw_buffer);
            bzero(rw_buffer , sizeof(rw_buffer));
        }
        else if(client_sockfd == -1)
        {
            perror("accept call failed!\n");
            continue;
        }
    }
    close(server_sockfd);
}

如果大家看不出来的话,再给大家一点提示:这些BUG都和阻塞与socket缓冲区有关

现在来公布一下答案吧,这个服务器的BUG在于——

  1. 如果一直没有客户端向服务器发起TCP链接请求,socket缓冲区中没有表示链接请求的标志数据SYN可读取,服务器的程序就会一直阻塞在accept函数那里,无法执行其他程序,整个服务器一直处于阻塞等待状态
  2. recv函数的调用,由于服务器使用阻塞状态的recv函数,如果客户端迟迟不发送信息,socket缓冲区就会一直为空,整个服务器就会一直阻塞等待该客户端发送数据,无法处理其他客户端的链接请求

那么我们要怎么处理这些BUG呢?

其实看完上面的BUG,大家或多或少都能明白这两个BUG的本质——其实无非就是,由于socket缓冲区中没有数据,可accept函数和recv函数却一直在等待数据的读取,导致服务器一直阻塞等待数据的读取

要想解决这两个BUG,其实也很简单——我们需要一个类似信号的功能,当socket缓冲区中有数据,触发相关事件,需要服务器进行处理时,我们再去进行处理,socket缓冲区中没有数据时,服务器不要一直阻塞等待数据

PS:socket的相关事件共分三种——1.读事件、2.写事件、3.异常事件

如何实现简易的一对多服务器

实现简易一对多服务器的大体步骤

  1. 网络初始化
  2. 启动监听,等待socket相关事件(也就是查看socket缓冲区中是否有数据需要处理)
  3. 监听到相关事件辨别是服务器socket还是客户端socket并进行处理

PS:处理socket相关事件这里要分两类——

  1. 服务器套接字接收到客户端的TCP链接请求,调用accept函数
  2. 服务器中为该客户端创建的套接字接收到客户端发来的信息,调用recv函数

每个步骤的具体流程

1.网络初始化

网络初始化这个步骤就不多做介绍了,就是简单的初始化网络信息结构体、创建服务器套接字、绑定套接字等操作

不会的同学可以去看一下我之前写的这篇博客,相关函数与使用方法都在里面:【Linux】如何在本地主机实现简易的一对一服务器(附图解与代码实现)icon-default.png?t=N7T8http://t.csdnimg.cn/thQS8

2.启动监听,等待socket相关事件

既然是要实现一对多的服务器,就代表着我们要为每个链接的客户端分别创建对应的套接字,同时这也就意味着我们自然也要去监听这些套接字

多路IO复用技术这么名字听起来很高大上,其实本质上就是一个IO事件监听技术,也就是一次性可以监听多个socket,来判断这些socket中是否有数据需要处理并反馈给服务器。所以在这个过程中我们也就要用到该技术中的一种——select

接下来我们来讲解一下select的原理实现与相关函数

多路IO复用技术之select模型

原理实现

select中有一个集合,叫做监听集合,我们可以将需要监听的套接字放入套接字文件描述符表中,由该集合负责帮我们监听该文件描述符表中这些套接字文件描述符对应的这些套接字的缓冲区中是否有数据需要处理

这个监听集合的大小为1024(固定大小,不可改),但需要注意的是,虽然这个集合的大小为1024,但实际能帮我们监听的客户端套接字只有1020个,因为前1-3个分别用于监听标准输入、标准输出和标准出错,第四个用于存放服务器套接字

可能只用文字描述过于抽象了,大家可以看下面的这个图来帮助理解

【Linux】多路IO复用技术①——select详解&如何使用select在本地主机实现简易的一对多服务器(附图解与代码实现)_第1张图片

通过这个监听集合,我们就可以实现对多个socket的同时监听

这时候可能就有同学好奇了,监听?他咋监听啊?是什么高级手段吗?

其实这个监听真的是一种很朴实无华的方法,就是遍历,一次次的遍历,当监听集合完成一遍遍历,发现有套接字处于就绪状态,也就是某些套接字的缓冲区中有数据需要处理时,他就会传出一个就绪码(处于就绪状态的套接字数量)和一个就绪集合(就绪的套接字的位码置1,未就绪的套接字的位码置0)

还是画个图来帮助大家理解

【Linux】多路IO复用技术①——select详解&如何使用select在本地主机实现简易的一对多服务器(附图解与代码实现)_第2张图片

以上就是select模型的相关原理了,接下来我们来讲一讲相关函数

以下函数的头文件都是#include

介绍一下一会会用到的变量:

  • fd_set set ; //创建监听集合
  • int sockfd ; //套接字文件描述符
  • int max_fd ; //套接字文件描述符表中的描述符个数
  • struct timeval *timeout ; //时间结构体,在这里表示工作模式——1.阻塞、2.非阻塞、3.定时阻塞(非阻塞与定时阻塞需要设置该结构体)

PS : timeout中有两个成员,一个表示秒(timeout.tv_sec),一个表示微秒(timeout.tv_usec)

struct timeval
{
__time_t  tv_sec;        /* Seconds. */
__suseconds_t  tv_usec;  /* Microseconds. */
};
  • timeout = NULL 就表示阻塞监听
  • timeout.tv_sec = 0 、timeout.tv_sec = 0 就表示非阻塞监听
  • timeout.tv_sec = 4、timeout.tv_usec = 30 就表示阻塞4秒30微秒,之后不阻塞
函数 功能 返回值
FD_ZERO(&set); 初始化监听集合,将所有位的位码都初始化为0 因为它们只是对文件描述符集合进行操作,而不是返回任何值,所以他们的返回值都是void
FD_SET(sockfd , &set); 将set集合中与sockfd对应位的位码设置为1 因为它们只是对文件描述符集合进行操作,而不是返回任何值,所以他们的返回值都是void
FD_CLR(sockfd , &set); 将set集合中与sockfd对应位的位码设置为0 因为它们只是对文件描述符集合进行操作,而不是返回任何值,所以他们的返回值都是void
FD_ISSET(sockfd , &set); 获取set集合中与sockfd对应位的位码 0或1
int select(max_fd, 是否监听读事件 , 是否监听写事件 , 是否监听错误事件 , timeout); 监听我们要求的文件描述符的状态变化情况,并通过返回值告知(PS:想监听对应时间就传入&set,不想就传NULL) 返回处于就绪状态的套接字数量

3.监听到相关事件 , 辨别是服务器socket还是客户端socket并进行处理

上面的就绪集合还需要我们自己去遍历,从而找到哪些套接字需要进行数据处理,并辨别是服务器套接字还是客户端套接字

如果是服务器套接字,就说明是有客户端向服务器发送了TCP链接请求,有以下步骤需要执行:

  1. 调用accept函数进行链接并获取与该客户端对应的套接字文件描述符
  2. 将其放入套接字文件描述符存放数组(由于该服务器为单进程,所以我们需要建立一个数组来存放这些客户端套接字文件描述符)(这个地方不太懂的话别着急,结合代码来看一定会让你豁然开朗)
  3. 将该套接字文件描述符放入套接字文件描述符表中,来让监听集合对该套接字进行监听

如果是客户端套接字,就说明是客户端向服务器发送了数据,有以下步骤需要执行:

  1. 调用recv函数读取套接字缓冲区中的数据
  2. 根据客户端发来的数据进行相应处理

具体过程如下图所示:

【Linux】多路IO复用技术①——select详解&如何使用select在本地主机实现简易的一对多服务器(附图解与代码实现)_第3张图片

本地主机实现简易一对多服务器的项目实现

项目构成

该项目由几个程序共同组成,分别是以下几个:

  • func_2th_parcel.h:定义二次包裹的函数名
  • func_2th_parcel.c:对网络初始化相关的函数进行二次包裹
  • select_server.c:使用select模型的服务器程序
  • client.c:客户端程序
/*************************************************************************
        > File Name: func_2th_parcel.h
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月18日 星期三 18时32分22秒
 ************************************************************************/

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

//socket函数的二次包裹
int SOCKET(int domain , int type , int protocol);

//bind函数的二次包裹
int BIND(int sockfd , const struct sockaddr* addr , socklen_t  addrlen);

//listen函数的二次包裹
int LISTEN(int sockfd , int backlog);

//send函数的二次包裹
ssize_t SEND(int sockfd , const void* buf , size_t len , int flags);

//recv函数的二次包裹
ssize_t RECV(int sockfd , void* buf , size_t len , int flags);

//connect函数的二次包裹
int CONNECT(int sockfd , const struct sockaddr* addr , socklen_t addrlen);

//accept函数的二次包裹
int ACCEPT(int sockfd , struct sockaddr* addr , socklen_t addrlen);

//网络初始化函数
int SOCKET_NET_CREATE(const char* ip , int port);

//服务端与客户端建立连接并返回客户端套接字文件描述符
int SERVER_ACCEPTING(int server_fd);
/*************************************************************************
        > File Name: func_2th_parcel.c
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月18日 星期三 18时32分42秒
 ************************************************************************/

#include 

int SOCKET(int domain , int type , int protocol){
    int return_value;
    if((return_value = socket(domain , type , protocol)) == -1){
        perror("socket call failed!\n");
        return return_value;
    }
    return return_value;
}

int BIND(int sockfd , const struct sockaddr* addr , socklen_t addrlen){
    int return_value;   
    if((return_value = bind(sockfd , addr , addrlen)) == -1){
        perror("bind call failed!\n");
        return return_value;
    }                      
    return return_value;   
}

int LISTEN(int sockfd , int backlog){
    int return_value;   
    if((return_value = listen(sockfd , backlog)) == -1){
        perror("listen call failed!\n");
        return return_value;
    }                      
    return return_value;   
}

ssize_t SEND(int sockfd , const void* buf , size_t len , int flags){
    ssize_t return_value;
    if((return_value = send(sockfd , buf , len , flags)) == -1){
        perror("send call failed!\n");
        return return_value;
    }
    return return_value;
}

ssize_t RECV(int sockfd , void* buf , size_t len , int flags){
    ssize_t return_value;   
    if((return_value = recv(sockfd , buf , len , flags)) == -1){
        perror("recv call failed!\n");
        return return_value;
    }                      
    return return_value;   
}

int CONNECT(int sockfd , const struct sockaddr* addr , socklen_t addrlen){
    int return_value;   
    if((return_value = connect(sockfd , addr , addrlen)) == -1){
        perror("connect call failed!\n");
        return return_value;
    }                      
    return return_value;   
}

int ACCEPT(int sockfd , struct sockaddr* addr , socklen_t addrlen){
    int return_value;   
    if((return_value = accept(sockfd , addr , &addrlen)) == -1){
        perror("accept call failed!\n");
        return return_value;
    }                      
    return return_value;   
}

int SOCKET_NET_CREATE(const char* ip , int port){
    int sockfd;
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    inet_pton(AF_INET , ip , &addr.sin_addr.s_addr);
    sockfd = SOCKET(AF_INET , SOCK_STREAM , 0);
    BIND(sockfd , (struct sockaddr*)&addr , sizeof(addr));
    LISTEN(sockfd , 128);
    return sockfd;
}

int SERVER_ACCEPTING(int server_fd)
{
    int client_sockfd;
    struct sockaddr_in client_addr;
    char client_ip[16];
    char buffer[1500];
    bzero(buffer , sizeof(buffer));
    bzero(&client_addr , sizeof(client_addr));
    socklen_t addrlen = sizeof(client_addr);
    client_sockfd = ACCEPT(server_fd , (struct sockaddr*)&client_addr , addrlen);
    bzero(client_ip , 16);
    //将客户端的IP地址转成CPU可以识别的序列并存储到client_ip数组中
    inet_ntop(AF_INET , &client_addr.sin_addr.s_addr , client_ip , 16);
    sprintf(buffer , "Hi , %s welcome tcp test server service..\n" , client_ip);
    printf("client %s , %d , connection success , client sockfd is %d\n" , client_ip , ntohs(client_addr.sin_port) , client_sockfd);
    SEND(client_sockfd , buffer , strlen(buffer) , MSG_NOSIGNAL);
    return client_sockfd;
}

/*************************************************************************
        > File Name: select_server.c
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月25日 星期三 18时53分30秒
 ************************************************************************/

#include 

int main(void)
{
    //一、进行网络初始化
    int server_sockfd;//服务器套接字文件描述符
    int max_fd;//套接字文件描述符表中的描述符个数
    int client_sockfd_array[1020];//存放客户端套接字文件描述符的数组  
    int client_sockfd;//客户端套接字文件描述符
    int ready_num = 0;//获取处于就绪状态的套接字数目
    char rw_buffer[1500];//读写缓冲区
    int flag;
    int recv_len = 0;//客户端发来的数据长度
    memset(client_sockfd_array , -1 , sizeof(client_sockfd_array));//将套接字数组每一位都置为-1,方便后面查找就绪套接字
    bzero(rw_buffer , sizeof(rw_buffer));
    fd_set listen_set , ready_set;//监听集合,就绪集合
    server_sockfd = SOCKET_NET_CREATE("192.168.79.128" , 6060);//初始化服务器套接字
    max_fd = server_sockfd;//初始化最大套接字数目
    //初始化监听集合,将server_sockfd设置为监听套接字
    FD_ZERO(&listen_set);
    FD_SET(server_sockfd , &listen_set);
    printf("select_server wait TCP connect\n");
    //二、启动监听,等待socket相关事件
    while(1)
    {
        ready_set = listen_set;
        //阻塞等待socket相关事件
        if((ready_num = select(max_fd + 1 , &ready_set , NULL , NULL , NULL)) == -1)
        {
            perror("select call failed\n");
            exit(0);
        }

        //三、监听到相关事件 , 辨别是服务器socket还是客户端socket并进行处理
        while(ready_num)
        {
            //辨别就绪,如果是服务端套接字就绪
            if(FD_ISSET(server_sockfd , &ready_set))
            {
                client_sockfd = SERVER_ACCEPTING(server_sockfd);//与客户端建立TCP链接
                FD_SET(client_sockfd , &listen_set);//将该套接字放入监听集合中
                //如果max_fd小于客户端套接字返回的描述符,说明这个新的客户端套接字放到了最后一位,max_fd需要加1
                if(max_fd < client_sockfd)
                {
                    max_fd = max_fd + 1;
                }
                for(int i = 0 ; i < 1020 ; i++)
                {
                    //将该客户端套接字,放到数组中有空缺的地方
                    if(client_sockfd_array[i] == -1)
                    {
                        client_sockfd_array[i] = client_sockfd;
                        break;
                    }
                }
                //将就绪集合中服务器套接字这一位的位码置为0,因为如果ready_num > 1,不做该处理服务器会一直认为是客户端发送了TCP链接请求,从而导致错误处理
                FD_CLR(server_sockfd , &ready_set);
            }
            //如果是客户端套接字就绪
            else
            {
                for(int i = 0 ; i < 1020 ; i++)
                {
                    //检测存放的客户端套接字是否处于就绪状态
                    if(client_sockfd_array[i] != -1)
                    {
                        //如果该套接字处于就绪状态
                        if(FD_ISSET(client_sockfd_array[i] , &ready_set))
                        {
                            recv_len = RECV(client_sockfd_array[i] , rw_buffer , sizeof(rw_buffer) , 0);//获取数据长度
                            printf("客户端%d 发来数据 : %s , 现在进行处理\n" , client_sockfd_array[i] , rw_buffer);
                            flag = 0;
                        }
                        //如果recv_len = 0,就说明与客户端套接字对应的客户端退出了,将对应客户端套接字移出套接字存储数组与监听集合
                        if(recv_len == 0)
                        {
                            printf("客户端%d 已下线\n" , client_sockfd_array[i]); 
                            FD_CLR(client_sockfd_array[i] , &ready_set);
                            client_sockfd_array[i] = -1;
                            break;
                        }
                        //进行业务处理:小写字母转大写字母
                        while(recv_len > flag)
                        {
                            rw_buffer[flag] = toupper(rw_buffer[flag]);
                            flag++;
                        }
                        SEND(client_sockfd_array[i] , rw_buffer , recv_len , MSG_NOSIGNAL);
                        printf("已向客户端%d 发送处理后的数据 : %s\n" , client_sockfd_array[i] , rw_buffer);
                        bzero(rw_buffer , sizeof(rw_buffer));
                        recv_len = 0;
                        FD_CLR(client_sockfd_array[i] , &ready_set);
                        break;
                    }
                }
            }
            ready_num--;
        }
    }
    close(server_sockfd);
    printf("server shutdown\n");
    return 0;  
}
/*************************************************************************
        > File Name: client.c
        > Author: Nan
        > Mail: **@qq.com
        > Created Time: 2023年10月19日 星期四 18时29分12秒
 ************************************************************************/

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

//服务器实现大小写转换业务

int main()
{
    //1.定义网络信息结构体与读写缓冲区并初始化
    struct sockaddr_in dest_addr;
    char buffer[1500];
    bzero(&dest_addr , sizeof(dest_addr));
    bzero(buffer , sizeof(buffer));
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(6060);
    //字符串ip转大端序列
    inet_pton(AF_INET , "192.168.79.128" , &dest_addr.sin_addr.s_addr);
    int sockfd = socket(AF_INET , SOCK_STREAM , 0);
    int i;
    //2.判断连接是否成功
    if((connect(sockfd , (struct sockaddr*) &dest_addr , sizeof(dest_addr))) == -1)
    {
        perror("connect failed!\n");
        exit(0);
    }
    recv(sockfd , buffer , sizeof(buffer) , 0);
    printf("%s" , buffer);
    bzero(buffer , sizeof(buffer));
    //3.循环读取终端输入的数据
    while( (fgets(buffer , sizeof(buffer) , stdin) ) != NULL)
    {
        i = strlen(buffer);
        buffer[i-1] = '\0';
        //向服务端发送消息
        send(sockfd , buffer , strlen(buffer) , MSG_NOSIGNAL);
        //接收服务端发来的消息
        recv(sockfd , buffer , sizeof(buffer) , 0);
        //打印服务端发来的信息
        printf("response : %s\n" , buffer);
        //清空读写缓冲区,以便下一次放入数据
        bzero(buffer , sizeof(buffer));
    }
    //4.关闭套接字,断开连接
    close(sockfd);
    return 0;
}

结果演示

【Linux】多路IO复用技术①——select详解&如何使用select在本地主机实现简易的一对多服务器(附图解与代码实现)_第4张图片

以上就是本篇博客的全部内容了,大家有什么地方没有看懂的话,可以在评论区留言给我,咱要力所能及的话就帮大家解答解答

今天的学习记录到此结束啦,咱们下篇文章见,ByeBye!

你可能感兴趣的:(Linux,linux,服务器,学习,网络)