【Linux篇】网络编程——I/O复用

目录

一、初识复用

1. 认识复用

2. 复用的优点

3. 复用技术在服务端的应用

二、select 技术

1. 设置文件描述符(fd_set)

2. 文件描述符的控制

(1)FD_ZERO

(2)FD_SET

(3)FD_CLR

(4)FD_ISSET

3. select 函数

4. select 实例

(1)客户端

(2)服务器端

三、epoll 技术

1. 认识 epoll

2. epoll 优点

3. epoll 中的事件保存

4. epoll_create

5. epoll_ctl

6. epoll_wait

7. 条件触发和边缘触发

8. epoll 实例

(1)客户端

(2)服务端(边缘触发模式)


一、初识复用

1. 认识复用

        “复用” 的含义:为了提高物理设备的效率,用最少的物理要素传递最多数据时使用的技术。

        上图远距离的 3 人 可以同时通话的 3 方 对话纸杯电话系统。为使 3 人 同时对话,需准备图中所示系统。

        另外,为了完成 3 人 对话,说话时需同时对着两个纸杯,接听时也需要耳朵同时对准两个纸杯。此时引入复用技术会使通话更加方便,如下图。

2. 复用的优点

  • 减少连线长度。
  • 减少纸杯个数。

3. 复用技术在服务端的应用

        纸杯电话系统引入复用技术后,可以减少纸杯数和连线长度。同样,服务器端引入复用技术
可以减少所需进程数。

        下图模型中引入复用技术,可以减少进程数。重要的是,无论连接多少客户端,提供服务的进程只有1个。

二、select 技术

        上图为从调用 select 函数 到获取结果所经过程。

1. 设置文件描述符(fd_set)

        利用 select 函数 可以同时监视多个文件描述符。

        监视文件描述符可以视为监视套接字。此时首先需要将要监视的文件描述符集中到一起。集中时也要按照监视项(接收、传输、异常)进行区分,即按照上述 3 种 监视项分成 3 类。

        图中最左端的位表示文件描述符 0(所在位置)。如果该位设置为 1,则表示该文件描述符是监视对象。

2. 文件描述符的控制

(1)FD_ZERO

FD_ZERO(fd_set * fdset)

        将 fd_set 变量的 所有位初始化为 0。

(2)FD_SET

FD_SET(int fad, fd_set * fdset)

        在参数 fdset 指向的变量中注册文件描述符 fd 的信息。

(3)FD_CLR

FD_CLR(int fd, fd_set * fdset)

        从参数 fdset 指向的变量中清除文件描述符 fd 的信息。

(4)FD_ISSET

FD_ISSET(int fd, fd_set * fdset)

        用于验证select函数的调用结果。若参数 fdset 指向的变量中包含文件描述符 fd 的信息,则返回“真”。

3. select 函数

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

int select(int maxfd
           ,fd_set * readset
           ,fd_set * writeset
           ,fd_set * exceptset
           ,const struct timeval * timeout);
① maxfd

        监视对象文件 描述符数量。文件描述符的监视范围 与这个参数有关。

② readset

        将所有关注 “是否存在待读取数据” 的文件描述符注册到 fd_set 型变量,并传递其 地址值。

③ writeset

        将所有关注 “是否可传输无阻塞数据” 的文件描述符注册到 fd_set 型变量,并传递其地址值。

④ exceptset

        将所有关注 “是否发生异常” 的文件描述符注册到 fd_set 型变量,并传递其地址值。

⑤ timeout

        调用 select 函数后,为防止 陷入无限阻塞的 状态,传递 超时(time-out)信息。

struct timeval
{
	long tv_sec;	// seconds(秒)
	long tv_usec;  // microseconds(毫秒)
}
⑥ 返回值

        发生错误时 返回 -1,超时返回时 返回 0。因发生关注的事件 返回时,返回大于 0 的值,该值是发生事件的 文件描述符数。

补充:

       ① select 函数 用来验证 3 种 监视项的 变化情况。根据 监视项声明 3 个 fd_set 型变量,分别向其注册文件描述符 信息,并把 变量的地址值 传递到上述函数的 第二 到 第四个参数。

       ② select 函数要求通过 第一个参数 传递监视对象文件 描述符的 数量。因此,需要得到 注册在 fd_set 变量中的 文件描述符数。但 每次新建文件描述符时,其值都会 增 1,故 只需将 最大的 文件描述符值 加 1 再传递到 select 函数 即可。加 1 是因为文件描述符的值 从 0 开始。

        ③ 由上图可知,select函数调用完成后,向其传递的 fd_set 变量中将 发生变化。原来为 1 的所有位均变为 0,但发生变化的文件描述符 对应位 除外。因此,可以认为值 仍为 1 的位置上的文件描述符发生了变化。

