前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >网络IO

网络IO

原创
作者头像
ruochen
修改2021-11-25 12:55:28
4470
修改2021-11-25 12:55:28
举报
文章被收录于专栏:若尘的技术专栏

1. 基本概念

1.1. 用户空间与内核空间

操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。

1.2. 进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。进程的切换要经历保存被切换进程上下文(包括程序计数器和其他寄存器)、更新PCB信息、选择另一个进程、更新PCB、恢复上下文;总而言之就是很耗资源;

1.3. 进程的阻塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是

不占用CPU资源 的。

1.4. 同步异步 和 阻塞非阻塞 的关系

很多人会将这两个概念混淆,实际上这是两个完全不同的东西;

  • 同步异步与消息的同步方式相关undefined同步是一个主动去要结果的过程,而异步是被动通知结果;
  • 阻塞非阻塞与执行过程中的状态相关undefined如果一个方法是阻塞的,程序执行时会一直等待数据返回(代码卡死,线程挂起),而如果是非阻塞的,不管有没有结果也立即返回(如果没数据就返回空)代码见如下非阻塞IO

1.5. 文件描述符fd

文件描述符(File

descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

1.6. 零拷贝

应用进程的一次完整的读写操作,都需要在用户空间与内核空间中来回拷贝,并且每一次拷贝,都需要 CPU

进行一次上下文切换(由用户进程切换到系统内核,或由系统内核切换到用户进程),这样是不是很浪费 CPU

和性能呢?那有没有什么方式,可以减少进程间的数据拷贝,提高数据传输的效率呢?

网络IO读写流程

所谓的零拷贝,就是取消用户空间与内核空间之间的数据拷贝操作,应用进程每一次的读写操作,都可以通过一种方式,让应用进程向用户空间写入或者读取数据,就如同直接向内核空间写入或者读取数据一样,再通过

DMA 将内核中的数据拷贝到网卡,或将网卡中的数据 copy 到内核。

可以想到将用户空间与内核空间都将数据写到一个地方,就不需要拷贝;

主要通过两种方式:mmap+write 和 sendfile,mmap+write 方式的核心原理就是通过虚拟内存来解决的

虚拟内存

2. 常见的网络IO模型

说到网络通信,就不得不提网络IO模型,因为所谓的两台PC机之间的网络通信,实际上就是两台PC机对网络IO的操作;

常见的网络IO模型分为四种:同步阻塞IO(BIO)、同步非阻塞IO(NIO)、IO多路复用、异步非阻塞IO(AIO);只有AIO为异步IO,其他都是同步IO,最常用的就是

同步阻塞IOIO多路复用

2.1. 阻塞IO(BIO)

最简单、最常见的 IO 模型,在Linux中,默认所有的socket都是blocking的(如下代码);

首先,应用进程发起IO系统调用后,会转到内核空间处理,内核开始等待数据,等到来数据时再将内核中的数据拷贝到用户内存中,整个IO处理完毕后返回进程;

整个过程中(等待数据、拷贝数据)应用进程中IO操作的线程一直处于阻塞状态,如果要同时处理多个连接,势必每一个IO操作都要占用一个线程;

代码语言:txt
复制
ServerSocket server = new ServerSocket(9090);
代码语言:txt
复制
while (true) {
代码语言:txt
复制
    Socket client = server.accept(); // 接受客户端-阻塞1
代码语言:txt
复制
    new Thread(() -> {
代码语言:txt
复制
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
代码语言:txt
复制
        String str = bufferedReader.readLine(); // 读取IO数据-阻塞2
代码语言:txt
复制
    }).start();
代码语言:txt
复制
}

2.2. 非阻塞IO(NIO)

相比于阻塞IO,内核在处理时,如果没有数据就 直接返回 ,基于这个特点可以实现一个线程内同事处理多个socket的IO请求;

如下代码是利用Java的NIO(New IO)来实现的非阻塞IO模型;

代码语言:txt
复制
LinkedList<SocketChannel> clients = new LinkedList<>();
代码语言:txt
复制
ServerSocketChannel ss = ServerSocketChannel.open();
代码语言:txt
复制
ss.bind(new InetSocketAddress(9090));
代码语言:txt
复制
ss.configureBlocking(false); // 内核级别非阻塞,只让接受客户端不阻塞
代码语言:txt
复制
// 单线程:即处理连接客户端,也处理遍历每个连接
代码语言:txt
复制
while (true) {
代码语言:txt
复制
    // 接受客户端连接-非阻塞,没有客户端时,阻塞模式下回一直卡着,非阻塞下回直接返回-1;
代码语言:txt
复制
    SocketChannel client = ss.accept();
代码语言:txt
复制
    if (client != null) {
代码语言:txt
复制
        // 设置对client的连接也是非阻塞的(内核级别)
代码语言:txt
复制
        client.configureBlocking(false); 
代码语言:txt
复制
        clients.add(client);
代码语言:txt
复制
    }
代码语言:txt
复制
    ByteBuffer buffer =ByteBuffer.allocateDirect(4096); // buffer缓冲区
代码语言:txt
复制
    // 遍历所有客户端,处理IO数据
代码语言:txt
复制
    for (SocketChannel c : clients) {
代码语言:txt
复制
        // 获取IO数据-非阻塞,BIO下这里会阻塞,所以没法用一个线程遍历所有socket连接
代码语言:txt
复制
        int num = c.read(buffer);
代码语言:txt
复制
        if (num > 0) {  // 有数据时返回大于0
代码语言:txt
复制
            buffer.flip();
代码语言:txt
复制
            String str = new String(buffer.array());
代码语言:txt
复制
            buffer.clear();
代码语言:txt
复制
        }
代码语言:txt
复制
    }
代码语言:txt
复制
}

2.3. IO多路复用(IO multiplexing)

上面的代码有什么弊端?

如果有1万个客户端只有一个发来数据,那么另外9999个是不需要遍历的,并且99%情况都是没有数据来的,但是它一直在空转浪费CPU,还不如阻塞着;

IO多路复用解决了这个问题:

linux下会调用内核的select(poll、epoll)方法,传入1万个FD(文件描述符),内核会 阻塞

地监听1万个socket连接是否有数据,当有数据时内核会返回具体是哪个socket;这时用户进程再调用read操作,将数据从内核中拷贝到用户进程;(下文会介绍Linux下的三种多路复用的实现方式)

image.png

一句话解释 IO多路复用:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力。

多路复用 IO 是在高并发场景中使用最为广泛的一种 IO 模型,如 Java 的 NIO、Redis、Nginx 的底层实现就是此类 IO

模型的应用,经典的 Reactor 模式也是基于此类 IO 模型。

各平台实现IO多路复用的方式
  • Linux:Select、Poll、Epoll(目前一般都使用Epoll)
  • MacOS:Kqueue
  • Windows:IOCP

2.4. 异步非阻塞IO(AIO)

异步IO只有高版本的Linux系统内核才会支持;

IO多路复用本质是同步IO,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是同步的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

3. Select、Poll、Epoll

Linux下实现IO多路复用有select、poll、epoll等方式,监视多个描述符(fd),一旦某个描述符就绪(读/写/异常)就能通知程序进行相应的读写操作。读写操作都是自己负责的,也即是阻塞的,所以本质上都是同步(堵塞)IO

3.1. Select

代码语言:txt
复制
// select接口,调用时会堵塞,linux下执行man 2 select查阅
代码语言:txt
复制
int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);
执行过程
  1. 使用copy_from_user从用户空间拷贝fd_set到内核空间
  2. 内核遍历[0,nfds)范围内的每个fd,调用fd所对应的设备的驱动poll函数,poll函数可以检测fd的可用流(读流、写流、异常流)。
  3. 检查是否有流发生,如果有发生,把流设置对应的类别,并执行4,如果没有流发生,执行5。或者timeout=0,执行4。
  4. select返回。
  5. select阻塞进程,等待被流对应的设备唤醒时执行2,或timeout到期,执行4。
