• 数据的IO和复用


    一、IO函数

    1.使用recv()函数接收数据

    此函数用于接收数据,函数原型:

    #include
    #include
    //此函数从套接字s中接收数据放到缓冲区buf,buf长度为len,操作方式由flags指定。
    //参数s:套接口文件描述符,由系统调用socket()返回的。
    //参数buf:指针,指向接收网络数据的缓冲区。
    //参数s:接收缓冲区的大小,以字节为单位。
    sszie_t recv(int s,void *buf,size_t len,int flags);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    参数flags用于设置接收数据的方式,可选择的值如下所示:值可以是表中按位或生成的复合值。
    在这里插入图片描述

    1. MSG_ DONTWAIT: 这个标志将单个IO操作设为非阻塞方式,而不需要在套接字上打开非阻塞标志,执行IO操作,然后关闭非阻塞标志。

    2. MSG_ERRQUEUE: 该错误的传输依赖于所使用的协议。

    3. MSG_OOB: 这个标志可以接收带外数据,而不是接收一般数据。

    4. MSG_PEEK: 这个标志用于查看可读的数据,在recv()函数执行后,内核不会将这些数据丢弃。

    5. MSG_TRUNC: 在接收数据后,如果用户的缓冲区大小不足以完全复制缓冲区中的数据,则将数据截断,仅靠复制用户缓冲区大小的数据。其他的数据会被丢弃。

    6. MSG_WAITALL: 这个标志告诉内核在没有读到请求的字节数之前不使读操作返回。如果系统支持这个标志,可以去掉readn()函数而用下面的代替:

    #define readn(fd ,ptr,n)  recv(fd ptr,n,MSG_WAITALL)
    
    • 1

    设置了MSG_WAITALL,发生下面情况:

    1. 捕获一个信号;
    2. 连接终止;
    3. 在套接字上发生错误。
      这时函数返回的字节数仍然会比请求的少。当指定WAITALL标志时,函数会复制与用户指定的长度相等的数据,如果内核中的当前数据不能满足要求,会一直等待直到数据足够的时候才返回。

    此函数返回值是成功接收的字节数,当返回值为-1时错误发生,可通过errno获取错误码,另一方使用正常方式关闭连接的时候返回值为0,如:调用close()函数关闭连接,例:
    在这里插入图片描述

    • recv()函数通常用于TCP类型的套接字,UDP使用recvfrom()函数接收数据,当然在数据报套接字绑定地址和端口后,也可以使用 recv()函数接收数据。

      • recv()函数从内核的接收缓冲区中复制数据到用户指定的缓冲区,当内核中的数据比指定的缓冲区时, 一 般情况下(没有采用MSG_WAITALL标志)会复制缓冲区中的所有数据到用户缓冲区,并返回数据的长度。

        • 内核接收缓冲区中的数据比用户指定时,会将用户指定长度 len 的接收缓冲区中的数据复制到用户指定地址,其余的数据需要下次调用接收函数的时候再复制,内核在复制用户指定的数据之后,会销毁已经复制完毕的数据,并进行调整。

    2.使用send()函数发送数据

    send()函数用于发送数据,函数原型:

    #include
    #include
    //将缓冲区buf中大小为len的数据,通过套接字文件描述符按照flags指定的方式发送出去。
    //参数含义与recv中的含义一致,它的返回值是成功发送的字节数。
    ssize_t send(int s,const void *buf,size_t len,int flags);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 用户缓冲区buf中数据通过send()发送并不一定全部发送出去,所以要检查send()函数的返回值,按值与计划发送的字节长度len是否相等来判断如何进行下一步操作。

      • sen()函数的返回值小于len的时候,表明缓冲区仍然有部分数据没有发生成功,这时需要重新发送剩余部分的数据,通常的剩余数据发送方法是对原来buf中的数据位置进行偏移,偏移的大小为发送成功的字节数。

    send()函数发生错误的时候返回值为-1,这时可以查看errno获得错误码,用正常方式关闭连接的时候返回值为0,如:调用close()函数关闭连接,例:
    在这里插入图片描述

    1. 函数send()只能用于套接字处于连接状态的描述符,之前必须用connect()函数或者其他函数进行连接。
    2. 对于send()函数和write()函数之间的差别是表示发送方式的flag, 当flag为0时,send()函数和write()函数完全一 致。而且send(s,buf,len,flags)sendto(s,buf,len,flags, NULL,0)是等价的。

    3.使用readv()函数接收数据

    readv()函数用于接收多个缓冲区数据,函数原型如下:

    #include
    //此函数从套接字描述符s中读取count块数据放到缓冲区向量vector中。
    ssize_t readv(int s,const struct iovec *vector,int count);
    
    • 1
    • 2
    • 3

    此函数返回值为成功接收到的字节数,当为-1时错误发生,可以查看errno获得错误码:

    在这里插入图片描述
    参数vector为一个指向向量的指针,结构strcut iovec在文件中定义:

    struct iovec{
    	void *iov_base;//向量的缓冲区地址
    	size_t iov_len;//向量缓冲区的大小,以字节为单位
    	};
    
    • 1
    • 2
    • 3
    • 4
    1. 在调用readv()函数的时候必须指定iovec的iov_base的长度,将值放到成员iov_len中。
    2. 参数vector指向一 块结构vector的内存,大小由count指定,如下图所示(阴影部分表示需要设置的vector成员变量的值。)。
    3. 结构vector的成员变量iov_base指向内存空间,iov_len表示内存的长度。

    在这里插入图片描述

    4、使用writev()函数发送数据

    writev()函数可向多个缓冲区同时写入数据,函数原型如下:

    #include
    //此函数向套接字描述符s中写入在向量vector中保存的count块数据。
    ssize_t writev(int fd,const struct iovec *vector,int count);
    
    • 1
    • 2
    • 3

    writev()函数返回值为成功发送的字节数,当为-1时错误发生,可通过errno错误码查看:
    在这里插入图片描述
    参数vector为一个指向向量的指针,结构strcut iovec在文件中定义:

    struct iovec{
    	void *iov_base;//向量的缓冲区地址
    	size_t iov_len;//向量缓冲区的大小,以字节为单位
    	};
    
    • 1
    • 2
    • 3
    • 4
    1. 在调用 writev()函数的时候必须指定iovec 的iov_base 的长度,将值放到成员 iov_len中。

    2. 参数 vector指向一 块结构vector的内存,大小由count指定,如下图 所示,阴影部分表示需要设置的vector成员变量的值。

    3. 结构vector的成员变量iov_base指向内存空间,iov_len表示内存的长度。

    4. 与readv()函数相区别的是,writev()函数的vector内存空间的值都已经设定好了。

    在这里插入图片描述

    5.使用recvmsg()函数接收数据

    recvmsg()函数用于接收数据,与recv()函数、readv()函数比较,这个函数的使用复杂点。

    ①.函数recvmsg()原型含义

    函数原型如下:

    #include
    #include
    //此函数从套接字s中接收数据放到缓冲区msg中,操作的方式由flags指定。
    ssize_t recvmsg(int s,struct msghdr *msg,int flags);
    
    • 1
    • 2
    • 3
    • 4

    函数的返回值为成功接收到的字节数,当为-1时错误发生,通过errno获取错误码,另一方使用正常方式关闭连接的时候返回值为0,如调用close()函数关闭连接:
    在这里插入图片描述
    在这里插入图片描述
    recvmsg()函数的flags参数表示数据接收的方式,(值可以采用按位或的复合值):
    在这里插入图片描述

    ②.地址结构msghdr
    函数recvmsg()中用到结构msghdr的原型如下:

    struct msghdr{
        void *msg_name; //可选地址
        socklen_t msg_namelen;//地址长度
        struct iovec *msg_iov;//接收数据的数组
        size_t msg_iovlen;//msg_iov中的元素数量
        void *msg_control;//ancillary data,see below
        socklen_t msg_controllen;//ancillary data buffer lem
        int msg_flags;//接收消息的标志
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 成员msg_name表示源地址,即为一 个指向struct sockaddr 的指针,当套接字还没有连接的时候有效。
    2. 成员msg_namelen表示msg_name指向结构的长度。
    3. 成员msg_iov 与函数readv()中的含义一 致。
    4. 成员msg_iovlen 表示msg_iov 缓冲区的字节数。
    5. 成员msg_control指向缓冲区,根据msg_flags的值,会放入不同的值。
    6. 成员msg_controllen 为msg_control指向缓冲区的大小。
    7. 成员msg_flags为操作的方式。

    recv() 函数通常用于TCP类型的套接字,UDP使用 recvfr om() 函数接收数据,当然在数据报套接字绑定地址和端口后,也可以使用 recv() 函数接收数据。

    ③.函数recvmsg()用户空间与内核空间的交互

    • 函数 recvmsg()内核的接收缓冲区中复制数据到用户指定的缓冲区,当内核中的数据比指定的缓冲区时,一般情况下(没有采用MSG_ WAITALL标志)会复制缓冲区中的所有数据到用户缓冲区,并返回数据的长度。

      • 当内核接收缓冲区中的数据比用户指定的时,会将用户指定长度 len 的接收缓冲区中的数据复制到用户指定地址,其余的数据需要下次调用接收函数的时候再复制,内核在复制用户指定的数据之后,会销毁已经复制完毕的数据,并进行调整。

    使用 一 个 msghdr 结构的头部数据如下图所示,msg_name 为指向一 个 20 个字节缓冲区的指针, msg_iov 为指向4 个向量的指针,每个向量的缓冲区大小为60 个字节。本机的IP地址为 192. 168. 1. 15 1

    在这里插入图片描述
    使用recvmsg()函数接收来自192.168.1.150的发送到192.168.1.151的200个UDP数据,则接收数据后msghdr结构的情况如图所示:

    在这里插入图片描述

    6.使用sendmsg()函数发送数据

    函数sendmsg()可用于多个缓冲区发送数据,函数原型如下:

    #include
    //此函数向套接字描述符s中按照结构msg的设定写入数据,操作方式由flags指定。
    ssize_t sending(int s,const struct msghdr *msg,int flags);
    
    • 1
    • 2
    • 3

    函数 sendmsg( )recvmsg()相区别的地方在于sendmsg 的操作方式由flags 参数设定,而recvmsg 的操作方式由参数msg 结构里的成员变量msg_ flags 指定。
    例如,向IP 地址为192.168.1.200 主机的9999 端口发送300个数据,协议为UDP 协议,将msg 参数中的向量缓冲区大小设为100 个,使用3个向量,msg的状态如图9 .5 所示。
    在这里插入图片描述

    7.IO函数的比较
    1. 函数read()/write()和readv()/writev()可以对所有的文件描述符使用;recv()/send()、recvfrom()/write()和recvmsg()/sendmsg()只能操作套接字描述符。

    2. 函数readv()/writev()和recvmsg()/sendmsg()可以操作多个缓冲区,read()/write()、recv()/send()recvfrom()/sendto()只能操作单个缓冲区。

    3. 函数recv()/ send()、recvfrom()/sendto()和recvmsg()/sendmsg()具有可选标志。

    4. 函数recvfrom()/sendto()和recvmsg()/sendmsg()可以选择对方的IP地址。

    5. 函数recvmsg()/sendmsg()有可选择的控制信息,能进行高级操作。
      在这里插入图片描述

    二、使用IO函数的例子

    1.客户端处理框架的例子

    ①.客户端程序框架

    步骤如下:

    1. 程序的输入参数进行判断,查看是否输入了要连接的服务器IP地址。
    2. 挂接信号SIGINT的处理函数和sig_proccess()SIGPIPE的处理函数sig_pipe(),用于处理子进程退出信号和套接字连接断开的情况。
    3. 建立 一 个流式套接字,结果放置在 s 中。
    4. 对要绑定的地址结构进行赋值,IP 地址为用户输入的值,端口为 8888。
    5. 连接服务器。
    6. 调用函数process_conn_client()进行客户端数据的处理,这个函数在不同的模式下,收发函数的实现方式不同。
    7. 处理完毕后关闭套接字。
      在这里插入图片描述

    ②.客户端程序框架代码

    在程序的开始调用函数signal()注册SIGINT和SIGPIPE信号的处理函数,然后连接服务器并进行数据处理。

    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include"fun.h"
    extern void sig_proccess(int signo);
    extern void sig_pipe(int signo);
    extern void process_conn_client(int s);
    static int s;
    
    void sig_proccess_client(int signo)//客户端信号处理回调函数
    {
            printf("Catch a exit signal\n");
            close(s);
            exit(0);
    }
    
    #define PORT 8888
    int main(int argc,char *argv[])
    {
            struct sockaddr_in server_addr;//服务器地址结构
            int err;//返回值
    
            if(argc == 1){
                    printf("PLS input server addr\n");
                    return 0;
            }
            signal(SIGINT,sig_proccess);//挂接SIGINT信号,处理函数sig_proccess
            signal(SIGPIPE,sig_pipe);//挂接SIGPIPE信号,处理函数sig_pipe
    
            s=socket(AF_INET,SOCK_STREAM,0);//建立一个流式套接字
            if(s<0){//建立套接字出错
                    printf("socket error\n");
                    return -1;
            }
            //设置服务器地址
            bzero(&server_addr,sizeof(server_addr));//将导致结构清零
            server_addr.sin_family = AF_INET;//将协议族设置为AF_INET
            server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    		//IP地址为本地任意IP地址
            server_addr.sin_port = htons(PORT);//设置服务器端口为8888
    
            inet_pton(AF_INET,argv[1],&server_addr.sin_addr);//将用户输入的字符串类型的IP地址转为整型
    
            connect(s,(struct sockaddr*)&server_addr,sizeof(struct sockaddr));//连接服务器
    
            process_conn_client(s);//客户端处理过程
            close(s);//关闭
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    2.服务器端程序框架

    服务器端处理程序是一个程序框架,为后面使用3种类型的收发函数建立基本的架构,函数process_conn_server()是进行服务器端处理的函数,不同收发函数的实现方式:

    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include"fun.h"
    extern void sig_proccess(int signo);
    #define PORT 8888//监听端口地址
    #define BACKLOG 2//侦听队列长度
    
    int main(int argc,char *argv[])
    {
            int ss,sc;//ss为服务器的socket描述符,sc为客户端的socket描述符
            struct sockaddr_in server_addr;//服务器地址结构
            struct sockaddr_in client_addr;//客户端地址结构
    
            int err;//错误值
            pid_t pid;//分叉的进行id
            signal(SIGINT,sig_proccess);//挂接SIGINT信号,处理函数sig_proccess
            signal(SIGPIPE,sig_proccess);//挂接SIGPIPE信号,处理函数sig_pipe
    
    
            ss = socket(AF_INET,SOCK_STREAM,0);//建立一个流式套接字
            if(ss<0){//出错
                    printf("socket error\n");
                    return -1;
            }
    		//设置服务器地址
            bzero(&server_addr,sizeof(server_addr));//清零
            server_addr.sin_family = AF_INET;//协议族
            server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//本地地址
            server_addr.sin_port = htons(PORT);//服务器端口
    		//绑定地址结构到套接字描述符
            err = bind(ss,(struct sockaddr*)&server_addr,sizeof(server_addr));
            if(err<0){//绑定出错
                    printf("listen error\n");
                    return -1;}
    		err = listen(ss,BACKLOG);//设置侦听队列长度
    		if(err<0){//出错
    		  printf("listen error\n");
                    return -1;}
            for(;;){
                    int addrlen = sizeof(struct sockaddr);
                    //接收客户端连接
                    sc = accept(ss,(struct sockaddr*)&client_addr,&addrlen);
                    if(sc<0){//客户端连接出错
                            continue;//结束循环
                    }
                    //建立新的进程处理到来的连接
                    pid = fork();//分叉进程
                    if(pid==0){//子进程中
                            close(ss);//在子进程中关闭服务器的侦听
                            process_conn_server(sc);//处理连接
                    }else{
                            close(sc);//在父进程中关闭客户端的连接
                    }
            }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    3.使用recv()和send()函数

    使用recv()和send()函数进行网络数据收发时服务器和客户端的实现:

    //服务器实现代码
    //使用recv()函数从套接字文件描述符s中读取数据到缓冲区buffer中,
    //不能接收到数据,退出;成功,利用接收到的数据构建发送给客户端的响应字符串
    //调用send()函数将响应字符串发送给客户端
    void process_conn_server(int s)
    {
            ssize_t  size = 0;
            char buffer[1024];//数据缓冲区
    
            for(;;){//循环处理过程
                    size = recv(s,buffer,1024,0);//从套接字中读取数据放到缓冲区buffer中
    
                    if(size == 0){//没有数据
                            return;
                    }
                    sprintf(buffer,"%ld bytes altogether\n",size);//构建响应字符,为接收到客户端字节的数量
                    send(s,buffer,strlen(buffer)+1,0);//发给客户端
            }
    }
    
    
    
    //客户端处理代码
    //是个循环过程,客户端调用read()函数从标准输入读取输入信息;
    //调用send()函数将信息发给服务器后,调用recv()函数接收服务器端的响应,
    //并将服务器端响应结果写到标准输出端。
    void process_conn_client(int s)
    {
            ssize_t size = 0;
            char buffer[1024];//数据缓冲区
    
            for(;;){//循环处理
                    size = read(0,buffer,1024);//从标准输入中读取数据放到缓冲区buffer中
                    
                    if(size>0){//读到数据
                            send(s,buffer,size,0);//发给服务器
                            size = recv(s,buffer,1024,0);//从服务器读取数据
                            write(1,buffer,size);//写到标准输出
                    }
            }
    }
    
    //信号SIGINT处理函数
    void sig_proccess(int signo)
    {
            printf("Catch a exit signal\n");
            _exit(0);
    }
    
    
    void sig_piep(int sign)
    {
    	printf("Catch a SIGPIPE signal\n");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    4.使用readv()和write()函数

    下面代码代替上面函数process_conn_client()和process_conn_server(),使用readv()和write()函数进行读写:

    
    //服务器实现代码
    
    //使用readv()和write()进行数据IO的服务器处理的代码,利用向量来接收和发送网络数据。
    //3个向量完成数据的接收和响应;申请3个向量,每个大小为10个字符。
    //利用公共的30个字节大小的缓冲区buffer来初始化3个向量的地址缓冲区,将每个向量的向量长度设置为10。
    //调用readv()读取客户端的数据后,利用3个缓冲区构建响应信息,最后将响应信息发给服务器端。
    
    
    
    #include
    #include
    #include
    #include
    #include
    
    static struct iovec *vs = NULL,*vc = NULL;
    
    void process_conn_server(int s)//服务器对客户端的处理
    {
            char buffer[30];//向量缓冲区
            sszie_t size  = 0;
    
            struct iovec *v = (struct iovec*)malloc(3*sizeof(struct iovec));//申请3个向量
    
            if(!v){
                    printf("Not enough memory\n");
                    return;
            }
    
            vs = v;//挂接全局变量,便于释放管理
            //每个向量10个字节的空间
            v[0].iov_base = buffer;//0~9
            v[1].iov_base = buffer+10;//10~19
            v[2].iov_base = buffer+20;//20~29
            v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;//初始化长度为10
    
            for(;;){//循环处理
                    size = readv(s,v,3);//从套接字中读取数据放到向量缓冲区中
                    if(size == 0){//没有数据
                            return;
                    }
    		//构建响应字符,为接收到客户端字节的数量,分别放到3个缓冲区中
                    sprintf(v[0].iov_base,"%d",size);//长度
                    sprintf(v[1].iov_base,"bytes alt");//"bytes alt"字符串
                    sprintf(v[2].iov_base,"ogether\n");//"ogether\n"字符串
    				//写入字符串长度
                    v[0].iov_len = strlen(v[0].iov_base);
                    v[1].iov_len = strlen(v[1].iov_base);
                    v[2].iov_len = strlen(v[2].iov_base);
                    write(s,v,3);//发给客户端
                    }
               }
    
    
    
    
    
    //客户端处理代码
    
    //使用3个10字节大小的向量来完成数据的发送和接收操作
    void process_conn_client(int s)
    {
        char buffer[30];//向量缓冲区
        ssize_t size = 0;
        struct iovec *v = (struct iovec*)malloc(3*sizeof(struct iovec));//申请3个向量
        
        if(!v){
            printf("Not enough memory\n");
            return;
        }
        //挂接全局变量,便于释放管理
        vc = v;
        //每个向量10个字节的空间
        v[0].iov_base = buffer;//0~9
        v[1].iov_base = buffer+10;//10~19
        v[2].iov_base = buffer+20;//20~29
        //初始化长度为10
        v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;
        
        int i =0;
        for(;;){//循环处理
        //从标准输入中读取数据放到缓冲区buffer中
            size = read(0,v[0].iov_base,10);
            if(size>0){//读取数据
                v[0].iov_len = size;
                write(s,v,1);//发送给服务器
                v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;
                size = readv(s,v,3);//从服务器读取数据
                for(i=0;i<3;i++){
                    if(v[i].iov_len>0){
                        write(1,v[i].iov_base,v[i].iov_len);//写到标准输出
                    }
                }
            }
        }
    }
    
    
    //信号处理代码
    
    //向量的内存空间是动态申请的,程序退出的时候不能自动释放,所以在信号SIGIN到来时,
    //先释放申请的内存空间,再退出应用程序
    void sig_proccess(int signo)
    {
        printf("Catch a exit signal\n");
        free(vc);
        free(vs);
        _exit(0);
    }
    
    
    void sig_pipe(int sign)
    {
        printf("Catch a SIGPIPE signal\n");
        
        free(vc);
        free(vs);
        _exit(0);
    }
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    5.使用recvmsg()和sendmsg()函数

    下面代码代替上面函数process_conn_client()和process_conn_server(),使用recvmsg()和sendmsg()函数进行读写:

    //服务器端实现代码
    
    //使用消息函数进行IO的服务器处理过程同样适合3个10字节大小的向量缓冲区来保存数据
    //但是并不是直接对这些向量进行操作,而是将向量挂载消息结构msghdr的msg_iov成员变量上
    //进行操作,并将向量的存储空间长度设置为30。在服务器端调用函数recvmsg()从套接字s中接收数据到消息msg中,将接收到的信息进行处理后,调用sendmsg()函数将响应数据通过套接字s发出。
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    
    
    static struct iovec *vs = NULL,*vc = NULL;
    //服务器对客户端的处理
    void process_conn_server(int s)
    {
        char buffer[30];//向量的缓冲区
        ssize_t size = 0;
        struct msghdr msg;//消息结构
        struct iovec *v = (struct iovec*)malloc(3*sizeof(struct iovec));//申请3个向量
        if(!v){
            printf("Not enough memory\n");
            return;
        }
        //挂接全局变量,便于释放管理
        vs = v;
        msg.msg_name = NULL;//无名字域
        msg.msg_namelen = 0;//名字域长度为0
        msg.msg_control = NULL;//无控制域
        msg.msg_controllen = 0;//控制域长度为0
        msg.msg_iov = v;//挂接向量指针
        msg.msg_iovlen = 30;//接收缓冲区长度为30
        msg.msg_flags = 0;//无特殊操作
        //每个向量10个字节的空间
        v[0].iov_base = buffer;//0~9
        v[1].iov_base = buffer+10;//10~19
        v[2].iov_base = buffer+20;//20~29
        //初始化长度为10
         v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;
         
         for(;;){//循环处理过程
         //从套接字中读取数据放到向量缓冲区中
             size = recvmsg(s,&msg,0);
             if(size == 0){//没有数据
                 return;
             }
             
         //构建响应字符,为接收到客户端字节的数量,分别放到3个缓冲区中
        sprintf(v[0].iov_base,"%d",size);//长度
        sprintf(v[1].iov_base,"bytes alt");//"bytes alt"字符串
        sprintf(v[2].iov_base,"ogether\n");//"ogether\n"字符串
        //写入字符串长度
        v[0].iov_len = strlen(v[0].iov_base);
        v[1].iov_len = strlen(v[1].iov_base);
        v[2].iov_len = strlen(v[2].iov_base);
          sendmsg(s,&msg,0);
          }
                    
    }
    
    
    //客户端处理代码
    //与服务器对应,客户端的实现也将3个向量挂接在一个消息上进行数据的收发操作
    void process_conn_client(int s)
    {
        char buffer[30];//向量缓冲区
        ssize_t size = 0;
        struct msghdr msg;//消息结构
      
        struct iovec *v = (struct iovec*)malloc(3*sizeof(struct iovec));//申请3个向量
        
        if(!v){
            printf("Not enough memory\n");
            return;
    }
        //挂接全局变量,便于释放管理
        vs = v;
        //初始化消息
        msg.msg_name = NULL;//无名字域
        msg.msg_namelen = 0;//名字域长度为0
        msg.msg_control = NULL;//无控制域
        msg.msg_controllen = 0;//控制域长度为0
        msg.msg_iov = v;//挂接向量指针
        msg.msg_iovlen = 30;//接收缓冲区长度为30
        msg.msg_flags = 0;//无特殊操作
         //每个向量10个字节的空间
        v[0].iov_base = buffer;//0~9
        v[1].iov_base = buffer+10;//10~19
        v[2].iov_base = buffer+20;//20~29
        //初始化长度为10    
        v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;
        
        
        int i = 0;
        
        for(;;){
        //从标准输入中读取数据放到缓冲区buffer中
            size = read(0,v[0].iov_base,10);
            if(size >0 ){//读到数据
                v[0].iov_len = size;
                sendmsg(s,&msg,0);//发送给服务器
                v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;
                size = recvmsg(s,&msg,0);//从服务器读取数据
                for(i=0;i<3;i++){
                    if(v[i].iov_len>0){
                        write(1,v[i].iov_base,v[i].iov_len);//写到标准输出
                    }
                    }
                }
                
            }
        }
    
    
    //信号处理代码
    //向量的内存空间是动态申请的,程序退出的时候不能自动释放,所以在信号SIGIN到来时,
    //先释放申请的内存空间,再退出应用程序
    void sig_proccess(int signo)
    {
        printf("Catch a exit signal\n");
        free(vc);
        free(vs);
        _exit(0);
    }
    
    
    void sig_pipe(int sign)
    {
        printf("Catch a SIGPIPE signal\n");
        
        free(vc);
        free(vs);
        _exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136

    三、IO模型

    IO的方式有阻塞IO、非阻塞IO模型、IO复用、信号驱动、异步IO等。

    1.阻塞IO模型

    阻塞IO是最通用的IO类型,使用这种模型进行数据接收的时候,在数据没有到之前程序会一直等待。例如,对于函数recvfrom(),内核会一直阻塞该请求直到有数据到来才返回,如下图所示。
    在这里插入图片描述

    2.非阻塞IO模型

    当把套接字设置成非阻塞的IO,则对每次请求,内核都不会阻塞,会立即返回;当没
    有数据的时候,会返回一个错误。例如,对recvfrom()函数,前几次都没有数据返回,直到最后内核才向用户层的空间复制数据,如下图所示:
    dao

    3.IO复用

    使用IO复用模型可以在等待的时候加入超时的时间,当超时时间没有到达的时候与阻塞的情况一致,而当超时时间到达仍然没有数据接收到,系统会返回,不再等待。select()函数按照一定的超时时间轮询,直到需要等待的套接字有数据到来,利用recvfrom()函数,将数据复制到应用层,如下图所示。
    在这里插入图片描述

    4.信号驱动IO模型

    信号驱动的IO在进程开始的时候注册一个信号处理的回调函数,进程继续执行,当信号发生时,即有了IO的时间,这里就有数据到来,利用注册的回调函数将到来的数据用recvfrom()接收到,如下图所示。

    在这里插入图片描述

    5.异步IO模型

    异步IO与前面的信号驱动IO相似,其区别在于信号驱动IO数据到来的时候,使用信号通知注册的信号处理函数,而异步IO则在数据复制完成的时候才发送信号通知注册的信号处理函数,如下图所示。

    在这里插入图片描述

    四、select()函数和pselect()函数

    函数select()和pselect()用于IO的复用,它们监视多个文件描述符的集合,判断是否有符合条件的时间发生。

    1.select()函数

    • 函数select()与之前的函数recv()和send()直接操作文件描述符不同

      • 使用select()函数可以先对需要操作的文件描述符进行查询,查看目标文件描述符是否可以进行读、写或者错误操作,然后当文件描述符满足操作的条件的时候才进行真正的IO操作。

    ①.简介

    #include
    #include
    #include
    #include
    int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. nfds: 一个整型的变量,它比所有文件描述符集合中的文件描述符的最大值大1。使用select的时候必须计算最大值的文件描述的值,将值通过nfds传入。

    2. readfds: 这个文件描述符集合监视文件集中的任何文件是否有数据可读,当select()函数返回的时候,readfds将清除其中不可读的文件描述符,只留下可读的文件描述符,即可以被函数recv()、read()等进行读数据的操作。

    3. writefds: 这个文件描述符集合监视文件集中的任何文件是否有数据可写,当select()函数返回的时候,readfds将清除其中的不可写的文件描述符,只留下可写的文件描述符,即可以被send()、write()函数等进行写数据的操作。

    4. exceptfds: 这个文件集将监视文件集中的任何文件是否发生错误,其实,它可用于其他的用途。例如,监视带外数据00B,带外数据使用MSG_OOB标志发送到套接字上。当select()函数返回的时候,readfds将清除其中的其他文件描述符,只留下可读00B数据。

    5. timeout: 设置在select()所监视的文件集合中的事件没有发生时,最长的等待时间,当超过此时间时,函数会返回。当超时时间为NULL时,表示阻塞操作,会一直等待,直到某个监视的文件集中的某个文件描述符符合返回条件。当timeout的值为0时,select会立即返回。

    6. sigmask: 信号掩码。

    函数select()返回值为0、-1或者一个大于1的整数值

    当监视的文件集中有文件描述符 符合要求,即读文件描述符集中的文件可读写文件描述符中的文件可写或者错误文件描述符中的文件发生错误时,返回值为大于0的正值;
    超时的时候返回0;

    当返回值为-1发生错误,由errno指定:

    在这里插入图片描述

    1. 函数select()和函数pselect()允许程序监视多个文件描述符,当一个或者多个监视的文件描述符准备就绪,可以进行IO操作的时候返回。函数监视一个文件描述符的对应操作是否可以进行,例如对监视读文件集的对文件描述符可操作。

    2. 函数可以同时监视3类文件描述符。将监视在readfds文件描述符集合中的文件是否可读,即判断对此文件描述符进行读操作是否被阻塞;

    3. 函数监视writefds文件描述符集合中的文件是否可写,即判断是否对此文件描述符进行写操作是否阻塞;

    4. 函数还监视文件描述符集合exceptfds中的文件描述符是否发生意外。当函数退出的时候,上述的集合发生了改变。

    5. 不需要监视某种文件集时,可以将对应的文件集设置为NULL。如果所有的文件集和均为NULL, 则表示等待 一 段时间。

    timeout的类型结构如下:

    struct timeval{
    	time_t tv_sec;//秒
    	long tv_usec;//微秒
    	};
    
    • 1
    • 2
    • 3
    • 4
    1. 成员tv_sec表示超时的秒数
    2. 成员tv_usec表示超时的微秒数,即1/1000000s。

    4个宏可以操作文件描述符的集合:

    1. FD_ZERO():清理文件描述符的集合;
    2. FD_SET():向某个文件描述符集合加入文件描述符;
    3. FD_CLR():从某个文件描述符的集合中取出某个文件描述符。
    4. FD_ISSET():测试某个文件描述符是否是某个集合中的一员。

    文件描述符的交换存在最大的限制,其最大值为FD_SETSIZE,当超出最大值时,将发生不能确定的事情。

    ②.select函数的例子

    使用select()函数监视标准输入是否有数据处理,设置超时时间为5s,如果出错,则打印出错信息;如果标准输入有数据输入,则打印输入信息;如果等待超时,则打印超时信息。

    #include
    #include
    #include
    #include
    
    int main(void){
            fd_set rd;//读文件集合
            struct timeval tv;//时间间隔
            int err;//错误值
    		//监视标准输入是否可以读数据
            FD_ZERO(&rd);
            FD_SET(0,&rd);
    		//设置5s的等待超时
            tv.tv_sec = 5;
            tv.tv_usec = 0;
    		
            err = select(1,&rd,NULL,NULL,&tv);//函数返回,查看返回条件
    
            if(err = -1)//出错
                    perror("select()");
            else if(err)//标准输入有数据输入,可读
                    printf("Dara is available now.\n");//FD_ISSET(0,&rd)的值为真
            else
                    printf("No data within five seconds.\n");//超时,没有数据到达
            return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在这里插入图片描述

    2.pselect()函数

    函数select()是用一种超时轮循的方式查看文件的读写错误的可操作性,在Linux下,还有一种相似的函数pselect()。

    ①.简介

    函数原型:

    #include
    #include
    #include
    #include
    int pselect(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,const struct timespec *timeout,const sigset_t *sigmask);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    pselect()函数的含义与select()函数一致,除了如下几点:

    1. 超时的时间结构是一个纳秒级的结构,原型如下所示。不过在 Linux 平台下内核调度的精度为10毫秒级,所以即使设置了纳秒级的分辨率,也达不到设置的精度。
    struct timespec{
    long tv_sec;//超时的秒数
    long tv_nsec;//超时得到纳秒数
    };
    
    • 1
    • 2
    • 3
    • 4
    1. 增加了进入 pselect()函数时替换掉的信号处理方式,当sigmask 为NULL 的时候,与select的方式一致。

    2. select()函数在执行之后可能会改变 timeout 的值,修改为还有多少时间剩余,而pselect()函数不会修改该值。

    与select()函数相比,pselect()函数的代码如下:

    ready = pselect(nfds,&readfds,&writefds,&exceptfds,timeout,&sigmask);
    
    • 1

    对于下面的select()函数,在进入select()函数之前先手动将信号的掩码改变,并保存之前掩码值;select()函数执行后,再恢复为之前的信号掩码值。

    sigset_t origmask;
    sigprocmask(SIG_SETMASK,&sigmask,&origmask);
    read = select(nfds,&readfds,&writefds,&exceptfds,timeout);
    sigprocmask(SIG_SETMASK,&origmask,NULL);
    
    • 1
    • 2
    • 3
    • 4

    ②.pselcet()函数的例子

    下面是 一 个使用 pselect()的简单例子。在例子中先清空信号,然后将 SIG C H LD 信号加入到要处理的信号集合中。设置pselect()监视的信号时,在挂载用户信号的同时将系统原来的信号保存下来,方便程序退出的时候恢复原来的设置。

    #include
    #include
    #include
    #include
    #include
    #include
    int child_events = 0;
    
    void child_sig_handler(int x){//信号处理函数
            child_events++;//调用次数+1
            signal(SIGCHLD,child_sig_handler);//重新设定信号回调函数
    }
    
    int main(int argc,char *argv){
    //设定信号掩码sigmask和原始的信号掩码orig_sigmask
            sigset_t sigmask,orig_sigmask;
            sigemptyset(&sigmask);//清空信号
            sigaddset(&sigmask,SIGCHLD);//将SIGCHLD信号加入sigmask
    	
    	//设定信号SIG_BLOCK的掩码sigmask, 并将原始的掩码保存到orig_sigmask中
            sigprocmask(SIG_BLOCK,&sigmask,&orig_sigmask);
          //挂接对信号SIGCHLD的处理函数child_sig_handler()
            signal(SIGCHLD,child_sig_handler());
            for(;;){//主循环
                    for(;child_events > 0;child_events--){//判断是否退出
                    //处理动作
                    }
                    //pselect IO复用
                    r=pselect(1,&rd,&wr,&er,0,&orig_sigmask);
                    //主程序
            }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    五、poll()函数和ppoll()函数

    除了使用select()函数进行文件描述符监视,还有一组函数也可以完成相似功能,即函数poll()和函数ppoll()。

    1.poll()函数

    poll()函数等待某个文件描述符上的某个事情的发生,函数原型:

    #include
    int poll(struct pollfd *fds,nfds_t nfds,int timeout);
    
    • 1
    • 2

    poll()函数监视在fds数组指明的一 组文件描述符上发生的动作,当满足条件或者超时的时候会退出。

    1. 参数fds是一个指向结构pollfd 数组的指针,监视的文件描述符和条件放在里面。

    2. 参数nfds是比监视的最大描述符的值大1的值。

    3. 参数 timeout是超时时间,单位为毫秒,当为负值时,表示永远等待。

    poll()函数返回值的:

    1. 大于0: 表示成功,等待的某个条件满足,返回值为满足条件的监视文件描述符的数量。
    2. 0:表示超时。
    3. -1表示发生错误, errno 的错误代码如下图所示。

    在这里插入图片描述
    结构struct pollfd的原型如下:

    struct pollfd{
    	int fd;//监视文件描述符。
    	short events;//请求的事件;表示输入的监视事件。
    	short revents;//返回的监视事件,即返回时发生的事件。
    	};
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    2.ppoll()函数

    函数原型:

    #include
    int ppoll(struct pollfd *fds,nfds_t nfds,const struct timespec *timeout,const sigset_t *sigmask);
    
    • 1
    • 2

    其区别同函数select()和pselect()的区别相同,主要有两点:

    1. 超时时间timeout, 采用了纳秒级的变量。
    2. 可以在ppoll()函数的处理过程中挂接临时的信号掩码。

    ppoll()函数的代码如下:

    ready = ppoll(&fds,nfds,timeout,&sigmask);
    
    • 1

    与poll()函数的如下代码一致:

    sigset_t origmask;
    sigprocmask(SIG_SETMASK,&sigmask,&origmask);
    read = ppoll(&fds,nfds,timeout,&sigmask);
    sigprocmask(SIG_SETMASK,&origmask,NULL);
    
    • 1
    • 2
    • 3
    • 4

    六、非阻塞编程

    1.非阻塞方式程序设计介绍

    非阻塞方式的操作与阻塞方式的操作最大的不同点是函数的调用立刻返回,不管数据是否成功读取或者成功写入。使用fcntl()将套接字文件描述符按照如下的代码进行设置后,可以进行非阻塞的编程:

    fcntl(s,F_SETFL,O_NONBLOCK);//s:套接字文件描述符;
    //F_SETFL命令将套接字s设置为非阻塞方式后,在进行读写操作就可以正常返回。
    
    • 1
    • 2

    非阻塞程序设计的例子

    函数accept()可以使用非阻塞的方式轮询等待客户端的到来,在之前要设置O_NONBLOCK方式。下面使用了轮询的方式使用accept()和recv()函数:

    1. 当客户端发送HELLO字符串时,发送OK响应给客户端并关闭客户端

    2. 当客户端发送SHUTDOWN字符串给服务器时,服务器发送BYE的客户端并关闭客户端,然后退出程序

    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #define PORT 9999
    #define BACKLOG 4
    #define BUFFER_SIZE 1024
    int main(int argc,char *argv[])
    {
            struct sockaddr_in local;
            struct sockaddr_in client;
            int len;
            int s_s = -1,s_c = -1;
    		//初始化结构
            local.sin_family = AF_INET;
            local.sin_port = htons(PORT);
            local.sin_addr.s_addr = htonl(-1);
    		//建立套接字描述符
            s_s = socket(AF_INET,SOCK_STREAM,0);
    		//设置非阻塞方式
            fcntl(s_s,F_SETFL,O_NONBLOCK);
    
            listen(s_s,BACKLOG);
    
            char buffer[BUFFER_SIZE];
            for(;;)
            {
            		//轮询接收客户端
                    while(s_c<0){//等待客户端到来
                            s_c = accept(s_s,(struct sockaddr*)&client,&len);
                    }
    				//轮询接收,当接收到数据的时候退出while循环
                    while(recv(s_c,buffer,1024,0)<=0)
                			;
                	//接收到客户端的数据
                    if(strcmp(buffer,"HELLO")==0){//判断是否为HELLO字符串
                            send(s_s,"OK",3,0);//发送响应
                            close(s_c);//关闭连接
                            continue;//继续等待客户端连接
                    }
                    if(strcmp(buffer,"SHUTDOWN")==0){//判断是否为SHUTDIWN字符串
                                    send(s_s,"BYE",3,0);//发送BYE字符串
                                    close(s_c);//关闭客户端连接
                                    break;//退出注循环
                    }
            }
            close(s_s);
            return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    注:使用轮询的方式进行查询十分浪费CPU等资源。

  • 相关阅读:
    JPA Audit and Envers
    # Navicat报错:1045-Access denied for user root@localhost(using password:YES)怎么解决
    【无标题】是是是
    木聚糖-氨基|Xylan-NH2|木聚糖-聚乙二醇-氨基|氨基-PEG-木聚糖
    Shopee可以绑定大陆银行卡吗?Shopee收款方式选哪种?——站斧浏览器
    CPU受限直接执行
    大模型时代,AI如何成为数实融合的驱动力?
    firewalld防火墙处理的命令
    前缀和与树状数组(数据结构基础篇)
    Visual Studio Code 高效开发 C/C++ 插件推荐
  • 原文地址:https://blog.csdn.net/weixin_50866517/article/details/126118135