Linux C/C++之IO多路复用(select)

简介: 这篇文章主要介绍了TCP的三次握手和四次挥手过程,TCP与UDP的区别,以及如何使用select函数实现IO多路复用,包括服务器监听多个客户端连接和简单聊天室场景的应用示例。

1. TCP的连接与断开

1.1 创建连接过程(三次握手)

  1. 客户端向服务器发送连接请求SYN
  2. 服务器接收到连接请求SYN后, 向客户端发送收到指令ACK和连接请求SYN
  3. 客户端收到服务器发送的ACK和SYN后向服务器发送收到指令ACK

1.2 断开连接过程(四次挥手)

  1. 客户端向服务器发送断开请求FIN
  2. 服务器接收到客户端发送的断开请求FIN后向客户端发送收到指令ACK
  3. 服务器检查是否还有没有收发完的数据, 如果数据已经收发完毕, 服务器向客户端发送断开请求FIN
  4. 客户端接收到服务器发来的断开请求后, 检查是否还有没有接收完的数据,如果没有就向服务器发送收到指令ACK

2. TCP与UDP的区别

  1. TCP有连接, UDP没有连接
  2. TCP是数据流, UDP是数据报文
  3. TCP收发数据相对慢, UDP收发数据相对快(局域网内传输数据用UDP相对较好,它可以极大限度地利用带宽)
  4. TCP安全,稳定,可靠;UDP不安全,不稳定,不可靠(安全: 数据相对不容易被窃取 稳定: 几乎没有传输速率的变化 可靠: 一定能收到数据)
  5. TCP有序(先发送的数据先到, 后发送的数据后到), 数据有边界;UDP无序(可能后发送的数据会先到),数据无边界

3. IO多路复用之select

3.1 select函数

//select函数原型
//监视放在里面的描述符号,有反应返回1, 没有反应返回-1
int select(int nfds,                  //描述符号数量,最大描述符号数加一
           fd_set *readfds,           //描述符号集合(读取)   
           fd_set *writefds,          //描述符号集合(写入)
           fd_set *exceptfds,         //描述符号集合(异常)
           struct timeval *timeout);  //延时


void FD_CLR(int fd,fd_set *set);   //将fd从set中删除
int  FD_ISSET(int fd,fd_set *set); //判断fd是否在set中(是返回非0,否返回0)
void FD_SET(int fd,fd_set *set);   //将fd添加到set中
void FD_ZERO(fd_set *set);         //将set置为0(清空)

3.2 select函数实现监视标准输入 0

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

int main(){

    fd_set fds; //描述符号集合

    FD_ZERO(&fds);  //置零
    FD_SET(0,&fds); //将标准输入设备 0 添加到描述符号集合

    int r;
    char buff[1024] = {0};
    while(1){
        //使用一次阻塞替代多次阻塞
        r = select(1,&fds,NULL,NULL,NULL);
        if(r > 0){
            printf("%d有动静!\n",r);
            scanf("%s",buff);
            printf("接收到了:%s\n",buff);
        }
    }

    return 0;
}

3.3 select函数实现服务器连接多个客户端

服务器(server)端

//服务器端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>        
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#include <sys/select.h>
#include <fcntl.h>

//最多允许的客户端数量
#define NUM 100

int serverSocket,clientSocket[NUM];
int currentNum = 0;      //当前客户端数量

