2.1.1网络io与io多路复用select/poll/epoll

简介: 2.1.1网络io与io多路复用select/poll/epoll

关于网络io,我们可以通过一个服务端-客户端的示例来了解:

这是一段TCP服务端的代码:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
  //open
  //创建网络io
  int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io
  struct sockaddr_in servaddr;
  memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123
  servaddr.sin_family = AF_INET;
    //INADDR_ANY绑定任意网卡,接收任意网卡的数据
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
    servaddr.sin_port = htons(9999);
    if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
    printf("bind failed: %s", strerror(errno));
    return -1;
    }
    listen(sockfd, 10); 
}

值得注意的是htonlhtons都是将主机字节序转换为网络字节序

htonl表示转换四字节的无符号整数,htons表示转换两字节的无符号整数

htonl,  htons,  ntohl, ntohs - convert values between host and network byte order

运行这段程序,可以发现程序没有任何效果,直接退出,而当我们在listen后面加上

getchar();后,程序阻塞,这时通过命令netstat -anop | grep 9999查看端口状态:

发现该端口正处于listen状态,这时通过网络调试助手(充当客户端)连接192.168.209.130:9999发现连接成功。

服务端其实一直都处于listen状态,之后还需要通过accept接受连接

listen(sockfd, 10); 
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(struct sockaddr_in);
    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    getchar();

accept接受客户端的连接,并通过传出参数返回客户端的信息,以及函数返回clientfd,后续该客户端的数据收发都通过该clientfd进行。这也就反应了一个问题,每个客户端连接的都会对应一个clientfd。这时运行程序,程序阻塞等待客户端的连接,但这次是阻塞在accept系统调用上。创建的sockfd默认是阻塞的

而关于阻塞和非阻塞的概念,简单总结就是阻塞会等待有事件发生,非阻塞则是不管有无事件都会立即返回。

我们将sockfd设为非阻塞形式,并将getchar()注释掉再看看效果:

#include <fcntl.h>
  ...
  listen(sockfd, 10);
  //设为非阻塞
    int flags = fcntl(sockfd, F_GETFL, 0);
    flags |= O_NONBLOCK;
    fcntl(sockfd, F_SETFL, flags);
    struct sockaddr_in clientaddr;
    ...
    //getchar();

调用程序发现,程序立即返回,不再阻塞!

现在思考一个问题,连接成功是在listen完成,还是在accept完成呢?

我们可以在listen后加上一个sleep(10);,在accept返回后打印一下返回值

listen(sockfd, 10);
    sleep(10);
    ...
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(struct sockaddr_in);
    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    printf("clientfd: %d\n", clientfd);

程序运行后,立马连接,发现可以连接成功,并且10秒后,accept返回4

说明在listen连接就已经建立成功了,而clientfd为4则是因为,标准输入、标准输出、标准错误、以及sockfd已经占用了0、1、2、3再分配的文件描述符就是4。

接下来进行数据的收发(使用阻塞模式),accept之后调用recvsend

char buffer[BUFFER_LENGTH] = {0};
    int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
    printf("ret: %d, buffer: %s\n", ret, buffer);
    send(clientfd, buffer, ret, 0);

这时收发数据只能进行一次,我们可以加上while循环实现循环收发。若想实现多个客户端连接,并支持收发数据,也把accept放入while循环??形如这样?

while (1) {
        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
        char buffer[BUFFER_LENGTH] = {0};
        int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
        printf("ret: %d, buffer: %s\n", ret, buffer);
        send(clientfd, buffer, ret, 0);
}

我们开多个客户端连接发现确实能连接上服务端,但连接后仍然只能进行一次数据收发。

因为一直阻塞在accept上,服务端只会服务新来的连接的一次数据收发。

那要支持多个客户端连接,并且都能进行多次数据收发该如何做呢?

我们可以将数据收发的工作放在一个线程中循环做:

#include <pthread.h>
void *client_thread(void *arg) {
    int clientfd = *(int*)arg;
    //线程中循环数据收发
    while (1) {
        char buffer[BUFFER_LENGTH] = {0};
        int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
        //recv返回0说明对端关闭连接
        if (ret == 0) {
      close(clientfd);
      break;
    }
        printf("ret: %d, buffer: %s\n", ret, buffer);
        send(clientfd, buffer, ret, 0);
    }
}
...
  while (1) {
        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
        pthread_t threadid;
        pthread_create(&threadid, NULL, client_thread, &clientfd);
    }
...

来一个连接,创建一个线程,该线程循环负责该客户端的数据收发。

但是这种模式存在一个弊端,成千上万个客户端连接,难道要创建对应个数的线程吗?有没有更好的解决办法?有,那便是IO多路复用!

Linux中有三种IO多路复用:select、poll、epoll

下面介绍使用selectpoll的方式:

