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;
}

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
2月前
|
监控 应用服务中间件 Linux
掌握并发模型:深度揭露网络IO复用并发模型的原理。
总结,网络 I/O 复用并发模型通过实现非阻塞 I/O、引入 I/O 复用技术如 select、poll 和 epoll,以及采用 Reactor 模式等技巧,为多任务并发提供了有效的解决方案。这样的模型有效提高了系统资源利用率,以及保证了并发任务的高效执行。在现实中,这种模型在许多网络应用程序和分布式系统中都取得了很好的应用成果。
97 35
|
9月前
|
存储 Linux C语言
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
359 1
Linux C/C++之IO多路复用(aio)
|
9月前
|
Linux C++
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
231 0
Linux C/C++之IO多路复用(poll,epoll)
|
1月前
|
JSON 自然语言处理 Linux
linux命令—tree
tree是一款强大的Linux命令行工具,用于以树状结构递归展示目录和文件,直观呈现层级关系。支持多种功能,如过滤、排序、权限显示及格式化输出等。安装方法因系统而异常用场景包括:基础用法(显示当前或指定目录结构)、核心参数应用(如层级控制-L、隐藏文件显示-a、完整路径输出-f)以及进阶操作(如磁盘空间分析--du、结合grep过滤内容、生成JSON格式列表-J等)。此外,还可生成网站目录结构图并导出为HTML文件。注意事项:使用Tab键补全路径避免错误;超大目录建议限制遍历层数;脚本中推荐禁用统计信息以优化性能。更多详情可查阅手册mantree。
linux命令—tree
|
1月前
|
Unix Linux
linux命令—cd
`cd` 命令是 Linux/Unix 系统中用于切换工作目录的基础命令。支持相对路径与绝对路径,常用选项如 `-L` 和 `-P` 分别处理符号链接的逻辑与物理路径。实际操作中,可通过 `cd ..` 返回上级目录、`cd ~` 回到家目录,或利用 `cd -` 在最近两个目录间快速切换。结合 Tab 补全和 `pwd` 查看当前路径,能显著提升效率。此外,需注意特殊字符路径的正确引用及脚本中绝对路径的优先使用。
|
24天前
|
Linux
Linux命令拓展:为cp和mv添加进度显示
好了,就这样,让你的Linux复制体验充满乐趣吧!记住,每一个冷冰冰的命令背后,都有方法让它变得热情起来。
78 8
|
29天前
|
安全 Linux 定位技术
Linux环境下必备的基础命令概览
以上就是Linux系统中的基本命令和工具,掌握它们就能帮你在Linux世界里游刃有余。这其实就像是学习驾驭一辆新车,熟悉了仪表盘,调整好了座椅,之后的旅程就只需要享受风驰电掣的乐趣了。
46 4
|
2月前
|
Ubuntu 搜索推荐 Linux
详解Ubuntu的strings与grep命令:Linux开发的实用工具。
这就是Ubuntu中的strings和grep命令,透明且强大。我希望你喜欢这个神奇的世界,并能在你的Linux开发旅程上,通过它们找到你的方向。记住,你的电脑是你的舞台,在上面你可以做任何你想做的事,只要你敢于尝试。
155 32
|
1月前
|
Unix Linux
linux命令—pwd
`pwd` 是 Linux/Unix 系统中的基础命令,用于显示用户当前所在的工作目录路径,帮助确认在文件系统中的位置。其核心功能包括打印逻辑路径(默认,-L 选项)和物理路径(-P 选项)。典型应用场景涵盖确认当前位置、调试符号链接问题及脚本编程中动态获取与操作路径。使用时需注意符号链接的区别、参数选择以及特殊字符处理,确保命令正确执行并满足需求。