【Linux】13. IO多路转接之详解select(select原理、接口函数、阻塞监控、非阻塞监控、超时时间监控、监控多个文件描述符、解决socket_tcp单线程存在的问题、select优缺点)

1. select的原理

程序员将多个文件描述符以及期望的IO事件告知给select(告知给内核),让内核轮询遍历文件描述符是否产生了程序员期望的IO事件。一旦发现有某个文件描述符就绪(期望的IO事件发生了),则返回该文件描述符,让用户执行相应的事件处理。

2. select接口函数

头文件:#include <sys/select.h>
int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

2.1 nfds

nfds:使用该参数告知select函数,轮询遍历文件描述符的范围
nfds传参是:传递目前监控最大文件描述符 + 1

2.2 readfds、 writefds、exceptfds

readfds: 读事件集合
writefds:写事件集合
exceptfds:异常事件集合
分别对应程序员关心的三件事件:可读事件,可写事件,异常事件
如果程序员关心某个文件描述符的可读事件,则将文件描述符添加到可读事件集合当中去。
如果程序员关心某个文件描述符的可写事件,则将文件描述符添加到可写事件集合当中去。
如果程序员关心某个文件描述符的异常事件,则将文件描述符添加到异常事件集合当中去。
总结:如果关心某个文件描述符的某个事件,则需要将文件描述符添加到对应的事件集合当中

2.3 fd_set

fd_set:事件集合类型 - 位图

 typedef struct
 {
 		__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
 }fd_set;

__FD_SETSIZE:1024,写死在内核的代码当中的

 #define __FD_SETSIZE 1024

NFDBITS:

 #define _NFDBITS (8 * (int) sizeof(__fd_mask))

typedef long int __fd_mask;

 #define _NFDBITS (8 * (int) sizeof(long int))

细节:sizeof(long):64位windows机器下是4字节
                                 64位Linux机器下是8字节

#define __NFDBITS 8 * 8

fds_bits[1024 / (8 * 8)] ⇒ __fd_mask fds_bits[16]
结论:fds_bits这个数组的元素个数为16个,元素的类型为"__fd_mask"

     事件集合怎么用?

__fd_mask fds_bits[16];这个数组在使用的时候并不是按照数组下标访问每一个元素的使用方式,而是按照比特位(位图)的方式来进行使用。每一个比特位代表一个文件描述符。
fds_bits[16]到底有多少个比特位
总比特位的个数 = 元素个数 * 单个元素占用比特位的大小
1024 = 16 * 8 * 8
结论: 内核宏 __FD_SETSIZE的值决定了 事件集合 占用的比特位个数
1024 / (8 * 8) * 8 * 8 = 1024

每一个比特位代表一个文件描述符是什么含义
在这里插入图片描述
每一个比特位代表一个文件描述,如果程序员想让select监控某一个文件描述符的某一种关心的事件时,则只需要将文件描述符添加到对应的事件集合当中(引申的含义:将对应的位图当中相应比特位的位置置为1,则就添加成功了)
在这里插入图片描述

     如何操作事件集合(位图)

如何操作事件集合(位图)
// 将fd从事件集合set当中移除掉
void FD_CLR(int fd, fd_set *set);

// 判断fd是否在事件集合set当中,通过返回值:0:不在;1:在
int FD_ISSET(int fd, fd_set *set);

// 将文件描述符fd放到事件集合set当中
void FD_SET(int fd, fd_set *set);

// 将事件集合清空(将所有的比特位全部置为0)
void FD_ZERO(fd_set *set);

2.4 timeout

timeout:NULL:阻塞监控,一旦select监控起来,直到有文件描述符就绪才返回
                0:非阻塞监控,调用select监控,会轮询遍历一次,不管有没有文件描述符就绪,都会调用返回。不能直接传0,需要传下面的结构体。使得 tv_sec = 0;v_usec = 0;
               大于0:带有超时时间的监控
                             在超时时间范围内是阻塞等待的过程,一旦有文件描述符就绪,也就返回了
                             当超过超时时间,则不论文件描述符是否就绪,都会返回。

