网络编程(学习)2024.8.29

目录

阻塞式IO(BIO)

特点

阻塞原因与阻塞反应

TCP流式套接字缓冲区

非阻塞式IO(NIO)     

特点

设置非阻塞

1.通过对参数的修改实现

2.通过对文件描述符的属性进行设置 fcntl

信号驱动IO (异步IO模型)

IO多路复用  select、poll、epoll

IO多路复用机制 

1.select

2.fd_set表的结构与机制

3.流程

阻塞式IO(BIO)

特点

最简单、最常用;但是相对于进程来说效率低

阻塞原因与阻塞反应

1.当程序调用某些接口时,如果期望的动作无法触发,那么进程会进入阻塞态(等待状态,让出CPU的调度),当期望动作可以被触发了,那么会被唤醒,然后处理事务。

2.阻塞I/O模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O,缺省情况下(及系统默认状态),套接字建立后所处于的模式就是阻塞I/O模式。
3.之前学习的很多读写函数在调用过程中会发生阻塞。

例如:
(1)读操作中的read、recv、recvfrom
        读阻塞——>需要读缓冲区中有数据可读,读阻塞解除
(2)写操作中的write、send
        写阻塞——>阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。 


注意:sendto没有写阻塞
无sendto函数的原因:
        sendto不是阻塞函数,本身udp通信不是面向连接的,udp无发送缓冲区,即sendto没有发送缓冲区,send是有发送缓存区的,即sendto不是阻塞函数。
        UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在缓冲区满的情况,在UDP套接字上进行写操作永远不会阻塞。
其他阻塞函数操作:accept、connect

TCP流式套接字缓冲区

da1d35b3ba4143f48f29237ad1c218b4.png

非阻塞式IO(NIO)     

特点

可以处理多路IO;但是需要轮询,浪费CPU资源

当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试一个文件描述符是否有数据可读,这步操作叫做轮询(polling)。
应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
这种模式使用中不普遍。

设置非阻塞

1.通过对参数的修改实现

例如recv()最后的参数:

 char buf[N];
        while (1)
        {
            memset(buf, 0, N);
            int ret = recv(acceptfd, buf, N, 0);//当是0的时候阻塞,当是MSG_DONTWAIT的时候非阻塞
            if (ret < 0)
            {
                if (errno == 11)
                {
                    printf("读缓存区内没数据\n");
                    continue;
                }
                else
                {
                    perror("recv失败\n");
                    return -1;
                }
            }
            else if (ret == 0)
            {
                printf("客户端ip:%s退出\n", inet_ntoa(caddr.sin_addr));
                close(acceptfd);
                break;
            }
            else
            {
                printf("客户端%s\n", buf);
            }
        }

2.通过对文件描述符的属性进行设置 fcntl

由于阻塞基本都是对有缓冲区的文件进行操作导致的,修改文件描述符的属性,就可以使read等的时候实现非阻塞。
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ...);
功能:
        获取/改变文件属性(linux中一切皆文件)
参数:
        fd:文件描述符
        cmd:设置的命令
                F_GETFL  //获取文件的属性
                F_SETFL  //设置文件的属性
        第三个参数:由第二个参数决定,set时候写需要设置的值,get时候写0

返回值:
        成功:文件状态标志(文件的属性)  
        失败: -1 

57a4b800b9fd4991a26aae6c9a9f5e0b.png

置0:目标位和0相与,其他位和1相与
置1:目标位和1相或,其他位和0相或

设置阻塞还是非阻塞

int flag;        //文件状态的标志 
flag = fcntl(fd, F_GETFL);         //读        fd是文件描述符
flag |= O_NONBLOCK;        //改        O_NONBLOCK = 0x00004000
fcntl(fd, F_SETFL, flag);        //写

例如:标准输入非阻塞设置

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

#define N 32
int main(int argc, char const *argv[])
{
    int flag;                
    flag = fcntl(0, F_GETFL); 
    flag |= O_NONBLOCK;    
    fcntl(0, F_SETFL, flag); 
    char buf[N];
    while (1)
    {
        memset(buf, 0, N);
        gets(buf);
        printf("buf:%s\n", buf);
        if (strlen(buf) > 0)
        {
            sleep(5);
        }
    }

    return 0;
}

信号驱动IO (异步IO模型)

特点:异步通知模式,需要底层驱动的支持

IO多路复用  select、poll、epoll

案例分析:键盘鼠标事件

同时对键盘和鼠标进行监听,当敲击键盘按下回车,就打印键盘输入的东西,动鼠标就要打印鼠标写入的内容。

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

#define N 64
int main(int argc, char const *argv[])
{
    int mouse = open("/dev/input/mouse0", O_RDONLY);
    if (mouse < 0)
    {
        perror("open失败");
        return -1;
    }
    char buf[N];
    while (1)
    {
        memset(buf, 0, N);
        gets(buf);
        printf("buf:%s\n", buf);
        int ret = read(mouse, buf, N);
        if (ret < 0)
        {
            perror("read失败");
            return -1;
        }
        else
        {
            printf("mouse:%s\n", buf);
        }
    }
}

IO多路复用机制 

使用I/O多路复用技术。其基本思想是:

1.先构造一张有关描述符的表,然后调用一个函数。
2.当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
3.函数返回时告诉进程哪个描述符已就绪,可以进行I/O操作。

1.select

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

 int select(int nfds(要轮询的文件描述符个数), fd_set *readfds(阻塞的读文件描述符), fd_set *writefds(阻塞的写文件描述符),fd_set *exceptfds(阻塞的异常文件描述符), struct timeval *timeout(超时时长));

一般写:select(int nfds,fd_set *readfds,NULL,NULL,NULL);

