socket的四种IO模型

socket的四种IO模型

  阻塞型:
    1.最常用/最简单/效率低;
    2.函数本身不具备阻塞属性,而是由于文件描述符本身导致函数阻塞;
    3.在默认情况下Linux建立的socket套接都是阻塞的。
  非阻塞:
    1.设置某个文件描述符为非阻塞的,需要轮询;
    2.占用CPU资源较大。
  多路复用型:
    1.同时对多个IO进行操作;
    2.可以设置在规定的时间内检测数据是否到达。
  信号驱动型:
    1.属于异步通信方式;
    2.当socket中有数据到达时,通过发送信号告知用户。

阻塞型

  读阻塞
    1.当套接字接收缓冲区中没有数据可以读取时调用 如 read/recv/recvfrom就会导致阻塞,当有数据到达时,内核便会去唤醒进程,通过read等函数来访问数据;
    2.如果进程阻塞过程中发生意外,那么进程将永远阻塞下去。
  写阻塞
    1.发生写阻塞的机会比较少,一般出现在写缓冲区无法写入和即将写入的数据时;
    2.当无法写入数据时便会进入阻塞等待,一旦发送的缓冲区拥有足够的空间,则内核会唤醒对应的进程进行写入操作;
    3.UDP协议中并不存在发送缓冲区满的情况,UDP套接字执行写操作时永远不会发生阻塞。

非阻塞型

  如果有一个IO操作不能马上完成则系统则会让我们的进程进入睡眠状态等待,当我们将一个套接字设置为非阻塞模式时,则系统不会让我们的进程进入睡眠等待而是直接返回错误
当一个应用使用了非阻塞模式的套接字,他需要使用循环来不断检查文件描述如是否有数据可读应用程序不同循环判断将会消耗非常大的CPU资源,所以一般不推荐使用。

非阻塞型socket的实现

  当我们一开始建立一个套接字描述符时,系统内核会默认设置为阻塞型IO,我们可以使用函数fcntl()来设置套接字为非阻塞状态。

相关API

fcntl()
    函数功能:
            文件描述符操作
    头 文 件:
            #include <unistd.h>
            #include <fcntl.h>
    定义函数:
            int fcntl(int fd, int cmd);
            int fcntl(int fd, int cmd, long arg);
            int fcntl(int fd, int cmd, struct flock * lock);
    参数分析:
            fd --> 需要设置的文件描述符
            cmd --> 设置的功能:
                        F_GETFL 取得文件描述词状态旗标,此旗标为open()的参数 lags.
                        F_SETFL 设置文件描述词状态旗标,参数arg为新旗标,但只允许O_APPEND、O_NONBLOCK和O_ASYNC位的改变,其他位的改变将不受影响.
    返 回 值:
            成功返回 0 
            失败返回 -1
    备    注:

实验程序

nob_server.c

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

 void *recv_func(void *connect_fd)
 {
     
    char *rev_data = (char *)calloc(100, sizeof(char));
    int cfd = (int)connect_fd;
    printf("cfd:%d\n", cfd);
    if (rev_data == NULL)
    {
        printf("calloc error");
        exit(-1);
    }
    while (1)
    {
        bzero(rev_data , 100);
        int  ret_val = recv(cfd, rev_data, 100, 0);
        if (ret_val > 0)
        {
            printf("recv msg: %s", rev_data); 
        }
    }
 }

int main(int argc, char const *argv[])
{
    int ret_val;

    
   // printf("bind\n");
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sock_fd)
    {
        perror("socket error");
        return -1;
    }

    int state = fcntl(sock_fd , F_GETFL , 0); // 获得当前描述符的旗标
    state |= O_NONBLOCK;  // 在原基础上增加非阻塞属性
    fcntl(sock_fd ,F_SETFL, state); // 把配置的好的旗标重新设置回描述符中

    struct sockaddr_in server_addr;
    bzero(&server_addr , sizeof(server_addr)); 
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(65110);
    server_addr.sin_addr.s_addr = inet_addr("172.31.42.110");

    ret_val = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (-1 == ret_val)
    {
        perror("bind error");
        return -1;
    }
    printf("bind successfully\n");


    ret_val = listen(sock_fd, 2);
    if (-1 == ret_val)
    {
        perror("listen error");
        return -1;
    }
    
    printf("listen successfully\n");

    struct sockaddr_in other_addr;
    socklen_t addrlen = sizeof(other_addr);
    int connect_fd = 0;
    
    while (1)
    {
        connect_fd = accept(sock_fd, (struct sockaddr *)&other_addr, &addrlen);
        if (-1 == connect_fd)
        {
            //perror("accept error");
            continue;
        }
        printf("accept successfully\n");

        pthread_t TID;
        printf("connect_fd:%d\n", connect_fd);
        pthread_create(&TID, NULL, recv_func, (void *)connect_fd); //注意线程创建的时候,不能传递一个变化的参数,所以不能是&connect_fd

    }
    
    close(connect_fd);
    close(sock_fd);
    return 0;
}