void hand(int val){
    //7. 关闭连接
    for(int i = 0;i < NUM; i++){
        if(-1 != clientSocket[i])
            close(clientSocket[i]);
    }
    close(serverSocket);
    printf("bye bye!\n");
    exit(0);
}
int main(int argc,char* argv[]){
    if(argc != 3) printf("请输入ip地址和端口号!\n"),exit(0);
    printf("ip: %s     port:%d\n",argv[1],atoi(argv[2]));

    signal(SIGINT,hand);

    //1. 创建socket 参数一: 协议类型(版本) 参数二: 通信媒介 参数三: 保护方式
    serverSocket = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == serverSocket) printf("创建socket失败:%m\n"),exit(-1);
    printf("创建socket成功!\n");

    //2. 创建服务器协议地址簇
    struct sockaddr_in sAddr = { 0 };
    sAddr.sin_family = AF_INET;        //协议类型 和socket函数第一个参数一致
    sAddr.sin_addr.s_addr = inet_addr(argv[1]);  //将字符串转整数
    sAddr.sin_port = htons(atoi(argv[2]));    //将字符串转整数,再将小端转换成大端

    //3. 绑定服务器协议地址簇
    int r = bind(serverSocket,(struct sockaddr*)&sAddr,sizeof sAddr);
    if(-1 == r) printf("绑定失败:%m\n"),close(serverSocket),exit(-2);
    printf("绑定成功!\n");

    //4. 监听  
    r = listen(serverSocket,10);   //数量
    if(-1 == r) printf("监听失败:%m\n"),close(serverSocket),exit(-3);
    printf("监听成功!\n");


    //初始化客户端描述符号数组
    for (int i = 0; i < NUM; ++i){
        clientSocket[i] = -1;
    }

    //开始监视
    //不仅需要监视serverSocket还要监视每一个返回回来的clientSocket
    fd_set fds;

    int maxFd;       //最大描述符号
    struct sockaddr_in cAddr = {0};
    int len = sizeof(cAddr);
    int cfd;

    char buff[1024] = {0};

    maxFd = 0;
    maxFd = ((maxFd > serverSocket) ? maxFd : serverSocket);

    while(1){

        FD_ZERO(&fds);   //清空

        FD_SET(serverSocket,&fds);    //将服务器socketFd放到监视集合之中

        //将客户端socketFd放到监视集合之中
        for (int i = 0; i < NUM; ++i){
            if(-1 != clientSocket[i]){
                FD_SET(clientSocket[i],&fds);
            }
        }

        //开始监视
        r = select(maxFd+1,&fds,NULL,NULL,NULL);
        if(-1 == r)
            printf("服务器崩溃:%m\n"),close(serverSocket),exit(-1);
        else if(0 == r){
            printf("服务器处于等待状态!\n");
            continue;
        }else{
            //检查是不是serverSocket的动静
            if(FD_ISSET(serverSocket,&fds)){
                cfd = accept(serverSocket,NULL,NULL);
                if(-1 == cfd){
                    printf("客户端连接失败!\n");
                }else{
                    printf("有客户端连接上服务器了:%d\n",cfd);

                    //保存客户端描述符号
                    for (int i = 0; i < NUM; ++i){
                        if(-1 == clientSocket[i]){
                            clientSocket[i] = cfd;
                            maxFd = ((maxFd > cfd) ? maxFd : cfd);
                            break;
                        }
                    }
                }
            }
        }

        //检查客户端是否有动静
        for (int i = 0; i < NUM; ++i){
            if(-1 != clientSocket[i] && FD_ISSET(clientSocket[i],&fds)){
                r = recv(clientSocket[i],buff,1023,0);
                if(r > 0){
                    buff[r] = 0;
                    printf("%d >> %s\n",clientSocket[i], buff);
                }else{
                    printf("客户端: %d 已经断开连接了\n",clientSocket[i]);
                    clientSocket[i] = -1;
                }
            }
        }
    }

    return 0;
}

客户(Client)端

//客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>        
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>

int clientSocket;
void hand(int val){
    //5. 关闭连接
    close(clientSocket);
    printf("bye bye!\n");
    exit(0);
}
int main(int argc,char* argv[]){
    if(argc != 3) printf("请输入ip地址和端口号!\n"),exit(0);
    printf("ip: %s     port:%d\n",argv[1],atoi(argv[2]));

    signal(SIGINT,hand);

    //1. 创建socket 参数一: 协议类型(版本) 参数二: 通信媒介 参数三: 保护方式
    clientSocket = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == clientSocket) printf("创建socket失败:%m\n"),exit(-1);
    printf("创建socket成功!\n");

    //2. 创建服务器协议地址簇
    struct sockaddr_in cAddr = { 0 };
    cAddr.sin_family = AF_INET;
    cAddr.sin_addr.s_addr = inet_addr(argv[1]);  //将字符串转整数
    cAddr.sin_port = htons(atoi(argv[2]));    //将字符串转整数,再将小端转换成大端

    //3.连接服务器
    int r = connect(clientSocket,(struct sockaddr*)&cAddr,sizeof cAddr);
    if(-1 == r) printf("连接服务器失败:%m\n"),close(clientSocket),exit(-2);
    printf("连接服务器成功!\n");


    //4. 通信
    char buff[256] = {0};
    while(1){
        printf("你想要发送:");
        scanf("%s",buff);
        send(clientSocket,buff,strlen(buff),0);
    }

    return 0;
}

3.4 select函数实现简单聊天室

服务器(Server)端

//服务器端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>        
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#include <sys/select.h>
#include <fcntl.h>

//最多允许的客户端数量
#define NUM 100

int serverSocket,clientSocket[NUM];
int currentNum = 0;      //当前客户端数量

void hand(int val){
    //7. 关闭连接
    for(int i = 0;i < NUM; i++){
        if(-1 != clientSocket[i])
            close(clientSocket[i]);
    }
    close(serverSocket);
    printf("bye bye!\n");
    exit(0);
}