void FD_CLR(int fd, fd_set *set);        //将某一文件描述符在表里去除
int  FD_ISSET(int fd, fd_set *set);        //判断某一文件描述符是否在表里
void FD_SET(int fd, fd_set *set);        //将某一文件描述符放入表中
void FD_ZERO(fd_set *set);        //将表置零

2.fd_set表的结构与机制

e26c46e3e64c4269ba46d4f5e45ce5d6.png

3.流程

第一步:建表初始化
第二步:填表
第三步:监听表
第四步:判断,操作

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/select.h>

#define N 64
int main(int argc, char const *argv[])
{
    int mouse = open("/dev/input/mouse0", O_RDONLY);
    if (mouse < 0)
    {
        perror("open失败");
        return -1;
    }
    char buf[N];

    // 第一步:建表初始化
    fd_set readfds, tempfds;
    FD_ZERO(&readfds);
    // 第二步:填表
    FD_SET(0, &readfds);
    FD_SET(mouse, &readfds);
    // 第三步:监听表
    while (1)
    {
        memset(buf, 0, N);
        temphfds = readfds;
        int ret = select(4, &tempfds, NULL, NULL, NULL);
        // 第四步:判断,操作
        if (ret == -1)
        {
            perror("select失败");
            return -1;
        }
        if (FD_ISSET(0, &tempfds))
        {
            gets(buf);
            printf("buf:%s\n", buf);
        }
        if (FD_ISSET(mouse, &tempfds))
        {
            int n = read(mouse, buf, N);
            if (n < 0)
            {
                perror("read失败");
                return -1;
            }
            else
            {
                printf("mouse:%s\n", buf);
            }
        }
    }

    return 0;
}

例题:创建全双工客户端(既能接收也能发送)

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

int main(int argc, char const *argv[])
{
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    printf("sorkfd:%d\n", sockfd);

    // 2.连接
    unsigned short post = 0;
    char ip[15];
    printf("请输入ip地址");
    scanf("%s", ip);
    getchar();
    printf("请输入端口号");
    scanf("%hd", &post);
    getchar();
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(post);
    saddr.sin_addr.s_addr = inet_addr(ip);
    socklen_t addrlen = sizeof(saddr);
    if (connect(sockfd, (struct sockaddr *)&saddr, addrlen) < 0)
    {
        perror("connect失败\n");
        return -1;
    }

    // 3.接收
#define N 64
    char buf[N];
    while (1)
    {
        // 第一步:建表初始化
        fd_set readfds, tempfds;
        FD_ZERO(&readfds);
        // 第二步:填表
        FD_SET(0, &readfds);
        FD_SET(sockfd, &readfds);
        // 第三步:监听表
        while (1)
        {
            memset(buf, 0, N);
            tempfds = readfds;
            int ret = select(4, &tempfds, NULL, NULL, NULL);
            // 第四步:判断,操作
            if (ret == -1)
            {
                perror("select失败");
                return -1;
            }
            if (FD_ISSET(0, &tempfds))
            {
                scanf("%s", buf);
                send(sockfd, buf, N, 0);
            }
            if (FD_ISSET(sockfd, &tempfds))
            {
                int ret = recv(sockfd, buf, N, 0);
                printf("服务端:%s\n", buf);
            }
        }
    }
    close(sockfd);
    return 0;
}

例题:创建全双工服务端(既能接收也能发送)

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

#define ERR_MSG(msg)                           \
    do                                         \
    {                                          \
        fprintf(stderr, "line:%d ", __LINE__); \
        perror(msg);                           \
    } while (0)

int main(int argc, char const *argv[])
{
    if (argc != 2)
    {
        printf("用法:<port>\n");
        return -1;
    }
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    printf("sorkfd:%d\n", sockfd);
    // 2.bind绑定IP和Port端口号
    struct sockaddr_in saddr, caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    // saddr.sin_addr.s_addr = inet_addr("192.168.50.213");
    socklen_t addrlen = sizeof(saddr);
#if 0
    saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
#else
    saddr.sin_addr.s_addr = INADDR_ANY;
#endif

    if (bind(sockfd, (struct sockaddr *)&saddr, addrlen) < 0)
    {
        ERR_MSG("bind失败");
        return -1;
    }
    printf("bind成功\n");

    // 3.监听listen将主动套接字变为被动套接字
    if (listen(sockfd, 7) < 0)
    {
        ERR_MSG("lisren失败");
        return -1;
    }
    printf("listen成功\n");
    while (1)
    {
        // 4.accept阻塞等待链接
        int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &addrlen);
        if (acceptfd < 0)
        {
            ERR_MSG("accept失败\n");
            return -1;
        }
        printf("acceptfd:%d\n", acceptfd);
        printf("客户端ip:%s\t 端口号:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));

        // 5.发送
#define N 64
        char buf[N];
        while (1)
        {
            // 第一步:建表初始化
            fd_set readfds, tempfds;
            FD_ZERO(&readfds);
            // 第二步:填表
            FD_SET(0, &readfds);
            FD_SET(acceptfd, &readfds);
            // 第三步:监听表
            while (1)
            {
                memset(buf, 0, N);
                tempfds = readfds;
                int ret = select(5, &tempfds, NULL, NULL, NULL);
                // 第四步:判断,操作
                if (ret == -1)
                {
                    perror("select失败");
                    return -1;
                }
                if (FD_ISSET(0, &tempfds))
                {
                    scanf("%s", buf);
                    send(acceptfd, buf, N, 0);
                }
                if (FD_ISSET(acceptfd, &tempfds))
                {
                    int ret = recv(acceptfd, buf, N, 0);
                    printf("客户端:%s\n", buf);
                }
            }
        }

        close(sockfd);
        return 0;
    }
}

 

  • 16
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值