输出结果:

注意:
  1.注意线程创建不能传递变化的参数;
  2.客户端程序使用我的随笔<网络套接字socket>中的客户端程序。

多路复用

  当应用程序同时处理多路数据的输入或输出时,若采用阻塞模式,将达不到预期的效果,如果采用非阻塞模式,对多个输入进行轮询可以实现,而且CPU的消耗非常大,如果使用多进程/多线程,将产生进程与线程同步互斥的问题使得程序变得非常复杂。这样使用多路复用则是最佳的选择,他的基本思想是:
    1.先把所有需要监听等待的文件描述符添加到一个集合中;
    2.在规定的时间内等待集合中所有描述符数据的变化,如果超时则跳出或进入下一次等待;
    3.如果在规定时间内文件描述符的数据有发生变化,则把其他没有数据变化的描述符踢出到集合之外等待进行下一次的等待状态。

相关API

select()
    函数功能:
            I/O 多工机制,用来等待文件描述符组状态的改变
    头 文 件:
            #include <sys/time.h>
            #include <sys/types.h>
            #include <unistd.h>
    定义函数:
            int select(int n, fd_set * readfds, fd_set * writefds, fd_set * exceptfds, struct timeval * timeout);
    参数分析:
            n --> 当前最大的描述符+1
            readfds -->  读取描述符组 
            writefds --> 写入描述符组 
            exceptfds --> 其他描述符组 
            timeout --> 超时时间
    返 回 值:
            如果参数 timeout 设为 NULL 则表示 select ()没有 timeout.
            超时 返回 0 
            发生错误 返回 -1 
    备    注:
            struct timeval
            {
                  time_t tv_sec;
                  time_t tv_usec;
            };   
          
            FD_CLR(inr fd, fd_set* set); 用来清除文件描述符组set中相关fd的位
            FD_ISSET(int fd, fd_set *set);  用来测试文件描述符组set中相关fd的位是否为真
            FD_SET(int fd, fd_set*set);  用来设置文件描述符组set中相关fd的位
            FD_ZERO(fd_set *set);  用来清除文件描述符组set的全部位

实验程序

sel_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/select.h>

int main(int argc, char const *argv[])
{
    int ret_val;
    char *rev_data = (char *)calloc(100, sizeof(char));
    if (rev_data == NULL)
    {
        printf("calloc error");
        return -1;
    }
   
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sock_fd)
    {
        perror("socket error");
        return -1;
    }

    struct sockaddr_in server_addr;
    bzero(&server_addr , sizeof(server_addr)); 
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(65110);
    server_addr.sin_addr.s_addr = inet_addr("172.31.42.110");

    ret_val = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (-1 == ret_val)
    {
        perror("bind error");
        return -1;
    }
    printf("bind successfully\n");

    ret_val = listen(sock_fd, 2);
    if (-1 == ret_val)
    {
        perror("listen error");
        return -1;
    }
    
    printf("listen successfully\n");

    struct sockaddr_in other_addr;
    socklen_t addrlen = sizeof(other_addr);
    int connect_fd;

    connect_fd = accept(sock_fd, (struct sockaddr *)&other_addr, &addrlen);
    if (-1 == connect_fd)
    {
        perror("accept error");
    }
    printf("accept successfully\n");

    char buf [100];
    fd_set set;
    FD_ZERO(&set);
    FD_SET(connect_fd, &set);
    FD_SET(STDIN_FILENO, &set);
    int max_fd = connect_fd > STDIN_FILENO ? connect_fd : STDIN_FILENO;
    select(max_fd+1, &set, NULL,NULL, NULL);

    while (1)
    {
        bzero(rev_data , 100);
        if (FD_ISSET(connect_fd, &set))
        {
            int ret_val = recv(connect_fd, rev_data, 100, 0);
            if (ret_val > 0)
            {
                printf("recv client msg: %s", rev_data); 
            }
            FD_SET(STDIN_FILENO, &set);
            select(max_fd+1, &set, NULL,NULL, NULL);
        }
        if (FD_ISSET(STDIN_FILENO, &set))
        {
            fgets(buf , 100 , stdin);
            ret_val = send(connect_fd , buf , strlen(buf) , 0);
            FD_SET(connect_fd, &set);
            select(max_fd+1, &set, NULL,NULL, NULL);
        }
    }
    
    close(connect_fd);
    close(sock_fd);
    return 0;
}

sel_client.c

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

void *recv_func(void *sock_fd)
{
    int sfd = *(int *)sock_fd;
    char *rev_data = (char *)calloc(100, sizeof(char));
    if (rev_data == NULL)
    {
        printf("calloc error");
        exit(-1);
    }
    while (1)
    {
        bzero(rev_data , 100);
        int ret_val = recv(sfd, rev_data, 100, 0);
        if (ret_val > 0)
        {
            printf("recv server msg: %s", rev_data); 
        }
    }
}