4. select 实例

(1)客户端

#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <iostream>
#include <unistd.h>
using namespace std;

#define MAX_LEN  1024

int main(int argc, char const *argv[])
{
    int client_sock;
    struct sockaddr_in server_addr;
    socklen_t server_addr_len;
    char message[MAX_LEN];

    if (argc != 3) {
        cout << "Using: " << argv[0] << " 10.211.55.8 <port>" << endl;
        exit(1);
    }

    // 定义客户端 socket
    client_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (client_sock == -1) {
        perror("socket()");
        exit(1);
    }

    // 连接服务器
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    server_addr.sin_port = htons(atoi(argv[2]));
    server_addr_len = sizeof(server_addr);
    if (connect(client_sock, (struct sockaddr *)&server_addr, server_addr_len) == -1) {
        perror("connect()");
        exit(1);
    }
    else {
        cout << "connect success ·······" << endl;
    }

    for (int i = 0; i < 3; i++) {
        memset(&message, 0, sizeof(message));
        cout << "client send: ";
        cin >> message;
        send(client_sock, message, sizeof(message), 0);
        recv(client_sock, message, MAX_LEN, 0);
        cout << "client recv: " << message << endl;
    }

    close(client_sock);

    return 0;
}

(2)服务器端

#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <iostream>
#include <unistd.h>
using namespace std;

#define MAX_LEN  1024

int main(int argc, char const *argv[])
{
    int server_sock, client_sock;
    struct sockaddr_in server_addr, client_addr;
    struct timeval timeset;
    socklen_t client_addr_len;
    int fd_max, df_num, str_len;
    char message[MAX_LEN];

    if (argc != 2) {
        cout << "Using: " << argv[0] << " <port>" << endl;
        exit(1);
    }

    // 创建服务端 socket
    server_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (server_sock == -1) {
        perror("socket()");
        exit(1);
    }

    // 绑定 IP和端口
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(atoi(argv[1]));
    if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind()");
        exit(1);
    }

    // 开启监听
    if (listen(server_sock, 5) == -1) {
        perror("listen()");
        exit(1);
    }

    // 初始化监听窗口
    fd_set fdset, copy_fdset;
    FD_ZERO(&fdset);
    FD_SET(server_sock, &fdset);
    fd_max = server_sock;

    while (1) {
        // 设置超时
        // timeset.tv_sec = 5;     // 5秒后超时
        // timeset.tv_usec = 0; 

        copy_fdset = fdset;
        if(df_num = select(fd_max+1, &copy_fdset, 0, 0, 0) == -1) {
            perror("select()");
            break;
        } 

        // if (df_num == 0) {
        //     cout << "not client ·······" << endl;
        //     continue;
        // }

        for (int i = 0; i < fd_max + 1; i++) {  // 循环到最大的文件标识符的索引处
            if (FD_ISSET(i, &copy_fdset)) {
                if (i == server_sock) {
                    // 检测到的标识符为 服务端的
                    // 表示服务端的 socket 接收到连接请求
                    client_addr_len = sizeof(client_addr);
                    if ((client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len)) == -1) {
                        perror("accept()");
                        exit(1);
                    }

                    FD_SET(client_sock, &fdset);   // 将新添加的客户端 socket 加入监视队列中
                    if (client_sock > fd_max) fd_max = client_sock;     // 更新最大文件标识符

                    char client_ip[16];
                    inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, 16);
                    cout << "connected client IP: " << client_ip << " post: " << ntohs(client_addr.sin_port) << endl;
                    // cout << "continue ··········" << endl;
                }
                else {
                    // 检测到的标识符不是服务端
                    // 表示为客户端向服务端 传输数据,或者是 发送的断开连接请求
                    str_len = recv(i, message, MAX_LEN, 0);
                    if (str_len == -1) {
                        perror("recv");
                        exit(1);
                    }
                    else if (str_len > 0) {
                        cout << "receive " << i << " client data: " << message << endl;
                        send(i, message, sizeof(message), 0);
                    }
                    else if (str_len == 0) {
                        // 断开连接
                        FD_CLR(i, &fdset);
                        cout << "closed client: " << i << " successed ········" << endl;
                    }
                }
            }
        }
    }

    close(server_sock);

    return 0;
}

三、epoll 技术