select的局限性:

每次调select获取哪个连接有数据时都要传入所有的fd,如果fd_set很大,那么拷贝和遍历的开销会很大;

3.2. Poll

poll的实现和select非常相似,轮询+遍历+根据描述符的状态处理,只是fd_set换成了pollfd,而且去掉了最大描述符数量限制,其他的局限性同样存在。

代码语言:txt
复制
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);

3.3. Epoll

优化了select和poll中的缺点,与select和poll只提供一个接口函数不同的是,epoll提供了三个接口函数及一些结构体:

代码语言:txt
复制
/*建一个epoll句柄*/
代码语言:txt
复制
int epoll_create(int size);
代码语言:txt
复制
/*向epoll句柄中添加需要监听的fd和时间event*/
代码语言:txt
复制
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
代码语言:txt
复制
/*返回发生事件的队列*/
代码语言:txt
复制
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
代码语言:txt
复制
struct eventpoll{
代码语言:txt
复制
    /*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,也就是这个epoll_ctl监控的事件*/  
代码语言:txt
复制
    struct rb_root rbr;
代码语言:txt
复制
    /*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/  
代码语言:txt
复制
    struct list_head rdllist;
代码语言:txt
复制
}
  • 使用红黑树而不是数组存放描述符和事件,增删改查非常高效,轻易可处理大量并发连接。
  • 红黑树及双向链表都在内核cache中,避免拷贝开销。
  • 采用回调机制,事件的发生只需关注rdllist双向链表即可。

epoll

4. Java的NIO

Java的NIO指的是JDK 1.4引入的new IO包,不是上面说的NIO指的是非阻塞IO概念;

目的是解决传统IO中面向数据流的同步阻塞问题,NIO是面向缓存区的;

Java NIO也属于IO多路复用模型,底层是调用相应的内核函数(Select、Poll、Epoll)。

传统IO和JavaNIO对比

4.1. Java NIO组成三大构件

4.1.1. Channel通道

一个Socket连接对应一个Channel通道;

4.1.2. Buffer缓冲区

用于和Channel进行数据交互;

4.1.3. Selector选择器

可以理解为Java NIO的多路复用器,多个Channel注册到同一个Selector选择器上,一个选择器同时监听多个连接通道(单线程);

在执行Selector.select()方式时,如果内核采用select实现的多路复用,则调用select函数,如果是Epoll,则调用epoll_wait函数,阻塞的获取多个socket连接或者数据请求;

Selector的IO事件类型
  • 可读 OP_READ
  • 可写OP_WRITE
  • 连接OP_CONNECT 完成了对端的握手,就处于连接就绪状态。
  • 接收OP_ACCEPT 当检测到一个新的连接到来则处于接收就绪转态。

image.png

代码语言:txt
复制
public void init() {
代码语言:txt
复制
    private Selector selector;
代码语言:txt
复制
    ServerSocketChannel ssc =ServerSocketChannel.open();
代码语言:txt
复制
    ssc.configureBlocking(false); // 非阻塞
代码语言:txt
复制
    ssc.bind(new InetSocketAddress(port));
代码语言:txt
复制
    selector = Selector.open();
代码语言:txt
复制
    ssc.register(selector, SelectionKey.OP_ACCEPT);
代码语言:txt
复制
    System.out.println("NioServer started ......");
代码语言:txt
复制
}
代码语言:txt
复制
public void start() {
代码语言:txt
复制
    this.init();
代码语言:txt
复制
    while (true) {
代码语言:txt
复制
        // 阻塞,等待连接请求、接受数据操作,调的内核底层的select、poll、epoll?
代码语言:txt
复制
        int events = selector.select();
代码语言:txt
复制
        if (events > 0) {
代码语言:txt
复制
            Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
代码语言:txt
复制
            while (selectionKeys.hasNext()) {
代码语言:txt
复制
                SelectionKey key = selectionKeys.next();
代码语言:txt
复制
                selectionKeys.remove();
代码语言:txt
复制
                if (key.isAcceptable()) {
代码语言:txt
复制
                    // 处理连接请求
代码语言:txt
复制
                    accept(key);
代码语言:txt
复制
                } else {
代码语言:txt
复制
                    // 处理接受数据请求,多线程事件处理
代码语言:txt
复制
                    service.submit(new NioServerHandler(key));
代码语言:txt
复制
                }
代码语言:txt
复制
            }
代码语言:txt
复制
        }
代码语言:txt
复制
    }
代码语言:txt
复制
}
代码语言:txt
复制
public void accept(SelectionKey key) {
代码语言:txt
复制
    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
代码语言:txt
复制
    SocketChannel sc = ssc.accept();
代码语言:txt
复制
    sc.configureBlocking(false);
代码语言:txt
复制
    // 将多个Channel注册到同一个Selector上
代码语言:txt
复制
    sc.register(selector, SelectionKey.OP_READ);
代码语言:txt
复制
    System.out.println("accept a client : " + sc.socket().getInetAddress().getHostName());
代码语言:txt
复制
}

5. Reactor模式

很多框架、中间件底层都是基于Reactor模式来实现的,比如Netty、Redis等,而其核心又是IO多路复用;

5.1. 基本设计思想:I/O复用+线程池

  • Reactor 模式,通过一个或多个输入同时传递给服务处理器的模式(基于事件驱动)
  • 服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程, 因此Reactor模式也叫 Dispatcher模式
  • Reactor 模式使用IO复用监听事件, 收到事件后,分发给某个线程(进程), 这点就是网络服务器高并发处理关键

5.2. 核心组成

  • Reactor:undefinedReactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人;
  • Handlers:undefined处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。

5.3. 模式分类

根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现:

  • 单 Reactor 单线程
  • 单 Reactor 多线程
  • 主从 Reactor 多线程

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
作者已关闭评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 基本概念
    • 1.1. 用户空间与内核空间
      • 1.2. 进程切换
        • 1.3. 进程的阻塞
          • 1.4. 同步异步 和 阻塞非阻塞 的关系
            • 1.5. 文件描述符fd
              • 1.6. 零拷贝
              • 2. 常见的网络IO模型
                • 2.1. 阻塞IO(BIO)
                  • 2.2. 非阻塞IO(NIO)
                    • 2.3. IO多路复用(IO multiplexing)
                      • 2.4. 异步非阻塞IO(AIO)
                      • 3. Select、Poll、Epoll
                        • 3.1. Select
                          • 3.2. Poll
                            • 3.3. Epoll
                            • 4. Java的NIO
                              • 4.1. Java NIO组成三大构件
                                • 4.1.1. Channel通道
                                • 4.1.2. Buffer缓冲区
                                • 4.1.3. Selector选择器
                            • 5. Reactor模式
                              • 5.1. 基本设计思想:I/O复用+线程池
                                • 5.2. 核心组成
                                  • 5.3. 模式分类
                                  相关产品与服务
                                  云数据库 Redis
                                  腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                                  领券
                                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档