int main(int argc, char const *argv[])
{
  
    int sock_fd = socket( AF_INET, SOCK_STREAM, 0);
    if (-1 == sock_fd)
    {
        perror("socket error");
        return -1 ;
    }
    printf("socket successfully\n");
    
    struct sockaddr_in client_addr ;  // 定义一个结构体变量
    bzero(&client_addr , sizeof(client_addr)); // 清空结构体
    client_addr.sin_family = AF_INET ; // IPV4地址协议
    client_addr.sin_port = htons(65110) ;
    client_addr.sin_addr.s_addr = inet_addr("172.31.42.110"); // 应该设置为服务器的IP地址

    int ret_val= connect(sock_fd, (struct sockaddr *)&client_addr,sizeof(client_addr));
    if (-1 == ret_val )
    {
        perror("connect error");
        return -1 ;
    }
    printf("connect successfully\n");

    pthread_t TID;
    pthread_create(&TID, NULL, recv_func, (void *)&sock_fd);
    
	struct in_addr addr;
	addr.s_addr = client_addr.sin_addr.s_addr;
    char buf[100];
    while(1)
    {
        fgets(buf , 100 , stdin);
        ret_val = send(sock_fd , buf , strlen(buf) , 0 );
        if (ret_val > 0)
        {
            // printf("server ip:%s server port:%d  send data is:%s", 
            //         inet_ntoa(addr), ntohs(client_addr.sin_port), buf);
        }
    }

    close(sock_fd);
    return 0;
}

输出结果:

信号驱动

  信号驱动涉及到Systrm_V的信号,通过监听文件描述符的状态(是否有产生信号),当文件描述符有数据到达时就会产生一个IO信号(SIGIO),来通知用户进行IO操作。
  特点:
    信号驱动一般用于UDP协议中,很少用于TCP协议中, 因为TCP协议中会有多次IO状态的改变,所以会有非常多的SIGIO信号产生,非常难捕捉到哪一个时数据到达产生的。
  由于数据变化时,产生SIGIO信号,所以必须提前设置好信号捕获,并设置其对应的响应函数和必须给文件描述符设置信号触发模式。

实验程序

sig_server.c

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

int sockfd = 0;

void sig_recv(int sig)
{
    struct sockaddr_in from_addr;
    int addrlen = sizeof(from_addr);
    char *recv_msg = calloc(1, 100);
    bzero(recv_msg, 100);
    int ret_val = recvfrom(sockfd, recv_msg, 100, 0, (struct sockaddr *)&from_addr, &addrlen);
    if (ret_val > 0)
    {
        printf("ip:%s port:%d receive a message:%s", 
            inet_ntoa(from_addr.sin_addr),
            ntohs(from_addr.sin_port), 
            recv_msg);
    }
}

int main(int argc, char const *argv[])
{
    struct sigaction *act = calloc(1, sizeof(struct sigaction));
    act->sa_handler = sig_recv;
    act->sa_flags =  SA_NODEFER;
    sigaction(SIGIO, act, NULL);

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1)
    {
        perror("socker error");
        return -1;
    }
    printf("Socket created successfully\n");

    struct sockaddr_in server_addr;
    int addrlen = sizeof(server_addr);
    bzero(&server_addr, addrlen);

    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(65000);
    server_addr.sin_addr.s_addr = inet_addr("172.31.42.110");

    int ret_val = bind(sockfd, (struct sockaddr *)&server_addr, addrlen);
    if (-1 == ret_val)
    {
        perror("bind error");
        return -1;
    }
    printf("bind successfully\n");

    
    fcntl(sockfd ,F_SETOWN, getpid());// 设置套接字的拥有者
    int fl ;
    fl = fcntl(sockfd , F_GETFL);
    fl |= O_ASYNC ; // 添加信号触发 选项到描述符中
    fcntl(sockfd , F_SETFL , fl);

    while (1);

    close(sockfd);
    return 0;
}

sig_client.c

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


int main(int argc, char const *argv[])
{
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == sockfd)
    {
       perror("socker error");
       return -1;
    }
    printf("Socket created successfully\n");

    struct sockaddr_in server_addr;
    int addrlen = sizeof(server_addr);
    bzero(&server_addr, addrlen);

    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(65000);
    server_addr.sin_addr.s_addr = inet_addr("172.31.42.110");

    int ret_val;
    char *msg = calloc(1, 1024);
    while (1)
    {
        bzero(msg, 100);
        fgets(msg, 100, stdin);
        ret_val = sendto(sockfd, msg, 100, 0, (struct sockaddr *)&server_addr, addrlen);
        if (ret_val > 0)
        {
            printf("ip:%s port:%d senda message:%s", 
                inet_ntoa(server_addr.sin_addr),
                ntohs(server_addr.sin_port), 
                msg);
        }
    }
    close(sockfd);
    return 0;
}

输出结果:

注意:
  1.UDP为不可靠传输,数据有可能会丢失、出错,但是实现会比较简单,表头所占字节少,开销小。

posted @ 2021-01-18 22:58  ding-ding-light  阅读(650)  评论(0编辑  收藏  举报