struct timeval
{
   long tv_sec;  /* seconds*/long tv_usec;  /* microseconds*/ 微秒
}

NULL:阻塞
0:非阻塞
      tv_sec = 0
      tv_usec = 0
带有超时时间:tv_sec和tv_usec至少有一个变量大于0

2.5 返回值

大于0:返回就绪的文件描述符个数
==0:监控超时了
-1:监控出错了

结论:select在监控成功之后,只会返回就绪的文件描述符,将未就绪的文件描述符从集合当中去除掉

在这里插入图片描述

3. select - 阻塞监控代码

#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>



//1. 监控0号文件描述符(标准输入)的可读事件,阻塞,非阻塞,带有超时时间
int main()
{
    //1. 将0号文件描述符添加到可读事件集合当中去
    //2. 监控起来,阻塞,带有超时时间
    //3. 监控成功之后,从0号文件描述符当中将内容读回来

    //定义一个事件集合fd_set
    fd_set readfds;
    //定义的这个事件集合里面不知道是0还是1,是随机的,需要初始化
    FD_ZERO(&readfds); //这个函数接受的是事件集合的地址

    FD_SET(0, &readfds); //将0号文件描述符添加到事件集合当中去

    //调用select函数进行监控
    select(0 + 1, &readfds, NULL, NULL, NULL); //第1个和第2个NULL表示可写事件和异常时间,第3个NULL表示阻塞接收

    //准备一个buf接收
    char buf[1024] = {0};
    read(0, buf, sizeof(buf) - 1); //从0号文件描述符读到buf当中来
    printf("buf is : %s\n",buf);

    return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4. select - 非阻塞监控代码

#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>

//select的非阻塞监控
int main()
{
    fd_set readfds;
    FD_ZERO(&readfds);

    //FD_SET(0, &readfds); 将这个放到循环里面

    //timeout的类型是一个timeval结构体
    struct timeval tv;
    tv.tv_sec = 0;
    tv.tv_usec = 0;

    while(1)
    {
        FD_SET(0, &readfds);

        int ret = select(0 + 1, &readfds, NULL, NULL, &tv); // 0表示非阻塞,但是不能直接写0,需要传结构体的地址,结构体中的tv_sec=0,tv_usec=0.
        if(ret < 0)
        {
            perror("select");
            return 0;
        }
        else if(ret == 0)
        {
            printf("select timeout\n");
            sleep(1);
            continue; //监控超时了,继续监控
        }

        //走到下面说明就监控成功了
        char buf[1024] = {0};
        read(0, buf, sizeof(buf) - 1);
        printf("buf is : %s\n", buf);
    }

    return 0;
}

FD_SET(0, &readfds);
放到循环里面的原因是:因为select在监控到一个文件之后,如果发现这个文件未就绪,就会将这个文件描述符从事件集合当中去除掉,就不会返回未就绪的文件描述符。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5. select - 超时时间监控代码

#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>


//select-带有超时时间的监控
int main()
{
    fd_set readfds;
    FD_ZERO(&readfds);

    while(1)
    {
        //当监控完一次时,timeval中的值就没有了,时时刻刻都需要给tv赋值,所以放到循环里面
        struct timeval tv;
        tv.tv_sec = 1; //每隔一秒去监控
        tv.tv_usec = 0;

        FD_SET(0, &readfds);

        int ret = select(0 + 1, &readfds, NULL, NULL, &tv);
        if(ret < 0)
        {
            perror("select");
            return 0;
        }
        else if(ret == 0)
        {
            printf("select timeout\n");
            continue;
        }

        char buf[1024] = {0};
        read(0, buf, sizeof(buf) - 1);
        printf("buf is : %s\n",buf);
    }

    return 0;
}

在这里插入图片描述

6. select - 监控多个文件描述符

#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


//监控多个文件描述符
//0:可读事件
//tcp侦听套接字:可读事件
//
//验证:select在返回的时候,只会返回就绪的文件描述符


int main()
{
    //创建侦听套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(listen_sock < 0)
    {
        perror("socket");
        return 0;
    }

    //绑定地址
    //绑定地址之前需要创建协议所使用地址信息结构体
    struct sockaddr_in addr; //ipv4所用的结构体是struct sockaddr_in 
    addr.sin_family = AF_INET; //地址域信息
    addr.sin_port = htons(20001); //端口
    addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //ip地址

    int ret = bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr)); //addr的类型需要强转
    if(ret < 0)
    {
        perror("bind");
        return 0;
    }

    ret = listen(listen_sock, 5);
    if(ret < 0)
    {
        perror("listen");
        return 0; //监听失败,直接退出
    }

    //以上操作客户端与服务端就可以建立三次握手
    

    fd_set readfds;
    FD_ZERO(&readfds);

    FD_SET(0, &readfds);
    FD_SET(listen_sock, &readfds); //将listen_sock文件描述符添加到可读事件集合当中
    //可读事件集合当中 有两个文件描述符
    

    while(1)
    {
        int ret = select(listen_sock + 1, &readfds, NULL, NULL, NULL);
        if(ret < 0)
        {
            perror("select");
            return 0;
        }
        else if(ret == 0)
        {
            printf("select timeout\n");
            continue;
        }
        
        if(FD_ISSET(0, &readfds)) //判断0号文件描述符是否在事件集合当中
        {
            printf("0 is in readfds\n");
            char buf[1024] = {0};
            read(0, buf, sizeof(buf) - 1);
            printf("buf is : %s\n", buf);
        }
        else 
        {
            printf("0 is not in readfds\n");
        }

        if(FD_ISSET(listen_sock, &readfds)) //判断listen_sock文件描述符是否在事件集合当中
        {
            printf("listen_sock is in readfds\n");
        }
        else 
        {
            printf("listen_sock is not in readfds\n");
        }

    }
    
    return 0;
}

