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

相关文章
|
1月前
|
网络协议 安全 Linux
Linux C/C++之IO多路复用(select)
这篇文章主要介绍了TCP的三次握手和四次挥手过程,TCP与UDP的区别,以及如何使用select函数实现IO多路复用,包括服务器监听多个客户端连接和简单聊天室场景的应用示例。
90 0
|
1月前
|
存储 Linux C语言
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
85 1
Linux C/C++之IO多路复用(aio)
|
1月前
|
Linux C++
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
24 0
Linux C/C++之IO多路复用(poll,epoll)
|
1月前
|
Java Linux
【网络】高并发场景处理:线程池和IO多路复用
【网络】高并发场景处理:线程池和IO多路复用
45 2
|
1月前
|
监控 网络协议 Java
IO 多路复用? 什么是 IO 多路复用? 简单示例(日常生活)来解释 IO 多路复用 一看就懂! 大白话,可爱式(傻瓜式)教学! 保你懂!
本文通过日常生活中的简单示例解释了IO多路复用的概念,即一个线程通过监控多个socket来处理多个客户端请求,提高了效率,同时介绍了Linux系统中的select、poll和epoll三种IO多路复用的API。
124 2
|
2月前
|
消息中间件 NoSQL Java
面试官:谈谈你对IO多路复用的理解?
面试官:谈谈你对IO多路复用的理解?
49 0
面试官:谈谈你对IO多路复用的理解?
|
2月前
|
网络协议 Java Linux
高并发编程必备知识IO多路复用技术select,poll讲解
高并发编程必备知识IO多路复用技术select,poll讲解
|
4月前
|
存储 Java Unix
(八)Java网络编程之IO模型篇-内核Select、Poll、Epoll多路复用函数源码深度历险!
select/poll、epoll这些词汇相信诸位都不陌生,因为在Redis/Nginx/Netty等一些高性能技术栈的底层原理中,大家应该都见过它们的身影,接下来重点讲解这块内容。
|
6天前
|
SQL 安全 网络安全
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享
【10月更文挑战第40天】在数字化时代,网络安全和信息安全已成为我们生活中不可或缺的一部分。本文将介绍网络安全漏洞、加密技术以及安全意识等方面的知识,帮助读者更好地了解网络安全的重要性,并提供一些实用的技巧和建议,以保护个人和组织的信息安全。
29 6
|
1天前
|
安全 网络安全 数据安全/隐私保护
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享
在数字化时代,网络安全和信息安全已成为我们生活中不可或缺的一部分。本文将介绍网络安全漏洞、加密技术和安全意识等方面的知识,并提供一些实用的技巧和建议,帮助读者更好地保护自己的网络安全和信息安全。
下一篇
无影云桌面