网络模型与 IO 多路复用

这篇具有很好参考价值的文章主要介绍了网络模型与 IO 多路复用。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、基础概念

1. socket

  socket也称作“套接字”,用于描述IP地址和端口,是一个通信链路的描述符。应用程序通常通过“套接字”向对端发出请求或者应答网络请求。

  socket是连接运行在网络上的两个程序之间的通信端点。通信的两端都有socket,它是一个通道,数据在两个socket之间进行传输。socket把复杂的TCP/IP协议族或者UDP/IP协议族隐藏在socket接口后面,对程序员来说,只要用好socket相关的函数,就可以完成数据通信。

网络模型与 IO 多路复用

2. FD:file descriptor**

​   Linux 系统中,把一切都看做是文件(一切皆文件),当进程打开现有文件或创建新文件时,内核向进程 返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文 件,所有执行I/O操作的系统调用都会通过文件描述符。

​   一个进程能够同时打开多个文件,对应需要多个文件描述符,所以需要用一个文件描述符表对文件描述符 进行管理;通常默认大小为1024,也即能容纳1024个文件描述符;

​   在 soket 通信中,当我们调用内核函数创建 socket 后,内核返回给我们的是 socket 对应的文件描述符( fd),所以我们对 socket 的操作基本都是通过 fd 来进行。这个文件描述符 fd 可能是就绪的状态即大于 1 的整 数,也可能是未就绪的状态-1。就绪状态表示客户端或服务端发送的全部数据通过网卡进入到内核缓冲区buf, 这时调用 系统read( )就可以将数据从内核区拷贝到用户缓冲区。

  见阻塞 IO 图。

3. 内核态和用户态

​   线程是操作系统调度CPU的最小单元,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、 堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些 线程在同时执行。线程的实现可以分为两类:用户级线程,内核线线程。(java线程就是内核级线程)

​   虚拟内存被操作系统划分成两块:内核空间和用户空间,内核空间是内核代码运行的地方,用户空间是用 户程序代码运行的地方。当进程运行在内核空间时就处于内核态,当进程运行在用户空间时就处于用户态,为 了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。说起这个概念就是因为线程上下文切换的 概念。虽然线程上下文切换比进程切换成本要低但是,线程切换也是很影响性能的。线程上下文切换就涉及用 户态到内核态的转换。

二、 IO 模型分类

  服务器端编程经常需要构造高性能的IO模型,

1. 常见的IO模型

  • 同步阻塞IO(Blocking IO):即传统的IO模型。

  • 同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。

  • IO多路复用(IO Multiplexing):即经典的Reactor模式(并非23种设计模式之一),有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。epoll有两种工作方式,一种是LT模式(level trigger,水平触发),事件驱动,内核告诉你一个文件描述符是否就绪,然后你可以对就绪的文件描述符进行IO操作。一种是ET模式(edge trigger,边缘触发)。默认情况下是LT模式。

  • 异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。

2. 阻塞和非阻塞

   阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式,主体是IO:

​   阻塞是指IO操作需要彻底完成后才返回到用户空间;

​   而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。

​   阻塞非阻塞的主体是IO。

3. 同步和异步

  同步和异步的概念描述的是用户线程与内核的交互方式,主体是线程:

​   同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;

​   而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后通知用户线程,或者调用用户线程注册的回调函数。

​   同步异步的主体是用户线程。

三、 阻塞IO

  为了方便理解,以下所有代码都是伪代码,知道其表达的意思即可。

  服务端为了处理客户端的连接和请求的数据,写了如下代码。

服务端												 客户端
listenfd = socket();   打开一个网络通信端口
bind(listenfd);        绑定
listen(listenfd);      监听
while(1) { 											 fd = socket();
  connfd = accept(listenfd);   阻塞建立连接			 connect(fd);
          											 write(fd,buf);
  int n = read(connfd, buf);  阻塞读数据               closed(fd);
  doSomeThing(buf);  利用读到的数据做些什么
  close(connfd);     关闭连接,循环等待下一个连接
}

  服务端的线程阻塞在了两个地方,一个是 accept 函数,一个是 read 函数。

  read函数包括两个阶段:
网络模型与 IO 多路复用

  这就是传统的阻塞 IO。如果这个连接的客户端一直不发数据,那么服务端线程将会一直阻塞在 read 函数上不返回,也无法接受其他客户端连接。

四、非阻塞 IO

  为了解决上面的问题,我们需要对 accept函数和 read 函数进行改造。

1、针对 read 函数造成的阻塞

​   这个 read 函数的效果是,如果没有数据到达内核缓冲区时,即第一阶段未完成,立刻返回一个错误值-1,而不是阻塞地等待。

​   操作系统提供了这样的功能,只需要在调用 read 前,将文件描述符设置为非阻塞即可。