在这里插入图片描述
在这里插入图片描述
验证未就绪的文件描述符是否在时间集合当中?
(1)正常监控listen_sock文件描述符的时候,使用telnet工具发起TCP连接,看看会不会触发
(2)当listen_sock不在可读事件集合当中的时候,使用telnet工具发起TCP连接,看看会不会触发
windows开启telnet的方法:
控制面板—》程序—》程序和功能—》启用或关闭Windows功能
在这里插入图片描述
在Windows下,输入cmd,进入命令提示符

(1)正常监控listen_sock文件描述符的时候,使用telnet工具发起TCP连接,看看会不会触发
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
处理这个疯狂输出,可以用accept把这个连接接收回来
在这里插入图片描述
在这里插入图片描述
accept已经把listen_sock这个可读事件处理掉了

每telnet一次,就会响应一次
在这里插入图片描述
(2)当listen_sock不在可读事件集合当中的时候,使用telnet工具发起TCP连接,看看会不会触发
在这里插入图片描述
在这里插入图片描述

怎么解决select只返回未就绪的文件描述符?
用临时变量的一种方式 fd_set tmp = readfds; ,解决了select只返回未就绪的文件描述符的问题

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>

int main()
{
    int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(listen_sock < 0)
    {
        perror("socket");
        return 0;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(20001);
    addr.sin_addr.s_addr = inet_addr("0.0.0.0");
    int ret = bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr));
    if(ret < 0)
    {
        perror("bind");
        return 0;
    }

    ret = listen(listen_sock, 5); //5是已连接完成队列的大小
    if(ret < 0)
    {
        perror("listen");
        return 0;
    }

    fd_set readfds;
    FD_ZERO(&readfds);

    FD_SET(0, &readfds);
    FD_SET(listen_sock, &readfds);

    while(1)
    {
        //每次监控的时候,将readfds重新告诉select
        fd_set tmp = readfds; //给一个临时变量,每次监控的时候,只监控这个临时变量tmp
        //当select返回tmp的时候,我又把readfds给了tmp
        //这样的话 每次都能拿到想要监控的文件描述符
        
        int ret = select(listen_sock + 1, &tmp, NULL, NULL, NULL);
        if(ret < 0)
        {
            perror("select");
            return 0;
        }
        else if(ret == 0)
        {
            printf("select timeout\n");
            continue;
        }

        if(FD_ISSET(0, &tmp))
        {
            printf("0 is in tmp\n");
            char buf[1024] = {0};
            read(0, buf, sizeof(buf) - 1);
            printf("buf is : %s\n", buf);
        }
        else 
        {
            printf("0 is not tmp\n");
        }

        if(FD_ISSET(listen_sock, &tmp))
        {
            printf("listen_sock is in tmp\n");
            accept(listen_sock, NULL, NULL);
        }
        else 
        {
            printf("listen_sock is not in tmp\n");
        }
    }
    return 0;
}