int main(int argc,char* argv[]){
    if(argc != 3) printf("请输入ip地址和端口号!\n"),exit(0);
    printf("ip: %s     port:%d\n",argv[1],atoi(argv[2]));

    signal(SIGINT,hand);

    //1. 创建socket 参数一: 协议类型(版本) 参数二: 通信媒介 参数三: 保护方式
    serverSocket = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == serverSocket) printf("创建socket失败:%m\n"),exit(-1);
    printf("创建socket成功!\n");

    //2. 创建服务器协议地址簇
    struct sockaddr_in sAddr = { 0 };
    sAddr.sin_family = AF_INET;        //协议类型 和socket函数第一个参数一致
    sAddr.sin_addr.s_addr = inet_addr(argv[1]);  //将字符串转整数
    sAddr.sin_port = htons(atoi(argv[2]));    //将字符串转整数,再将小端转换成大端

    //3. 绑定服务器协议地址簇
    int r = bind(serverSocket,(struct sockaddr*)&sAddr,sizeof sAddr);
    if(-1 == r) printf("绑定失败:%m\n"),close(serverSocket),exit(-2);
    printf("绑定成功!\n");

    //4. 监听  
    r = listen(serverSocket,10);   //数量
    if(-1 == r) printf("监听失败:%m\n"),close(serverSocket),exit(-3);
    printf("监听成功!\n");


    //初始化客户端描述符号数组
    for (int i = 0; i < NUM; ++i){
        clientSocket[i] = -1;
    }
    //开始监视
    //不仅需要监视serverSocket还要监视每一个返回回来的clientSocket
    fd_set fds;

    int maxFd;       //最大描述符号
    struct sockaddr_in cAddr = {0};
    int len = sizeof(cAddr);
    int cfd;

    char buff[1024] = {0};

    maxFd = 0;
    maxFd = ((maxFd > serverSocket) ? maxFd : serverSocket);

    while(1){
        FD_ZERO(&fds);   //清空监视集合

        FD_SET(serverSocket,&fds);    //将服务器socketFd放到监视集合之中

        //将客户端socketFd放到监视集合之中
        for (int i = 0; i < NUM; ++i){
            if(-1 != clientSocket[i]){
                FD_SET(clientSocket[i],&fds);
            }
        }

        //开始监视
        r = select(maxFd+1,&fds,NULL,NULL,NULL);
        if(-1 == r)
            printf("服务器崩溃:%m\n"),close(serverSocket),exit(-1);
        else if(0 == r){
            printf("服务器处于等待状态!\n");
            continue;
        }else{
            //检查是不是serverSocket的动静
            if(FD_ISSET(serverSocket,&fds)){
                cfd = accept(serverSocket,NULL,NULL);
                if(-1 == cfd){
                    printf("客户端连接失败!\n");
                }else{
                    printf("有客户端连接上服务器了:%d\n",cfd);

                    //保存客户端描述符号
                    for (int i = 0; i < NUM; ++i){
                        if(-1 == clientSocket[i]){
                            clientSocket[i] = cfd;
                            maxFd = ((maxFd > cfd) ? maxFd : cfd);
                            break;
                        }
                    }
                }
            }
        }
        //检查客户端是否有动静
        for (int i = 0; i < NUM; ++i){
            if(-1 != clientSocket[i] && FD_ISSET(clientSocket[i],&fds)){
                r = recv(clientSocket[i],buff,1023,0);
                if(r > 0){
                    buff[r] = 0;
                    printf("%d >> %s\n",clientSocket[i], buff);

                    //服务器将数据转发给每一个在线的客户端(除了发消息给服务器的客户端)
                    char tBuff[2048];
                    sprintf(tBuff,"来自%d客户端发给服务器的消息:%s",clientSocket[i],buff);
                    for(int j = 0; j < NUM; j++){
                        if(-1 != clientSocket[j] && clientSocket[i] != clientSocket[j]){
                            send(clientSocket[j],tBuff,strlen(tBuff),0);
                        }
                    }
                }else{
                    printf("客户端: %d 已经断开连接了\n",clientSocket[i]);
                    clientSocket[i] = -1;
                }
            }
        }
    }

    return 0;
}

客户(Client)端

//客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>        
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>

