Netty学习笔记(一) IO

五种IO模型

阻塞式IO模型

recv返回error,io未完成,进入阻塞状态让出cpu。recv发现数据准备好了也就是io完成,需要将数据从内核拷贝到用户

非阻塞式IO模型

这里写图片描述

recv发现数据IO未完成时,不断系统调用进行查询IO是否完成,一直占用cpu。完成后等待内核拷贝数据。

信号驱动IO模型

这里写图片描述

非阻塞IO,只是由主动查询变为信号通知

IO多路复用

这里写图片描述

与普通非阻塞式IO相比,多了一个select函数监听多socket,对其参数中的文件描述符进行循环监听,当某个文件描述符就绪的时候,就对这个文件描述符进行处理。可以监听多个Socket,哪个数据获取完成处理哪个。

异步IO

这里写图片描述

异步IO是指用户进程触发I/O操作以后就立即返回,继续开始做自己的事情,而当I/O操作已经完成的时候会得到I/O完成的通知。IO完成后的执行者是内核线程,内核线程将数据从内核态拷贝到用户态,所以这里没有等待

基础

accept

accept:三次握手后创建socket,从而建立连接。

数据的获取与线程,cpu无关。是协议和设备将数据包发到套接字缓冲区。

recv

recv:阻塞函数。读取缓冲区的数据,如果数据准备好了,进行拷贝(切换到内核态,cpu切换执行内核空间)。数据未准备好的话线程进入阻塞状态(进度卡住)。

select

select:令IO非阻塞的函数。它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。如果数据准备好了进行拷贝。nio中一个线程对应selector

BIO

简述

BIO(Blocking I/O),同步阻塞,实现模式为一个连接一个线程,即当有客户端连接时,服务器端需为其单独分配一个线程,如果该连接不做任何操作就会造成不必要的线程开销。BIO是传统的Java io编程,其相关的类和接口在java.io 包下。