在这里插入图片描述

7. select解决TCP单线程存在的问题

7.1 TCP单线程代码

     服务端代码

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(listen_sock < 0)
    {
        perror("socket");
        return 0;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(28989);
    addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0表示本地所有的网卡地址

    int ret = bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr));
    if(ret < 0)
    {
        perror("bind");
        return 0;
    }

    ret = listen(listen_sock, 1);// 1表示内核中已完成连接队列的大小,决定了服务端的并发连接数
    if(ret < 0)
    {
        perror("listen");
        return 0;
    }

     while(1)
     {
         //接受连接
         //accept接收的是客户端的地址信息,创建客户端的地址信息结构
         struct sockaddr_in cli_addr;
         socklen_t cli_addrlen = sizeof(cli_addr);

         int newsockfd = accept(listen_sock, (struct sockaddr*)&cli_addr, &cli_addrlen);//接收新连接的返回值是新连接的套接字描述符
        if(newsockfd < 0)
        {
            perror("accept");
            continue;  //接收失败,继续接收连接
        }

        printf("accept new connect from client %s : %d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));


        //已经建立连接,此时服务端与客户端可以交互,可以发送数据,也可以接收数据
        
        //接收数据
        char buf[1024] = {0};
        ssize_t recv_size = recv(newsockfd, buf, sizeof(buf) - 1, 0); //recv函数接收的是新连接的套接字newsockfd,返回值类型是ssize_t。最后一个参数0表示阻塞接收。
        if(recv_size < 0) //函数调用出错了
        {
            perror("recv");
            continue; //继续接收
        }
        else if(recv_size == 0) //表示对端关闭连接
        {
            printf("peer close connet\n");
            close(newsockfd); //对端把连接套接字关闭掉了,自己也把对应的连接套接字关闭
            continue;  //再去接收新的连接
        }

        //下面就是recv_size > 0的情况,就是接收到的情况
        printf("buf is : %s\n", buf);

        //发送数据
        memset(buf, '\0', sizeof(buf));
        strcpy(buf,"i am server!!!\n"); //strcpy不是安全函数
        ssize_t send_size = send(newsockfd, buf, strlen(buf), 0); //最后的0表示阻塞发送
        if(send_size <= 0)
        {
            perror("send"); 
            continue;
        }
        else 
        {
            printf("发送的字节数是:%zu\n", send_size); //ssize_t 用 %zu 输出
        }
     }

    close(listen_sock);  //最后要把侦听套接字关掉

    return 0;
}

     客户端代码

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    //创建套接
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //地址域信息,套接字类型,套接字类型使用的协议
    if(sockfd < 0) //socket返回的是套接字描述符,即文件描述符。文件描述符不能小于0
    {
        perror("socket");
        return 0;
    }

    //客户端不需要绑定地址信息
    //首先需要创建协议所用的地址信息结构体
    struct sockaddr_in addr; //IPV4使用的是 struct sockaddr_in 这个结构体
    addr.sin_family = AF_INET; //地址域信息
    addr.sin_port = htons(28989); //端口,要把这个端口转换成为网络字节序
    addr.sin_addr.s_addr = inet_addr("82.157.94.99"); //IP地址转换为unit32_t,再转换为网络字节序
   
    int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));//connect接收的是服务端的地址信息结构
    if(ret < 0)
    {
        perror("connect");
        return 0;
        //continue; //如果连接失败也可以继续连接
    }

    while(1)
    {
        //发送
        char buf[1024] = "i am client111";

        send(sockfd, buf, strlen(buf), 0);

        //接收
        memset(buf, '\0', sizeof(buf)); //接收之前先初始化buf,否则会覆盖初始化之前buf里的内容。比如接收到123,就会覆盖i a,变成123m client111

        ssize_t recv_size = recv(sockfd, buf, sizeof(buf) - 1, 0);
        //发送和接收最后一位参数都是标志位,0表示阻塞发送或接收
        if(recv_size < 0)
        {
            perror("recv");
            continue;
        }
        else if(recv_size == 0)
        {
            printf("peer close connect\n");
            close(sockfd);
            continue;
        }

        printf("%s\n", buf);

        sleep(1); //每隔一秒发一次
    }

    close(sockfd);
    return 0;
}

     TCP单线程代码的问题