1. 认识 epoll

        每次调用 select 函数时向操作系统传递 监视对象信息。应用程序 向操作系统传递数据 将对程序造成 很大负担,而且 无法通过优化代码解决,因此将成为 性能上的致命弱点

        有些函数 不需要操作系统的帮助 就能完成功能,而有些 则必须借助于操作系统。select 函数与 文件描述符有关,更准确地说,是 监视套接字变化的 函数。而 套接字是由 操作系统管理的,所以 select 函数绝对 需要借助于操作系统 才能完成功能。select 函数的这一缺点可以通过 如下方式弥补:

        仅向操作系统传递1次监视对象,监视范围或内容发生变化时只通知发生变化的事项。

2. epoll 优点

  • 无需编写以 监视状态变化为目的的 针对所有文件描述符的 循环语句。
  • 调用对应于 select 函数的 epoll_wait 函数时无需 每次传递监视对象信息。

3. epoll 中的事件保存

        epoll 方式中通过 如下结构体 epoll_event 将发生变化的(发生事件的)文件描述符单独集中到一起。

struct epoll_event
{
    __uint32_t events;
    epoll_data_t data;
}

typedef union epol1_data
{
    void * ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

        声明足够大的 epoll_event 结构体数组后,传递给 epoll_ wait 函数时,发生变化的 文件描述符信息将被填人该数组。因此,无需像 select 函数那样针 对所有文件描述符进行循环。

4. epoll_create

#include <sys/epoll.h>

int epol1_create(int size);
① size        

        epoll 实例的大小。

② 返回

        成功时返回 epoll 文件描述符,失败时返回 -1。

补充:
  1.     调用 epoll_create 函数时创建的文件描述符保存空间称为 “epoll 例程 ” ,但有些情况下名称不同。
  2.     通过 参数 size 传递的值决定 epoll 例程 的大小,但该值 只是向操作系统 提的建议。换言之,size 并非用来决定 epoll 例程 的大小,而 仅供操作系统 参考。
  3.     epoll_create 函数创建的资源 与 套接字相同,也由 操作系统管理。因此,该函数和创建 套接字的 情况相同,也会 返回 文件描述符。也就是说,该函数返回的 文件描述符主要用与于区分 epoll 例程。需要终止时,与 其他文件描述符 相同,也要调用 close 函数。

5. epoll_ctl

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
① epfd

        用于注册监视对象的epoll例程的文件描述符。

② op
EPOLL_CTL_ADD: 将文件描述符注册到epoll例程。
EPOLL_CTL_DEL: 从epoll例程中删除文件描述符。
EPOLL_CTL_MOD: 更改注册的文件描述符的关注事件发生情况。

        用于指定监视对象的添加、删除或更改等操作。

③ fd

        需要注册的监视对象文件描述符。

④ event

         监视对象的事件类型。

⑤ 返回

        成功时返回 0,失败时返回 -1。

补充:
epoll_ctl(A, EPOLL_CTL_ADD, B, C);

表示为:从epoll例程A中注册文件描述符B,主要目的是监视参数C中的事件。
struct epoll_event event;
······
event.events = EPOLLIN;//发生需要读取数据的情况(事件)时
event.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd,&event);
······
EPOLLIN:需要读取数据的情况。
EPOLLOUT:输出缓冲为空,可以立即发送数据的情况。
EPOLLPRI: 收到OOB数据的情况。
EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用。
EPOLLERR:发生错误的情况。
EPOLLET:以边缘触发的方式得到事件通知。
EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件。

6. epoll_wait

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
① epfd

        表示事件发生监视范围的 epoll 例程的 文件描述符。

② events

        保存发生事件的文件描述符集合 的结构体 地址值。

③ maxevents

        第二个参数中 可以保存的 最大事件数。

④ timeout

        以 1/1000 秒 为单位的等待时间,传递 -1 时,一直等待直到 发生事件。

⑤ 返回

        成功时返回发生事件的 文件描述符数,失败时返回 -1。同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。因此,无需像select那样插入针对所有文件描述符的循环。

补充:

        第二个参数 所指缓冲需要动态分配。

int event_cnt;
struct epoll_event * ep_events;
······
ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE); // EPOLL_SIZE 是宏常量
event_Cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE,-1);
······

7. 条件触发和边缘触发

        条件触发(Level Trigger)方式中,只要输入缓冲有数据就会一直通知该事件,就将以事件方式再次注册。

        边缘触发(Edge Trigger)中输入缓冲收到数据时仅注册 1 次 该事件。即使输入缓冲中还留有数据,也不会再进行注册。

8. epoll 实例

