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

相关文章
|
监控 应用服务中间件 Linux
掌握并发模型:深度揭露网络IO复用并发模型的原理。
总结,网络 I/O 复用并发模型通过实现非阻塞 I/O、引入 I/O 复用技术如 select、poll 和 epoll,以及采用 Reactor 模式等技巧,为多任务并发提供了有效的解决方案。这样的模型有效提高了系统资源利用率,以及保证了并发任务的高效执行。在现实中,这种模型在许多网络应用程序和分布式系统中都取得了很好的应用成果。
394 35
|
Ubuntu 网络协议 Unix
02理解网络IO:实现服务与客户端通信
网络IO指客户端与服务端通过网络进行数据收发的过程,常见于微信、QQ等应用。本文详解如何用C语言实现一个支持多客户端连接的TCP服务端,涉及socket编程、线程处理及通信流程,并分析“一消息一线程”模式的优缺点。
673 0
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
724 23
|
机器学习/深度学习 API Python
Python 高级编程与实战:深入理解网络编程与异步IO
在前几篇文章中,我们探讨了 Python 的基础语法、面向对象编程、函数式编程、元编程、性能优化、调试技巧、数据科学、机器学习、Web 开发和 API 设计。本文将深入探讨 Python 在网络编程和异步IO中的应用,并通过实战项目帮助你掌握这些技术。
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
10月前
|
Java Unix Go
【Java】(8)Stream流、文件File相关操作,IO的含义与运用
Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。!但本节讲述最基本的和流与 I/O 相关的功能。我们将通过一个个例子来学习这些功能。
385 1
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
Java 大数据 API
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
1054 12
|
Java 数据处理
Java IO 接口(Input)究竟隐藏着怎样的神秘用法?快来一探究竟,解锁高效编程新境界!
【8月更文挑战第22天】Java的输入输出(IO)操作至关重要,它支持从多种来源读取数据,如文件、网络等。常用输入流包括`FileInputStream`,适用于按字节读取文件;结合`BufferedInputStream`可提升读取效率。此外,通过`Socket`和相关输入流,还能实现网络数据读取。合理选用这些流能有效支持程序的数据处理需求。
774 2
|
XML 存储 JSON
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。