在单线程中实现客户端和服务端之间的交互,有两种情况,一是将accept放在while循环的里面,二是将accept放在while循环的外面。如果将accept放在while循环的里面,那么每次客户端发送数据之后,只能和服务端交互一次,然后就会阻塞在accept函数;如果将accept放在while循环的外面,那么就可以和多个客户端建立连接,但是服务端只能和同一个客户端交互,因为永远无法再执行到accept函数。

对于上面这种问题,提出了三种解决方法,方法一,使用多进程实现,方法二,使用多线程实现,方法三,使用select。

7.2 select解决tcp单线程问题的代码

     select_svr.hpp

将select封装成一个类

#pragma once 

#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
#include <vector>

using namespace std;

class SelectSvr
{
    public:
        //构造(初始化)
        SelectSvr()
        {
            max_fd_ = -1;
            FD_ZERO(&readfds_);
        }

        //析构(销毁)
        ~SelectSvr()
        {}

        //添加文件描述符到对应的事件集合(参数为需要添加的文件描述符)
        void AddFd(int fd)
        {   
            //1. 添加文件描述符到对应的可读事件集合当中
            FD_SET(fd, &readfds_);

            //2.添加完之后,文件描述符的个数就会变化 
            //  需要更新最大的文件描述符数值
            if(fd > max_fd_)
            {
                max_fd_ = fd;
            }
        }

        //去除文件描述符在事件集合当中
        void DeletFd(int fd)
        {
            //1. 将文件描述符从可读事件集合当中去除掉
            FD_CLR(fd, &readfds_);

            //2.更新最大的文件描述符数值
            //  从后往前遍历
            for(int i = max_fd_; i >= 0; i--)
            {
                if(FD_ISSET(i, &readfds_))
                {
                    max_fd_ = i;
                    break;  //如果在,就直接跳出
                }    
            }
        }


        int Select(vector<int>* vec, struct timeval* tv = NULL)
        {
            fd_set tmp = readfds_; 
            int ret = select(max_fd_ + 1, &tmp, NULL, NULL, tv);
            if(ret < 0) //监控出错了了
            {
                perror("select");
                return -1; 
            }
            else if(ret == 0)
            {
                printf("select timeout!\n");
                return 0;
            }

            //执行到下面就是正常监控到了,有可能只有一个文件描述符就绪了,也有可能有多个文件描述符就绪了
            //如果至少有一个文件描述符就绪,但是哪一个文件描述符就绪了,还不知道,需要判断
            //用 FD_ISSET 判断
            for(int i = 0; i <= max_fd_; i++)
            {
                if(FD_ISSET(i, &tmp))
                {
                    vec->push_back(i);
                }
            }
            return ret; //返回文件描述符
        }
    private:
        //保存当前最大的文件描述符数值
        int max_fd_;
        //保存可读事件集合
        fd_set readfds_;
};

     server.cpp

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "select_svr.hpp"