BIO适用于连接数目较小且固定的架构,对服务器资源的要求较高,是JDK1.4以前的唯一选择,但程序简单易理解。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F47JiNVX-1649063110079)(https://unpkg.zhimg.com/youthlql@1.0.0/netty/introduction/chapter_001/0003.png)]

服务器端启动一个 ServerSocket。

tcp连接中serverSocket监听是否有连接建立,服务端创建socket作为连接的网络接口。一个连接对应一个socket。
客户端启动 Socket 对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯(accept)。

多线程底层

服务器端采用多线程,当accept一个请求后,创建socket开启线程进行recv,单个线程会因为数据没准备好阻塞。整体并发处理,但随着请求数增加需要增加系统线程,大量的线程占用很大的内存空间,并且线程切换会带来很大的开销,10000个线程真正发生读写事件的线程数不会超过20%,每次accept都开一个线程也是一种资源浪费

线程是执行代码的载体(主函数是线程)

// 伪代码描述
while(1) {
  // accept阻塞
  client_fd = accept(listen_fd)
  // 开启线程read数据(fd增多导致线程数增多)
  new Thread func() {
    // recv阻塞(多线程不影响上面的accept)
    if (recv(fd)) {
      // logic
    }
  }  
}
单线程底层

服务端采用单线程,当accept一个请求后,在recv或send调用线程阻塞时,将无法accept其他请求(必须等上一个请求处recv或send完),无法处理并发

// 伪代码描述
while(1) {
  // accept阻塞
  client_fd = accept(listen_fd)
  fds.append(client_fd)
  for (fd in fds) {
    // recv阻塞(会影响上面的accept)
    if (recv(fd)) {
      // logic
    }
  }  
}
BIO实例
package com.atguigu.bio;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOServer {

    public static void main(String[] args) throws Exception {
        //线程池机制
        //思路
        //1. 创建一个线程池
        //2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        //创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务器启动了");
        while (true) {
            System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
            //监听,等待客户端连接
            System.out.println("等待连接....");
            //会阻塞在accept()
            final Socket socket = serverSocket.accept();
            System.out.println("连接到一个客户端");
            //就创建一个线程,与之通讯(单独写一个方法)
            newCachedThreadPool.execute(new Runnable() {
                public void run() {//我们重写
                    //可以和客户端通讯
                    handler(socket);
                }
            });
        }
    }

    //编写一个handler方法,和客户端通讯
    public static void handler(Socket socket) {
        try {
            System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
            byte[] bytes = new byte[1024];
            //通过socket获取输入流
            InputStream inputStream = socket.getInputStream();
            //循环的读取客户端发送的数据
            while (true) {
                System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
                System.out.println("read....");
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println(new String(bytes, 0, read));//输出客户端发送的数据
                } else {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("关闭和client的连接");
            try {
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

NIO

简介
  • Java NIO 全称 Java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 NewIO),是同步非阻塞的。

  • NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。【基本案例】
    NIO 有三大核心部分:Channel(通道)Buffer(缓冲区)Selector(选择器) 。

  • NIO 是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。

  • Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
    通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配 50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。

  • HTTP 2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP 1.1 大了好几个数量级。(socket内的多路复用不是select监听多个socket)

NIO 和 BIO 的比较
  • BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。
  • BIO 是阻塞的,NIO 则是非阻塞的。
  • BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
  • 线程开销差异。BIO每个socket对应一个线程一遍对准备好的数据处理。而NIOSelector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
  • Buffer和Channel之间的数据流向是双向的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AVqg9y0W-1649063110080)(https://unpkg.zhimg.com/youthlql@1.0.0/netty/introduction/chapter_001/0007.png)]

缓冲区(Buffer)

bio也有缓冲区(socket缓冲区)

缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u0S9HyYQ-1649063110080)(https://unpkg.zhimg.com/youthlql@1.0.0/netty/introduction/chapter_001/0009.png)]

Java 中的基本数据类型(boolean 除外),都有一个 Buffer 类型与之相对应,最常用的自然是 ByteBuffer 类(二进制数据),该类的主要方法如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xe5Ovq5m-1649063110081)(https://unpkg.zhimg.com/youthlql@1.0.0/netty/introduction/chapter_001/0014.png)]

通道(Channel)

ServerSocketChannel和SocketChannel是Socket的接口的升级版

NIO 的通道类似于流,但有些区别如下:
通道可以同时进行读写,而流只能读或者只能写
通道可以实现异步读写数据
通道可以从缓冲读数据,也可以写数据到缓冲:
BIO 中的 Stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,可以读操作,也可以写操作。
Channel 在 NIO 中是一个接口 public interface Channel extends Closeable{}
常用的 Channel 类有:FileChannel、DatagramChannel、ServerSocketChannel 和 SocketChannel。【ServerSocketChannel 类似 ServerSocket、SocketChannel 类似 Socket】
FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写

例子
package com.atguigu.nio;

import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NIOFileChannel01 {

    public static void main(String[] args) throws Exception {
        String str = "hello,尚硅谷";
        //创建一个输出流 -> channel
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");

        //通过 fileOutputStream 获取对应的 FileChannel
        //这个 fileChannel 真实类型是 FileChannelImpl
        FileChannel fileChannel = fileOutputStream.getChannel();

        //创建一个缓冲区 ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //将 str 放入 byteBuffer
        byteBuffer.put(str.getBytes());

        //对 byteBuffer 进行 flip
        byteBuffer.flip();

        //将 byteBuffer 数据写入到 fileChannel
        fileChannel.write(byteBuffer);
        fileOutputStream.close();
    }
}

Selector(选择器)
  • Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)。

  • Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

  • 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。

  • 避免了多线程之间的上下文切换导致的开销。

  • socketChannel是socket的接口的改进NIO 中的 ServerSocketChannel 功能类似 ServerSocket(监听创建连接用的socket)、SocketChannel 功能类似 Socket

  • SelectionKey

    将serverSocketChannel 注册到 selector

    • int OP_ACCEPT:有新的网络连接可以 accept,值为 16
    • int OP_CONNECT:代表连接已经建立,值为 8
    • int OP_READ:代表读操作,值为 1
    • int OP_WRITE:代表写操作,值为 4
  • 方法

    Selector 相关方法说明
    selector.select(); //阻塞 函数本身阻塞但令IO不阻塞
    selector.select(1000); //阻塞 1000 毫秒,在 1000 毫秒后返回
    selector.wakeup(); //唤醒 selector
    selector.selectNow(); //不阻塞,立马返还

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uGgXcJE1-1649063110081)(https://unpkg.zhimg.com/youthlql@1.0.0/netty/introduction/chapter_001/0019.png)]

    集合

    key set 包含着所有selectionKeys,当前所有注册到selector中的channel返回的注册关系SelectionKey都包含在内,这个集合可以通过selector.keys() 方法返回。

    selected-key set 包含着一部分selectionKeys,其中的每个selectionKey所关联的channel在selection operation期间被检测出至少 准备好 了一个可以在兴趣集中匹配到的操作。这个集合可以通过调用selector.selectedKeys()方法返回。selected-key set 一定是 key set 的子集。

    cancelled-key set 也包含着一部分selectionKeys,其中的每个selectionKey都已经被取消,但是所关联channel还没有被撤销登记。cancelled-key set 不能够被直接返回,但也一定是 key set 的子集。

    遍历selected-key进行accept和read等操作,创建连接或者读取数据。用完后删除key,防止重复处理。但channel在keys注册不会消失。

    流程

    1.当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel。
    2.Selector 进行监听 select 方法,返回有事件发生的通道的个数。
    3.将 socketChannel 注册到 Selector 上,register(Selector sel, int ops),一个 Selector 上可以注册多个 SocketChannel。
    4.注册后返回一个 SelectionKey,会和该 Selector 关联(集合)。
    5.进一步得到各个 SelectionKey(有事件发生)。
    6.在通过 SelectionKey 反向获取 SocketChannel,方法 channel()。
    7.可以通过得到的 channel,完成业务处理。

    例子
    package com.atguigu.nio;
    
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.*;
    import java.util.Iterator;
    import java.util.Set;
    
    public class NIOServer {
        public static void main(String[] args) throws Exception{
    
            //创建ServerSocketChannel -> ServerSocket
    
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    
            //得到一个Selecor对象
            Selector selector = Selector.open();
    
            //绑定一个端口6666, 在服务器端监听
            serverSocketChannel.socket().bind(new InetSocketAddress(6666));
            //设置为非阻塞
            serverSocketChannel.configureBlocking(false);
    
            //把 serverSocketChannel 注册到  selector 关心 事件为 OP_ACCEPT       pos_1
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    
            System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1
    
    
    
            //循环等待客户端连接
            while (true) {
    
                //这里我们等待1秒,如果没有事件发生, 返回
                if(selector.select(1000) == 0) { //没有事件发生
                    System.out.println("服务器等待了1秒,无连接");
                    continue;
                }
    
                //如果返回的>0, 就获取到相关的 selectionKey集合
                //1.如果返回的>0, 表示已经获取到关注的事件
                //2. selector.selectedKeys() 返回关注事件的集合
                //   通过 selectionKeys 反向获取通道
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                System.out.println("selectionKeys 数量 = " + selectionKeys.size());
    
                //遍历 Set<SelectionKey>, 使用迭代器遍历
                Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
    
                while (keyIterator.hasNext()) {
                    //获取到SelectionKey
                    SelectionKey key = keyIterator.next();
                    //根据key 对应的通道发生的事件做相应处理
                    if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
                        //该该客户端生成一个 SocketChannel
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
                        //将  SocketChannel 设置为非阻塞
                        socketChannel.configureBlocking(false);
                        //将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
                        //关联一个Buffer
                        socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
    
                        System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..
    
    
                    }
                    if(key.isReadable()) {  //发生 OP_READ
    
                        //通过key 反向获取到对应channel
                        SocketChannel channel = (SocketChannel)key.channel();
    
                        //获取到该channel关联的buffer
                        ByteBuffer buffer = (ByteBuffer)key.attachment();
                        channel.read(buffer);
                        System.out.println("form 客户端 " + new String(buffer.array()));
    
                    }
    
                    //手动从集合中移动当前的selectionKey, 防止重复操作
                    keyIterator.remove();
    
                }
    
            }
    
        }
    }
    
    
    

    零拷贝

    介绍

    (115条消息) 原来 8 张图,就可以搞懂「零拷贝」了_小林coding的博客-CSDN博客

    mmap

    整个过程发生了4次用户态和内核态的上下文切换3次拷贝,具体流程如下:

    1. 用户进程通过mmap()方法向操作系统发起调用,上下文从用户态转向内核态
    2. DMA控制器把数据从硬盘中拷贝到读缓冲区
    3. 上下文从内核态转为用户态,mmap调用返回
    4. 用户进程通过write()方法发起调用,上下文从用户态转为内核态
    5. CPU将读缓冲区中数据拷贝到socket缓冲区
    6. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write()返回

    mmap的方式节省了一次CPU拷贝,同时由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。

    sendfile

    整个过程发生了2次用户态和内核态的上下文切换3次拷贝,具体流程如下:

    1. 用户进程通过sendfile()方法向操作系统发起调用,上下文从用户态转向内核态
    2. DMA控制器把数据从硬盘中拷贝到读缓冲区
    3. CPU将读缓冲区中数据拷贝到socket缓冲区
    4. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,sendfile调用返回

    sendfile方法IO数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如静态文件服务器。

    sendfile+DMA Scatter/Gather

    Linux2.4内核版本之后对sendfile做了进一步优化,通过引入新的硬件支持,这个方式叫做DMA Scatter/Gather 分散/收集功能。

    它将读缓冲区中的数据描述信息–内存地址和偏移量记录到socket缓冲区,由 DMA 根据这些将数据从读缓冲区拷贝到网卡,相比之前版本约等于减少了一次CPU拷贝的过程

    1. 用户进程通过sendfile()方法向操作系统发起调用,上下文从用户态转向内核态
    2. DMA控制器利用scatter把数据从硬盘中拷贝到读缓冲区离散存储
    3. CPU把读缓冲区中的文件描述符和数据长度发送到socket缓冲区
    4. DMA控制器根据文件描述符和数据长度,使用scatter/gather把数据从内核缓冲区拷贝到网卡
    5. sendfile()调用返回,上下文从内核态切换回用户态

    DMA gathersendfile一样数据对用户空间不可见,而且需要硬件支持,同时输入文件描述符只能是文件,但是过程中完全没有CPU拷贝过程,极大提升了性能。

    例子
     SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("localhost", 7001));
            String filename = "protoc-3.6.1-win32.zip";
            //得到一个文件channel
            FileChannel fileChannel = new FileInputStream(filename).getChannel();
            //准备发送
            long startTime = System.currentTimeMillis();
            //在 linux 下一个 transferTo 方法就可以完成传输
            //在 windows 下一次调用 transferTo 只能发送 8m, 就需要分段传输文件,而且要主要
            //传输时的位置=》课后思考...
            //transferTo 底层使用到零拷贝
            long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
            System.out.println("发送的总的字节数 = " + transferCount + " 耗时: " + (System.currentTimeMillis() - startTime));
    
            //关闭
            fileChannel.close();
    
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值