#include <sys/select.h>
#define BUFFER_LENGTH 1024
listen(sockfd, 10); 
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(struct sockaddr_in);
    fd_set rfds, rset;
    FD_ZERO(&rfds);
    FD_SET(sockfd, &rfds);
    int maxfd = sockfd;
    int clientfd = 0;
    while (1) {
        rset = rfds;
        //这里传入文件描述符最大值加1
        //判断时是形如for(; i < maxfd; i++)所以要加一
        int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
        if (FD_ISSET(sockfd, &rset)) {
            clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
            printf("accept: %d\n", clientfd);
            FD_SET(clientfd, &rfds);
            if (clientfd > maxfd) maxfd = clientfd;
            if(--nready == 0) continue;
        }
        int i = 0;
        for (i = sockfd + 1; i <= maxfd; i++) {
            if (FD_ISSET(i, &rset)) {
                    char buffer[BUFFER_LENGTH] = {0};
                    int ret = recv(i, buffer, BUFFER_LENGTH, 0);
                    if (ret == 0) {
                        close(i);
                        break;
                    }
                    printf("ret: %d, buffer: %s\n", ret, buffer);
                    send(i, buffer, ret, 0);
            }
        }
    }
    getchar();

值得注意的是select是通过判断fd_set中的某些位,从而判断是否发生事件,因此,select所能处理的文件描述符个数是有限的,只有1024个。

下面是poll的使用方式:

#include <poll.h>
#define POLL_SIZE     1024
  listen(sockfd, 10); 
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(struct sockaddr_in);
    struct pollfd fds[POLL_SIZE] = {0};
    fds[sockfd].fd = sockfd;
    fds[sockfd].events = POLLIN;
    int maxfd = sockfd;
    int clientfd = 0;
    while (1) {
      int nready = poll(fds, maxfd + 1, -1);
      if (fds[sockfd].revents & POLLIN) {
              clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
              printf("accept: %d\n", clientfd);
              fds[clientfd].fd = clientfd;
              fds[clientfd].events = POLLIN;
              if (clientfd > maxfd) maxfd = clientfd;
              if(--nready == 0) continue;
        }
        int i = 0;
        for (i = 0; i <= maxfd; i++) {
          if (fds[i].revents & POLLIN) {
             char buffer[BUFFER_LENGTH] = {0};
                    int ret = recv(i, buffer, BUFFER_LENGTH, 0);
                    if (ret == 0) {
                        fds[i].fd = -1;
                        fds[i].events = 0;
                        close(i);
                        break;
                    }
                    printf("ret: %d, buffer: %s\n", ret, buffer);
                    send(i, buffer, ret, 0);
          }
        }
    }
    getchar();

poll相较select,支持的文件描述符数量不受限制,并且每次调用无需重新设置事件,因为内核不会修改,而是通过revent返回。但是他们都有性能瓶颈,他们返回就绪的文件描述符个数,但仍需我们自己去遍历到底是哪个文件描述符上有事件,而epoll解决了这种问题。

文章参考与<零声教育>的C/C++linux服务期高级架构系统教程学习:https://ke.qq.com/course/417774?flowToken=1020253

相关文章
|
26天前
|
Linux C++
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
20 0
Linux C/C++之IO多路复用(poll,epoll)
|
3月前
|
监控
【网络编程】poll函数
【网络编程】poll函数
24 0
|
3月前
|
监控
【网络编程】select函数
【网络编程】select函数
56 0
|
4月前
|
存储 Java Unix
(八)Java网络编程之IO模型篇-内核Select、Poll、Epoll多路复用函数源码深度历险!
select/poll、epoll这些词汇相信诸位都不陌生,因为在Redis/Nginx/Netty等一些高性能技术栈的底层原理中,大家应该都见过它们的身影,接下来重点讲解这块内容。
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
4月前
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
2月前
|
Java 大数据 API
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
|
3月前
|
Java 数据处理
Java IO 接口(Input)究竟隐藏着怎样的神秘用法?快来一探究竟,解锁高效编程新境界!
【8月更文挑战第22天】Java的输入输出(IO)操作至关重要,它支持从多种来源读取数据,如文件、网络等。常用输入流包括`FileInputStream`,适用于按字节读取文件;结合`BufferedInputStream`可提升读取效率。此外,通过`Socket`和相关输入流,还能实现网络数据读取。合理选用这些流能有效支持程序的数据处理需求。
41 2
|
3月前
|
XML 存储 JSON
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。
|
3月前
|
缓存 Java
【IO面试题 一】、介绍一下Java中的IO流
Java中的IO流是对数据输入输出操作的抽象,分为输入流和输出流,字节流和字符流,节点流和处理流,提供了多种类支持不同数据源和操作,如文件流、数组流、管道流、字符串流、缓冲流、转换流、对象流、打印流、推回输入流和数据流等。
【IO面试题 一】、介绍一下Java中的IO流

热门文章

最新文章