int main()
{
    //创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(listen_sock < 0)
    {
        perror("socket");
        return 0;
    }

    //绑定地址信息
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(28989);
    addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0表示本地所有的网卡地址

    int ret = bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr));
    if(ret < 0)
    {
        perror("bind");
        return 0;
    }

    //监听
    ret = listen(listen_sock, 1);// 1表示内核中已完成连接队列的大小,决定了服务端的并发连接数
    if(ret < 0)
    {
        perror("listen");
        return 0;
    }


    //1. 创建select对象
    SelectSvr select_svr;
    select_svr.AddFd(listen_sock); //将listen_sock添加到时间集合当中


    while(1)
    {
        vector<int> vec;
        int ret = select_svr.Select(&vec);
        if(ret < 0)
        {
            perror("select");
            return 0;
        }
        else if(ret == 0)
        {
            continue;
        }

        //正常监控到了文件描述符
        //就虚的文件描述符被放在了vec当中
        //只需要遍历整个vec,挨个处理,就会将所有的事件处理掉
        for(size_t i = 0; i < vec.size(); i++)
        {
            //需要判断,当前处理的文件描述符是侦听套接字还是新连接套接字
            if(vec[i] == listen_sock)
            {
                //侦听套接字
                struct sockaddr_in cli_addr;
                socklen_t cli_addrlen = sizeof(cli_addr);

                int newsockfd = accept(listen_sock, (struct sockaddr*)&cli_addr, &cli_addrlen);//接收新连接的返回值是新连接的套接字描述符
                if(newsockfd < 0)
                {
                    perror("accept");
                    continue;  //接收失败,继续接收连接
                }

                printf("accept new connect from client %s : %d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));

                //让select将新连接套接字也给监听起来
                select_svr.AddFd(newsockfd);
            }
            else 
            {
                //新连接的套接字
                //接收数据
                char buf[1024] = {0};
                ssize_t recv_size = recv(vec[i], buf, sizeof(buf) - 1, 0); //recv函数接收的是新连接的套接字newsockfd,返回值类型是ssize_t。最后一个参数0表示阻塞接收。
                if(recv_size < 0) //函数调用出错了
                {
                    perror("recv");
                    continue; //继续接收
                }
                else if(recv_size == 0) //表示对端关闭连接
                {
                    printf("peer close connet\n");

                    select_svr.DeletFd(vec[i]);
                    close(vec[i]); //对端把连接套接字关闭掉了,自己也把对应的连接套接字关闭
                    continue;  //再去接收新的连接
                }

                //下面就是recv_size > 0的情况,就是接收到的情况
                printf("buf is : %s\n", buf);

                //发送数据
                memset(buf, '\0', sizeof(buf));
                strcpy(buf,"i am server!!!\n"); //strcpy不是安全函数
                ssize_t send_size = send(vec[i], buf, strlen(buf), 0); //最后的0表示阻塞发送
                if(send_size <= 0)
                {
                    perror("send"); 
                    continue;
                }
                else 
                {
                    printf("发送的字节数是:%zu\n", send_size); //ssize_t 用 %zu 输出
                }
            }
        }
    }

    close(listen_sock);  //最后要把侦听套接字关掉

    return 0;
}

     client1.cpp

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    //创建套接
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //地址域信息,套接字类型,套接字类型使用的协议
    if(sockfd < 0) //socket返回的是套接字描述符,即文件描述符。文件描述符不能小于0
    {
        perror("socket");
        return 0;
    }

    //客户端不需要绑定地址信息
    //首先需要创建协议所用的地址信息结构体
    struct sockaddr_in addr; //IPV4使用的是 struct sockaddr_in 这个结构体
    addr.sin_family = AF_INET; //地址域信息
    addr.sin_port = htons(28989); //端口,要把这个端口转换成为网络字节序
    addr.sin_addr.s_addr = inet_addr("82.157.94.99"); //IP地址转换为unit32_t,再转换为网络字节序
   
    int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));//connect接收的是服务端的地址信息结构
    if(ret < 0)
    {
        perror("connect");
        return 0;
        //continue; //如果连接失败也可以继续连接
    }

    while(1)
    {
        //发送
        char buf[1024] = "i am client1";

        int send_size = send(sockfd, buf, strlen(buf), 0);
        if(send_size <= 0)
        {
            perror("send"); 
            continue;             
        }
        else                                            {
            printf("发送的字节数是:%zu\n", send_size); //ssize_t 用 %zu 输出

    }
        //接收
        memset(buf, '\0', sizeof(buf)); //接收之前先初始化buf,否则会覆盖初始化之前buf里的内容。比如接收到123,就会覆盖i a,变成123m client111

        ssize_t recv_size = recv(sockfd, buf, sizeof(buf) - 1, 0);
        //发送和接收最后一位参数都是标志位,0表示阻塞发送或接收
        if(recv_size < 0)
        {
            perror("recv");
            continue;
        }
        else if(recv_size == 0)
        {
            printf("peer close connect\n");
            close(sockfd);
            continue;
        }

        printf("%s\n", buf);

        sleep(1); //每隔一秒发一次
    }

    close(sockfd);
    return 0;
}

     client2.cpp

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    //创建套接
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //地址域信息,套接字类型,套接字类型使用的协议
    if(sockfd < 0) //socket返回的是套接字描述符,即文件描述符。文件描述符不能小于0
    {
        perror("socket");
        return 0;
    }

    //客户端不需要绑定地址信息
    //首先需要创建协议所用的地址信息结构体
    struct sockaddr_in addr; //IPV4使用的是 struct sockaddr_in 这个结构体
    addr.sin_family = AF_INET; //地址域信息
    addr.sin_port = htons(28989); //端口,要把这个端口转换成为网络字节序
    addr.sin_addr.s_addr = inet_addr("82.157.94.99"); //IP地址转换为unit32_t,再转换为网络字节序
   
    int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));//connect接收的是服务端的地址信息结构
    if(ret < 0)
    {
        perror("connect");
        return 0;
        //continue; //如果连接失败也可以继续连接
    }

    while(1)
    {
        //发送
        char buf[1024] = "i am client222";

        int send_size = send(sockfd, buf, strlen(buf), 0);
        if(send_size <= 0)
        {
            perror("send"); 
            continue;             
        }
        else                                            {
            printf("发送的字节数是:%zu\n", send_size); //ssize_t 用 %zu 输出

    }
        //接收
        memset(buf, '\0', sizeof(buf)); //接收之前先初始化buf,否则会覆盖初始化之前buf里的内容。比如接收到123,就会覆盖i a,变成123m client111

        ssize_t recv_size = recv(sockfd, buf, sizeof(buf) - 1, 0);
        //发送和接收最后一位参数都是标志位,0表示阻塞发送或接收
        if(recv_size < 0)
        {
            perror("recv");
            continue;
        }
        else if(recv_size == 0)
        {
            printf("peer close connect\n");
            close(sockfd);
            continue;
        }

        printf("%s\n", buf);

        sleep(1); //每隔一秒发一次
    }

    close(sockfd);
    return 0;
}

     client3.cpp

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    //创建套接
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //地址域信息,套接字类型,套接字类型使用的协议
    if(sockfd < 0) //socket返回的是套接字描述符,即文件描述符。文件描述符不能小于0
    {
        perror("socket");
        return 0;
    }

    //客户端不需要绑定地址信息
    //首先需要创建协议所用的地址信息结构体
    struct sockaddr_in addr; //IPV4使用的是 struct sockaddr_in 这个结构体
    addr.sin_family = AF_INET; //地址域信息
    addr.sin_port = htons(28989); //端口,要把这个端口转换成为网络字节序
    addr.sin_addr.s_addr = inet_addr("82.157.94.99"); //IP地址转换为unit32_t,再转换为网络字节序
   
    int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));//connect接收的是服务端的地址信息结构
    if(ret < 0)
    {
        perror("connect");
        return 0;
        //continue; //如果连接失败也可以继续连接
    }

    while(1)
    {
        //发送
        char buf[1024] = "i am client33333";

        int send_size = send(sockfd, buf, strlen(buf), 0);
        if(send_size <= 0)
        {
            perror("send"); 
            continue;             
        }
        else                                            {
            printf("发送的字节数是:%zu\n", send_size); //ssize_t 用 %zu 输出

    }
        //接收
        memset(buf, '\0', sizeof(buf)); //接收之前先初始化buf,否则会覆盖初始化之前buf里的内容。比如接收到123,就会覆盖i a,变成123m client111

        ssize_t recv_size = recv(sockfd, buf, sizeof(buf) - 1, 0);
        //发送和接收最后一位参数都是标志位,0表示阻塞发送或接收
        if(recv_size < 0)
        {
            perror("recv");
            continue;
        }
        else if(recv_size == 0)
        {
            printf("peer close connect\n");
            close(sockfd);
            continue;
        }

        printf("%s\n", buf);

        sleep(1); //每隔一秒发一次
    }

    close(sockfd);
    return 0;
}