(1)客户端

#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <iostream>
#include <unistd.h>
using namespace std;

#define MAX_LEN  1024

int main(int argc, char const *argv[])
{
    int client_sock;
    struct sockaddr_in server_addr;
    socklen_t server_addr_len;
    char message[MAX_LEN];

    if (argc != 3) {
        cout << "Using: " << argv[0] << " 10.211.55.8 <port>" << endl;
        exit(1);
    }

    // 定义客户端 socket
    client_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (client_sock == -1) {
        perror("socket()");
        exit(1);
    }

    // 连接服务器
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    server_addr.sin_port = htons(atoi(argv[2]));
    server_addr_len = sizeof(server_addr);
    if (connect(client_sock, (struct sockaddr *)&server_addr, server_addr_len) == -1) {
        perror("connect()");
        exit(1);
    }
    else {
        cout << "connect success ·······" << endl;
    }

    for (int i = 0; i < 3; i++) {
        memset(&message, 0, sizeof(message));
        cout << "client send: ";
        cin >> message;
        send(client_sock, message, sizeof(message), 0);
        recv(client_sock, message, MAX_LEN, 0);
        cout << "client recv: " << message << endl;
    }

    close(client_sock);

    return 0;
}

(2)服务端(边缘触发模式)

#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/epoll.h>
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
using namespace std;


#define MAX_LEN  4
#define EPOLL_SIZE  50


void setnonblockingmode(int fd);


int main(int argc, char const *argv[])
{
    int server_sock, client_sock;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len;

    int epfd, event_cnt;
    struct epoll_event * ep_events;     // 单独保存发生的事件
    struct epoll_event event;   // 单个时间
    int str_len;
    char message[MAX_LEN];

    if (argc != 2) {
        cout << "Using: " << argv[0] << " <port>" << endl;
        exit(1);
    }

    // 定义服务端 socket
    server_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (server_sock == -1) {
        perror("socket()");
        exit(1);
    }

    // 绑定 IP和端口
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(atoi(argv[1]));
    if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind()");
        exit(1);
    }

    // 开启监听
    if (listen(server_sock, 5) == -1) {
        perror("listen()");
        exit(1);
    }

    epfd = epoll_create(EPOLL_SIZE);    // 创建文件描述符保存空间
    ep_events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * EPOLL_SIZE);  // 为储存事件的空间开辟内存

    setnonblockingmode(server_sock);
    event.events = EPOLLIN;
    event.data.fd = server_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, server_sock, &event);    // 将服务器的 socket 注册进去

    while (1) {
        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);    // 等待有 socket 被填入到 ep_events 中
        if (event_cnt == -1) {
            perror("epoll_wait()");
            break;
        }

        cout << "return epoll_wait" << endl;

        for (int i = 0; i < event_cnt; i++) {
            if (ep_events[i].data.fd == server_sock) {
                // 客户端发来了连接请求
                client_addr_len = sizeof(client_addr);
                client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len);
                if (client_sock == -1) {
                    perror("accept()");
                    exit(1);
                }

                setnonblockingmode(client_sock);
                event.events = EPOLLIN | EPOLLET;   // 边缘触发模式
                event.data.fd = client_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, client_sock, &event);    // 将新的客户端注册进去

                char client_ip[16];
                inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, 16);
                cout << "client connected: IP " << client_ip << " port " << ntohs(client_addr.sin_port) << endl;
            }
            else {
                // 发送信息或者断开连接
                while (1) {     // 循环把输入缓存中的数据全都读完
                    str_len = recv(ep_events[i].data.fd, message, MAX_LEN, 0);
                    if (str_len == 0) {
                        // 断开连接
                        epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                        close(ep_events[i].data.fd);    // 关闭断开连接的 socket
                        cout << "closed client: " << ep_events[i].data.fd << endl;
                    }
                    else if (str_len < 0) {
                        if (errno == EAGAIN) {  // 没有数据可读
                            break;
                        }
                    }
                    else {
                        // cout << "receive client " << ep_events[i].data.fd << " : " << message << endl;
                        send(ep_events[i].data.fd, message, sizeof(message), 0);
                    }
                }
                
            }
        }
    }

    close(server_sock);
    close(epfd);

    return 0;
}


void setnonblockingmode(int fd) {
    // 设置非阻塞模式
    int flag = fcntl(fd, F_GETFL, 0);   // 获取之前设置的属性信息
    fcntl(fd, F_SETFL, flag | O_NONBLOCK);  // 在此基础上添加非阻塞的 O_NONBLOCK
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值