int clientSocket;
void hand(int val){
    //5. 关闭连接
    close(clientSocket);
    printf("bye bye!\n");
    exit(0);
}
int main(int argc,char* argv[]){
    if(argc != 3) printf("请输入ip地址和端口号!\n"),exit(0);
    printf("ip: %s     port:%d\n",argv[1],atoi(argv[2]));

    signal(SIGINT,hand);

    //1. 创建socket 参数一: 协议类型(版本) 参数二: 通信媒介 参数三: 保护方式
    clientSocket = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == clientSocket) printf("创建socket失败:%m\n"),exit(-1);
    printf("创建socket成功!\n");

    //2. 创建服务器协议地址簇
    struct sockaddr_in cAddr = { 0 };
    cAddr.sin_family = AF_INET;
    cAddr.sin_addr.s_addr = inet_addr(argv[1]);  //将字符串转整数
    cAddr.sin_port = htons(atoi(argv[2]));    //将字符串转整数,再将小端转换成大端

    //3.连接服务器
    int r = connect(clientSocket,(struct sockaddr*)&cAddr,sizeof cAddr);
    if(-1 == r) printf("连接服务器失败:%m\n"),close(clientSocket),exit(-2);
    printf("连接服务器成功!\n");


    //开始监视
    //不仅要监视标准输入设备, 还要监视clientSocket服务器是否发送数据
    fd_set fds;

    int maxFd = clientSocket > 0 ? clientSocket : 0;
    char buff[2048] = {0};
    while(1){
        //清空集合
        FD_ZERO(&fds);
        //将标准输入输出放入到集合中
        FD_SET(0,&fds);
        //将clientSocket放入到监视集合中
        FD_SET(clientSocket,&fds);

        //开始监视
        r = select(maxFd + 1, &fds, NULL,NULL,NULL);
        if(-1 == r)
            printf("客户端崩溃:%m\n"),close(clientSocket),exit(-1);
        else if(0 == r){
            printf("客户端处于等待状态!\n");
            continue;
        }else{
            memset(buff,0,2048);
            //如果 0 有动静就向服务器发消息
            if(FD_ISSET(0,&fds)){
                scanf("%s",buff);
                send(clientSocket,buff,strlen(buff),0);
                continue;
            }
            //如果 clientSocket有动静就接收服务器发来的消息
            if(FD_ISSET(clientSocket,&fds) && -1 != clientSocket){
                memset(buff,0,2048);
                printf("服务器发来了客户端的消息!\n");
                r = recv(clientSocket,buff,2047,0);
                if(r > 0){
                    buff[r] = 0;
                    printf("服务器发来消息 >> %s\n",buff);
                }
            }
        }
    }

    return 0;
}

相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
存储 Linux C语言
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
820 1
Linux C/C++之IO多路复用(aio)
|
11月前
|
监控 应用服务中间件 Linux
掌握并发模型:深度揭露网络IO复用并发模型的原理。
总结,网络 I/O 复用并发模型通过实现非阻塞 I/O、引入 I/O 复用技术如 select、poll 和 epoll,以及采用 Reactor 模式等技巧,为多任务并发提供了有效的解决方案。这样的模型有效提高了系统资源利用率,以及保证了并发任务的高效执行。在现实中,这种模型在许多网络应用程序和分布式系统中都取得了很好的应用成果。
302 35
|
10月前
|
Linux C语言 网络架构
Linux的基础IO内容补充-FILE
而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf和fwrite函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和fwrite函数打印的数据就有两份。此时我们就可以知道,
259 0
|
10月前
|
存储 Linux Shell
Linux的基础IO
那么,这里我们温习一下操作系统的概念我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。这就是因为在根本上操作系统确实像银行一样,并不完全信任用户程序,因为直接开放底层资源(如内存、磁盘、硬件访问权限)给用户程序会带来巨大的风险。所以就向银行一样他的服务是由工作人员隔着一层玻璃,然后对顾客进行服务的。
146 0
|
存储 网络协议 Linux
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
631 34
|
Linux API C语言
Linux基础IO
Linux基础IO操作是系统管理和开发的基本技能。通过掌握文件描述符、重定向与管道、性能分析工具、文件系统操作以及网络IO命令等内容,可以更高效地进行系统操作和脚本编写。希望本文提供的知识和示例能帮助读者更深入地理解和运用Linux IO操作。
308 14
|
存储 JSON Java
细谈 Linux 中的多路复用epoll
大家好,我是 V 哥。`epoll` 是 Linux 中的一种高效多路复用机制,用于处理大量文件描述符(FD)事件。相比 `select` 和 `poll`,`epoll` 具有更高的性能和可扩展性,特别适用于高并发服务器。`epoll` 通过红黑树管理和就绪队列分离事件,实现高效的事件处理。本文介绍了 `epoll` 的核心数据结构、操作接口、触发模式以及优缺点,并通过 Java NIO 的 `Selector` 类展示了如何在高并发场景中使用多路复用。希望对大家有所帮助,欢迎关注威哥爱编程,一起学习进步。
463 6
|
Ubuntu Linux 编译器
Linux/Ubuntu下使用VS Code配置C/C++项目环境调用OpenCV
通过以上步骤,您已经成功在Ubuntu系统下的VS Code中配置了C/C++项目环境,并能够调用OpenCV库进行开发。请确保每一步都按照您的系统实际情况进行适当调整。
2975 3
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
333 0
Linux C/C++之线程基础
下一篇
开通oss服务