Linux网络编程:I/O复用

本文介绍了IO多路复用的概念,它允许多个网络连接共用一个IO线程,减少了线程切换的开销。文章详细讲解了Select模式的使用,包括fd_set结构体和相关宏定义,以及select函数的参数和返回值。通过一个服务器示例,展示了如何使用Select模式监听和处理来自标准输入和多个客户端的连接请求。最后,提到了其他两种IO多路复用模式Poll和Epoll。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程过多会占用很多的资源:
在空间方面,线程是有内存开销的,1000个线程就要512M或2G内存;
在时间方面,线程切换是有CPU开销的,大量线程会让时间花在上下文切换。

1. IO多路复用

IO多路复用: 多个网络连接复用一个IO线程

使用一个线程来检查I/O流(Socket)的就绪状态。通过记录跟踪每个I/O流(Socket)的状态,来同时管理多个I/O流 。
在这里插入图片描述

多个Socket复用功能是在内核驱动实现的。

2. 分类

2.1 Select模式

结构体:
fd_set:描述符集合( long类型数组 )

宏定义:

参数含义
FD_ZERO(fd_set *fdset)清空文件描述符集
FD_SET(int fd,fd_set *fdset)设置监听的描述符(把监听的描述符设置为1)
FD_CLR(int fd,fd_set *fdset)清除监听的描述符(把监听的描述符设置为0)
FD_ISSET(int fd,fd_set *fdset)判断描述符是否设置(判断描述符是否设置为1)
FD_SETSIZE256

函数:
int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout)

参数含义
maxfd需要监视的最大的文件描述符值+1
rdset需要检测的可读文件描述符的集合
wrset需要检测的可写文件描述符的集合
exset需要检测的异常文件描述符的集合
timeout超时时间

返回值:

返回值含义
-1出错
=0超时
>0获取到数据

编码过程:

  1. 定义描述符集
  2. 清空描述符集
  3. 设置指定的描述符并获取最大的描述符值+1
  4. 等待描述符就绪
  5. 判断已就绪的描述符,并做对应处理。

在这里插入图片描述
程序:
服务器(可以对应上编码过程):

#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <thread>
#include <list>
#include <sys/select.h>
#include <algorithm>
using namespace std;

int main(int argc,char* argv[]) {
    if(3 != argc) {
        printf("argument error\n");
        printf("Usage:%s server_ip server_port\n",argv[0]);
        return 1;
    }

    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == fd) {
        perror("open socket error");
        return 1;
    }

    // 设置属性:端口释放
    int flag = 1;
    setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag));

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    inet_aton(argv[1],&addr.sin_addr);
    addr.sin_port = htons(atoi(argv[2]));
    int res = bind(fd,(sockaddr*)&addr,sizeof(addr));
    if(-1 == res) {
        perror("bind error");
        return 1;
    }

    res = listen(fd,4);
    if(-1 == res) {
        perror("listen error");
        return 1;
    }

    // IO复用
    fd_set fdset;
    FD_ZERO(&fdset);

    FD_SET(STDIN_FILENO,&fdset);
    FD_SET(fd,&fdset);
    list<int> fds = {STDIN_FILENO,fd};

    int maxfdp1 = *max_element(fds.begin(),fds.end())+1;

    while(true) {
    	// 阻塞等待监听读取数据的fd
        if(select(maxfdp1,&fdset,NULL,NULL,NULL) > 0) {        // block
            if(FD_ISSET(STDIN_FILENO,&fdset)) {           // 标准输入
                string s;
                cin >> s;
                for(auto connfd:fds) {
                    if(connfd == STDIN_FILENO || connfd == fd) continue;
                    write(connfd,s.c_str(),s.size()+1);
                }
            }
            if(FD_ISSET(fd,&fdset)) {               // 新的连接
                sockaddr_in remote_addr;
                socklen_t len = sizeof(remote_addr);
                int connfd = accept(fd,(sockaddr*)&remote_addr,&len);      // block
                cout << inet_ntoa(remote_addr.sin_addr) << ":" << ntohs(remote_addr.sin_port) << endl;
                if(-1 == connfd) {
                    perror("accept error");
                    return 1;
                }
                FD_SET(connfd,&fdset);
                fds.push_back(connfd);
            }
            for(auto connfd:fds) {
                if(connfd == STDIN_FILENO || connfd == fd) continue;
                if(FD_ISSET(connfd,&fdset)) {                 // 接收客户端的数据
                    char buffer[256] = {0};
                    int n = read(connfd,buffer,256);    // block
                    if(0 == n) {
                        printf("client exit\n");
                        fds.remove(connfd);
                        FD_CLR(connfd,&fdset);
                        break;
                    }
                    cout << buffer << endl;
                }
            }
            FD_ZERO(&fdset);
            maxfdp1 = *max_element(fds.begin(),fds.end())+1;
            for(auto f:fds) {
                FD_SET(f,&fdset);
            }
        }
    }

    for(auto connfd:fds) close(connfd);
    close(fd);
}

客户端(和之前一样):

#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>    // AF_INET  sockaddr_in
#include <unistd.h>      // write() read() close()
#include <thread>
using namespace std;

int main(int argc,char* argv[]) {
    if(3 != argc) {
        printf("argument error\n");
        printf("Usage:%s server_ip server_port\n",argv[0]);
        return 1;
    }

    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == fd) {
        perror("open socket error");
        return 1;
    }

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    inet_aton(argv[1],&addr.sin_addr);
    addr.sin_port = htons(atoi(argv[2]));    // atoi把字符串转为数字
    int res = connect(fd,(sockaddr*)&addr,sizeof(addr));
    if(-1 == res) {
        perror("connect error");
        return 1;
    }

    // 创建子线程
    thread t( [fd]() {
        for(;;) {
            char buffer[256] = {0};
            int n = read(fd,buffer,256);          // block
            if(0 == n) {
                printf("server exit\n");
                break;
            }
            cout << buffer << endl;
        }
    } );
    t.detach();

    // 主线程
    string s;
    while(cin >> s) {    // block
        write(fd,s.c_str(),s.size()+1);
    }

    close(fd);
}

结果为:
在这里插入图片描述
在这里插入图片描述

除了Select模式,还有 Poll模式 和 Epool模式,使用方法可查

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值