服务端																						       
listenfd = socket();   			       打开一个网络通信端口
bind(listenfd);        				   绑定
listen(listenfd);      				   监听
while(1) { 																						
  connfd = accept(listenfd);   	       阻塞建立连接		  
  fcntl(connfd, F_SETFL, O_NONBLOCK);  将文件描述符设置为非阻塞										
  int n = read(connfd, buffer); 	   如果 fd 未就绪,调用 read 会立即返回-1,处理下																					 一个连接
  doSomeThing(buf);  										
  close(connfd);     											
}

  如果 fd 未就绪,调用 read 会立即返回-1,如果就绪,就会阻塞式的 read。

2、针对 accept函数造成的阻塞(IO 多路复用)

​   有一种聪明的办法是,每次都创建一个新的进程或线程,去调用 read 函数,并做业务处理。

while(1) { 
  connfd = accept(listenfd);  // 阻塞建立连接
  pthread_create(doWork);  // 创建一个新的线程
}
void doWork() { 
  int n = read(connfd, buf);  // 阻塞读数据
  doSomeThing(buf);  // 利用读到的数据做些什么
  close(connfd);     // 关闭连接,循环等待下一个连接
}

  这样,当给一个客户端建立好连接后,就可以立刻等待新的客户端连接,而不用阻塞在原客户端的 read 请求上。不过,这不叫非阻塞 IO,只不过用了多线程的手段使得主线程没有卡在 read 函数上不往下走罢了。操作系统为我们提供的 read 函数仍然是阻塞的。

  为每个客户端创建一个线程,服务器端的线程资源很容易被耗光。

  当然还有个聪明的办法,我们可以每 accept 一个客户端连接后,将这个文件描述符(connfd)放到一个数组里。然后弄一个新的线程去不断遍历这个数组,调用每一个元素的非阻塞 read 方法。

服务端																						       
listenfd = socket();   						打开一个网络通信端口
bind(listenfd);        					    绑定
listen(listenfd);      						监听
fdlist; 

while(1) { 																						
  connfd = accept(listenfd);   			    阻塞建立连接		  
  fcntl(connfd, F_SETFL, O_NONBLOCK);    	将文件描述符设置为非阻塞
  fdlist.add(connfd);   											
}

新线程去处理
while(1) { 																						
  for(fd <-- fdlist) { 
    if(read(fd) != -1) { 
      doSomeThing();
    }
    close(fd);
    //移除此 fd
  }     											
}

  这样,我们就成功用一个线程处理了多个客户端连接。

​   但这和我们用多线程去将阻塞 IO 改造,看起来是 一样的,这种遍历方式也是我们用户层的小把戏,每次遍 历遇到 read 返回 -1 时仍然是一次浪费资源的系统调用。

  使用 while 循环不断地做系统调用,是不合理的。每次只传给 read 函数一个文件描述符,传一次调用一次。

  我们每次传给 read 函数一批文件描述符到内核,由内核层去遍历,这个问题才能真正解决。

2.1 select 模型

​   select 是操作系统提供的系统调用函数,通过它,我们可以把一个文件描述符的数组发给操作系统, 让 操作系统去遍历,确定哪个文件描述符可以读写, 然后告诉我们去处理。不过,当 select 函数返回后,用 户依然需要遍历刚刚提交给操作系统的 list。只不过,操作系统会将准备就绪的文件描述符做上标识,用户 层将不会再有无意义的系统调用开销。

网络模型与 IO 多路复用

服务端																						       
listenfd = socket();   						打开一个网络通信端口
bind(listenfd);        						绑定
listen(listenfd);      						监听
fdlist;

首先一个线程不断接受客户端连接,并把 socket 文件描述符放到一个 list 里。
while(1) { 																						
  connfd = accept(listenfd);   			    阻塞建立连接		  
  fcntl(connfd, F_SETFL, O_NONBLOCK);    	将文件描述符设置为非阻塞
  fdlist.add(connfd);   											
}

while(1) { 
  // 把一堆文件描述符 list 传给 select 函数
  // 有已就绪的文件描述符就返回,nready 表示有多少个就绪的
  nready = select(list);
    // 用户层依然要遍历,只不过少了很多无效的系统调用
  for(fd <-- fdlist) { 
    if(fd != -1) { 
      // 只读已就绪的文件描述符
      read(fd, buf);
      // 总共只有 nready 个已就绪描述符,不用过多遍历
      if(--nready == 0) 
        break;
    }
  }
}

  存在问题:

  • select 调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)

  • select 在内核层仍然是通过遍历的方式检查文件描述符的就绪状态,是个同步过程,只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)

  • select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)

2.2 poll模型

​   它和 select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制。

2.3 epoll模型