在这里插入图片描述

8. select的优缺点

优点:
(1)select遵循的POSIX标准,可以跨平台。
(2)select的超时时间可以精确到微妙,struct timeval{…} 中的 tv_sec 与 tv_usec。
缺点:
(1)select采用轮询遍历的方式进行监控,随着监控的文件越来越多,监控轮询效率就会下降。
(2)select监控的文件描述符个数是有上限的,上限由内核参数__FD_SETSIZE决定,为1024。
(3)select在监控成功之后,在返回给用户事件集合当中,会将未就绪的文件描述符去除掉。导致二次监控的时候,需要程序员进行添加去除掉的文件描述符进行监控。
(4)select在监控成功之后,给用户返回的是事件集合,并没有直白的告诉用户哪一个文件描述符就绪了,需要用户自己在返回的事件集合当中进行判断。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`select` 函数是一种 I/O 多路复用的机制,用于同时监听多个文件描述符的状态变化。它可以使用单个系统调用同时监视多个文件描述符,并在有一个或多个文件描述符就绪时通知应用程序。 `select` 函数的原型如下: ```c #include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); ``` 参数说明: - `nfds`:待监视的最大文件描述符加 1。 - `readfds`:可读文件描述符集合。 - `writefds`:可写文件描述符集合。 - `exceptfds`:异常条件文件描述符集合。 - `timeout`:超时时间,如果为 `NULL` 则为阻塞模式,即一直等待直到有文件描述符就绪;如果为零时间(`tv_sec` 和 `tv_usec` 均为 0),则为非阻塞模式,即立即返回;否则为指定超时时间。 `select` 函数的返回值表示就绪文件描述符的数量,如果返回值为 0,则表示超时;如果返回值为 -1,则表示出错。 使用 `select` 函数的一般流程如下: 1. 初始化需要监视的文件描述符集合。 2. 调用 `select` 函数等待文件描述符就绪。 3. 检查返回值确定哪些文件描述符已经就绪。 4. 处理就绪的文件描述符。 5. 重复上述步骤。 需要注意的是,`select` 函数有一些限制,比如只能监视的文件描述符数量有限,一般为 1024 或更小。此外,在某些平台上,使用 `select` 函数可能会有性能上的限制,可以考虑使用更高效的机制,如 `poll` 或 `epoll`。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值