​   针对select 模型的三个问题进行了改进。

  • ​ 内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。

  • ​ 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。

  • ​ 内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。

​   具体,操作系统提供了这三个函数。

//第一步,创建一个 epoll 句柄
int epoll_create(int size);
//第二步,向内核添加、修改或删除要监控的文件描述符。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//第三步,类似发起了 select() 调用
int epoll_wait(int epfd, struct epoll_event *events, int max events, int timeout);

网络模型与 IO 多路复用
网络模型与 IO 多路复用
网络模型与 IO 多路复用

参考文章https://www.dandelioncloud.cn/article/details/1615702819904651265文章来源地址https://www.toymoban.com/news/detail-427817.html

到了这里,关于网络模型与 IO 多路复用的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 网络编程 IO多路复用 [select版] (TCP网络聊天室)

    //head.h                 头文件 //TcpGrpSer.c        服务器端 //TcpGrpUsr.c        客户端 select函数  功能:阻塞函数,让内核去监测集合中的文件描述符是否准备就绪,若准备就绪则解除阻塞。 原型: head.h TcpGrpSer.c TcpGrpUsr.c    

    2024年02月14日
    浏览(53)
  • 【APUE】网络socket编程温度采集智能存储与上报项目技术------多路复用

    作者简介: 一个平凡而乐于分享的小比特,中南民族大学通信工程专业研究生在读,研究方向无线联邦学习 擅长领域:驱动开发,嵌入式软件开发,BSP开发 作者主页:一个平凡而乐于分享的小比特的个人主页 文章收录专栏:网络socket编程之温度采集智能存储与上报项目,本

    2024年04月10日
    浏览(47)
  • IO多路复用中select的TCP服务器模型和poll服务模型

    服务器端 客户端 poll客户端

    2024年02月12日
    浏览(51)
  • 网络通信基础 - 多路复用技术(频分多路复用、时分多路复用、波分多路复用)

    多路复用技术:把多个低速信道组合成一个高速信道的技术 这种技术要用到两个设备,统称为 多路器(MUX) 多路复用器(Multiplexer) :在发送端根据某种约定的规则把多个低带宽的信号复合成一个高带宽的信号 多路分配器(Demultiplexer) :在接收端根据同一规则把高带宽信

    2023年04月23日
    浏览(45)
  • 【Linux网络编程】TCP并发服务器的实现(IO多路复用select)

    服务器模型主要分为两种, 循环服务器 和 并发服务器 。 循环服务器 : 在同一时间只能处理一个客户端的请求。 并发服务器 : 在同一时间内能同时处理多个客户端的请求。 TCP的服务器默认的就是一个循环服务器,原因是有两个阻塞 accept函数 和recv函数 之间会相互影响。

    2024年02月03日
    浏览(82)
  • 【高并发网络通信架构】引入IO多路复用(select,poll,epoll)实现高并发tcp服务端

    目录 一,往期文章 二,基本概念 IO多路复用 select 模型 poll 模型 epoll 模型 select,poll,epoll 三者对比 三,函数清单 1.select 方法 2.fd_set 结构体 3.poll 方法 4.struct pollfd 结构体 5.epoll_create 方法 6.epoll_ctl 方法 7.epoll_wait 方法 8.struct epoll_event 结构体 四,代码实现 select 操作流程 s

    2024年02月12日
    浏览(61)
  • 【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端

    目录 一,往期文章 二,基本概念 IO多路复用 select 模型 poll 模型 epoll 模型 select,poll,epoll 三者对比 三,函数清单 1.select 方法 2.fd_set 结构体 3.poll 方法 4.struct pollfd 结构体 5.epoll_create 方法 6.epoll_ctl 方法 7.epoll_wait 方法 8.struct epoll_event 结构体 四,代码实现 select 操作流程 s

    2024年02月14日
    浏览(48)
  • IO、NIO、IO多路复用

    IO是什么? 网络IO是如何连接的? 下面是一次网络读取内容的I/O示意图,数据先从外设(网卡)到内核空间,再到用户空间(JVM),最后到应用程序的一个过程。 上述一次I/O读取,所谓的阻塞和非阻塞体现在哪里呢? Java最早期的版本的I/O就是这样实现的。当程序调用到读取

    2024年01月20日
    浏览(43)
  • 说说IO多路复用

    IO多路复用 I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流。直白点说:多路指的是多个socket连接,复用指的是复用一个线程进行管理。发明它的原因,是尽量多的提高服务

    2024年02月09日
    浏览(39)
  • IO多路复用详解

    在IO多路复用模型中,引入了一种新的系统调用,查询IO的就绪状态。在Linux系统中,对应的系统调用为select/poll/epoll系统调用。通过该系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核能够将就绪的状态返回给应用程序

    2024年02月08日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包