IO:BIO NIO AIO网络编程模型

参考视频:https://www.bilibili.com/video/av76223318?p=5

 

I/O模型简单的解释:用什么样的通道进行数据的发送和接收,很大程度上决定了程序通讯的性能

Java共支持三种网络编程模型:BIO,NIO,AIO

 

BIO:Blocking IO

同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时,服务器端就需要启动一个线程进行处理,如果这个连续不做任何事情会造成不必要的线程开销。可以通过线程池机制改善(实现多个客户连接服务器)。

放在java.io包下

适用场景:

连接数目小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中。jdk1.4之前的唯一选择,但程序简单易理解

BIO简单流程:

1  服务器端启动一个ServerSockert

2  客户端启动Socket对服务器进行通讯,默认情况下服务器端需要对每个客户建立一个线程与之通讯

3  客户端发送请求后,先咨询服务器是否有线程响应

        3.1  如果没有响应,则会等待,或者被拒绝

        3.2  如果有响应,客户端线程会等待请求结束后,再继续执行

 

public class BioServerSocket {
    public static void main(String[] args) throws Exception{
        //创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        //用线程池来管理线程
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

        System.out.println("===== 启动ServerSocket");
        //启用并监听
        while (true){
            //等待并监听
            Socket socket = serverSocket.accept();
            //获得监听后启用线程来处理
            cachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    byte[] bytes = new byte[1024];
                    try {
                        InputStream inputStream = socket.getInputStream();
                        int read;
                        while ((read = inputStream.read(bytes)) != -1){
                            System.out.println(new String(bytes,0,read));
                        }
                        inputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }finally {
                        try {
                            socket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                }
            });
        }
    }
}

利用命令窗口的telnet模仿客户端发送请求

telnet命令若需要启动:https://jingyan.baidu.com/article/7908e85c6ec355af491ad265.html

连接命令:telnet 127.0.0.1 6666

作为客户端发送命令:Ctrl  ]

发送内容命令:send XXXXX

实际发送内容为XXXX

 

NIO:Non-blocking/New IO

同步非阻塞,服务器实现为一个线程处理多个请求,即客户端发送的连接请求都会注册到多路复用器(Selector选择器)上,多路复用器轮询到连接有I/O请求就进行处理

放在java.nio包下

适用场景:

连接数目多且连接时间短(轻操作)的架构,比如聊天服务器,弹屏系统,服务期间通讯等。编程比较复杂,jdk1.4之后开始

三大核心部分:Channel通道,Buffer缓冲区,Selector选择器

NIO是面向缓冲区,或者面向块编程,是Channel的事件Event驱动的

 

BIO  和  NIO 比较:

1  BIO以流的方式处理数据,NIO以块的方式处理数据,块IO的效率比流IO的效率要高很多

2  BIO是阻塞的,NIO是非阻塞的

3  BIO是基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件,因此使用单个线程可以监听多个客户端通道

 

Buffer缓冲区:

本质是一个可以读写数据的内存块,可以理解成一个容器对象(含数组),该对象提供一组方法,可以更轻松的使用内存块。缓冲区对象内置了一些机制,可以跟踪和记录缓冲区的状态变化情况。Channel提供可以从文件,网络读取数据的渠道,但读取和写入的数据必须经由Buffer。

子类都有四个重要属性:

    Capatity:  容量,即可以容纳的最大数据量。在缓冲区创建时被设定并且不能改变。 

    Limit: 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写,且极限是可以修改的

    Position:  位置,下一个要被读或者写的元素的索引,每次读写缓冲区时都会改变该值,为下次读写做准备

    Mark:标记

    static void Test(){
        ByteBuffer byteBuffer = ByteBuffer.allocate(5); //创建缓冲区
        ByteBuffer direct = ByteBuffer.allocateDirect(5);//创建直接缓冲区

        byteBuffer.put("h".getBytes()[0]);
        byteBuffer.put("s".getBytes()[0]);
        byteBuffer.put("s".getBytes()[0]);
        byteBuffer.put("n".getBytes()[0]);
        byteBuffer.put("j".getBytes()[0]);
        byteBuffer.flip(); //切换读写
        while (byteBuffer.hasRemaining()){
            System.out.println(byteBuffer.get());
        }
        System.out.println(" ================ ");
        byteBuffer.put(1,"k".getBytes()[0]);
        System.out.println("第一个元素:"+byteBuffer.get(1));
        System.out.println("第二个元素:"+byteBuffer.get(2));
        System.out.println("容量:"+byteBuffer.capacity());
        System.out.println("位置:"+byteBuffer.position());
        System.out.println(".. 具体诸多其他方法搜搜就好了");
    }

 

Channel通道:

类似于流,但有区别

1  通道能同时进行读写,而流只能进行读或者只能写

2  通道能异步进行读写数据

3  通道能从缓冲读取数据,也能写入缓冲

Channel在java.nio中是接口,具体常用的实现类:FileChannel(文件数据读写),  DatagramChannel(UDP的数据读写),  ServerSocketChannel(TCP数据读写),  SocketChannel(TCP数据读写)

FileChannel:

方法:

      read(ByteBuffer des);     通道读取数据,放到缓存区

      write(ByteBuffer tar);      读取缓冲区数据,放到通道

      transferFrom(....);          从目标通道复制数据到当前通道

      transferTo(....);               从当前通道复制数据到目标通道

static void test2() throws Exception {
        //发送数据
        String str = "Hi,女孩";
        //封装的Buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put(str.getBytes());
        //buffer反转
        byteBuffer.flip();

        //最终存储地方
        FileOutputStream fileInputStream = new FileOutputStream("d://hiGirl.text");
        //获得通道
        FileChannel channel = fileInputStream.getChannel();

        //缓冲区读取数据到通道
        channel.write(byteBuffer);

        //关闭流
        fileInputStream.close();
    }


    static void test4() throws Exception{
        File file = new File("d://hiGirl.txt");
        FileInputStream fileInputStream = new FileInputStream(file);
        FileOutputStream fileOutputStream = new FileOutputStream("d://hiGir2.txt");

        FileChannel channel = fileInputStream.getChannel();
        FileChannel outputStreamChannel = fileOutputStream.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());

        while (true){
            byteBuffer.clear();
            int read = channel.read(byteBuffer);
            if (read == -1) break;
            byteBuffer.flip();
            outputStreamChannel.write(byteBuffer);
            byteBuffer.flip();
        }


        fileInputStream.close();
        fileOutputStream.close();
    }

    static void test5() throws Exception{
        File file = new File("d://hiGirl.txt");
        FileInputStream fileInputStream = new FileInputStream(file);
        FileOutputStream fileOutputStream = new FileOutputStream("d://hiGir4.txt");
        FileChannel inputStreamChannel = fileInputStream.getChannel();
        FileChannel outputStreamChannel = fileOutputStream.getChannel();

        inputStreamChannel.transferTo(0,inputStreamChannel.size(),outputStreamChannel);
        // 或 outputStreamChannel.transferFrom(inputStreamChannel,0,inputStreamChannel.size());

        inputStreamChannel.close();
        outputStreamChannel.close();
        fileInputStream.close();
        fileOutputStream.close();
    }

MapperedByteBuffer:

可以让文件直接在内存(对外内存)中进行修改(操作系统不需要copy),而如何同步到文件由NIO完成

 static void test6() throws Exception{
        RandomAccessFile randomAccessFile = new RandomAccessFile("d://hiGirl.txt","rw");
        FileChannel channel = randomAccessFile.getChannel();
        /**
         * FileChannel.MapMode
         *  READ_ONLY: 只读
         *  READ_WRITE:读写
         *  PRIVATE: private (copy-on-write)
         *
         * 定义可以修改的范围
         * position: 可以修改的起始位置
         * size: 映射内存大小
         */
        MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        map.put(0,(byte) 'H');
        map.put(2,(byte) 'L');

        randomAccessFile.close();
    }

scattering:将数据写入到Buffer时,可以采用Buffer数组,依次写入

gatthering:将数据读取到Buffer时,可以采用Buffer数组,依次读取

 static void test7() throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        SocketChannel accept = serverSocketChannel.accept();

        //设置数组Buffer
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(3);
        byteBuffers[1] = ByteBuffer.allocate(5);

        int mesLength = 3+5;

        while (true){
            long byteRead = 0;

            while (byteRead < mesLength){
                long read = accept.read(byteBuffers);
                byteRead += read;
                System.out.println("byteRead: "+byteRead);
                Arrays.asList(byteBuffers).stream().map(buffer -> "position: "+buffer.position()+" ,limit: "+buffer.limit())
                        .forEach(System.out::println);
            }

            //将所有buffer进行反转可以进行其他操作
            Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.flip());

            //将数据读出显示到客户端
            long byteWrite = 0;
            while (byteWrite < mesLength){
                long write = accept.write(byteBuffers);
                byteWrite += write;
            }

            //复位
            Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear());

            System.out.println("byteRead = "+byteRead + "  byteWrite = "+byteWrite);
        }
    }

 

selector:

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

1  Netty的IO线程聚合了Selector选择器,可以同时并发处理成百上千个并发请求

2  当线程从某客户端Socket通道进行读写数据时,若没有线程可用时,可进行其他操作

3  线程通常将阻塞IO的空闲时间用于其他通道上执行IO操作,所以单个线程可以管理多个输入和输出通道

4  由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁IO阻塞导致的线程挂起

5  一个IO线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞IO一连接一线程模型,架构的性能,弹性伸缩能力和可靠性都得到了极大的提升

Selector抽象类

public abstract class Selector implements Closeable {
    
    //得到一个选择器对象
     public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }
    //监控所有注册通道,当其中有IO操作可进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间
    //select()为阻塞方法,至少有一个事件发生才会返回
    //select(long timeout) 非阻塞,若无事件发生,超时时间后也会返回
    //selectNow() 非阻塞,有事件发生立马返回
    //wakeup() 唤醒selector
    public abstract int select(long timeout)
        throws IOException;
    //从内部集合中得到所有SelectionKey
    public abstract Set<SelectionKey> selectedKeys();

}

原理

1 当有客户端连接时,会通过ServerSocketChannel得到对应的SocketChannel

2 对应的SocketChannel注册倒Selector上,一个Selector上可以注册多个SocketChannel

   -----  SelectionKey register(Selector sel, int ops,Object att)

3 注册后返回一个SelectionKey,会和该Selector关联

4 Selector进行监听,用select()方法 ,会返回有事件发生的通道的个数

5 进一步得到各个SelectionKey

6 再通过SelectionKey反向获取SocketChannel channel()

7 可以通过得到的channel,完成业务处理

 

OP_ACCEPT: 有新的网络连接可以accept, 1<<4 = 16

OP_CONNECT:代表连接已经建立:1<<3=8

OP_READ:代表读操作:1<<0 = 1

OP_WRITE:代表写操作:1<<2=4

 

零拷贝:零拷贝不是不拷贝,而是没有CPU拷贝

  零拷贝是网络编程的关键,很多性能优化都离不开。在java程序中,常用的零拷贝mmap(内存映射)和sendFile。那么它们在OS里到底是一个怎样的设计?

1  我们说的零拷贝,是从操作系统角度来说。因为内核缓冲区之间,没有数据是重复的(只有kernel buffer有一份数据)

2  零拷贝不仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算

(用户态,kernel[内核态],硬件)

mmap:

文件映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数

 

SendFile:

Linux2.1提供了sendFile函数,基本原理:数据不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时由于和用户态完全无关,就减少了一次上下文切换

Linux2.4对sendFile函数进行了优化,避免了从内核缓冲区拷贝到SocketBuffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝

mmap 和sendfile区别

1 mmap适合小数据量读写,sendfile适合大文件传输

2 mmap需要3次上下文切换,3次数据拷贝;sendfile需要2次上下文切换,最少2次数据拷贝

3 sendfile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)

传统拷贝:

    4次拷贝:   

        硬件 -> DMA拷贝到 -> 内核态(kernelBuffer)

        内核态(kernelBuffer) -> cpu拷贝到 ->用户buffer

        用户buffer -> cpu拷贝到 -> socket buffer

        socket buffer -> DMA拷贝到 -> 协议栈

    3次切换:硬件,内核态, 用户

MMAP拷贝:

    3次拷贝:   

        硬件 -> DMA拷贝到 -> 内核态(kernelBuffer)

        内核态(kernelBuffer) -> cpu拷贝到 -> socket buffer

        socket buffer -> DMA拷贝到 -> 协议栈

    3次切换:硬件,内核态, 用户

SendFile拷贝:

    linux2.1版的sendfile中还有一次CPU拷贝:3次拷贝,2次切换

    linux2.4版的才是零拷贝:2次拷贝,2次切换

 

 

零拷贝代码Test:

server:

public class NewIOServiceSocket {
    public static void main(String[] args) throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open().bind(new InetSocketAddress(7000));
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        while (true){
            SocketChannel socketChannel = serverSocketChannel.accept();
            int readLength = 0;
            while (readLength != -1){
                readLength = socketChannel.read(byteBuffer);
                byteBuffer.rewind();//倒带
            }
        }
    }
}

client

public class NewIOClient {
    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1",7000));
        FileInputStream fileInputStream = new FileInputStream(new File("C:\\Users\\chinaoly\\Desktop\\12345.txt"));
        FileChannel fileChannel = fileInputStream.getChannel();
        long start = System.currentTimeMillis();
        /**
         * linux 环境下 transferTo 一次即可完成
         * windows环境下,transferTo 一次最多传8M,大于8M分段传输,需要记住传输时的位置
         */
        long count = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
        System.out.println(" 时间 : "+(System.currentTimeMillis() - start));
    }
}

 

AIO(NIO.2):

异步非阻塞,AIO引入异步通道的概念,采用Proactor模式,简化了程序编写,有校的请求才启用线程,它的特点是先有操作系统完成后才通知服务器端启动线程去处理,一般适用于连接数较多且连接时间较长的应用。jdk1.7以后引入,但目前还未得到广泛运用

适用场景:

连接数目多且连接时间长(重操作)的架构。比如相册服务器,充分调用OS参与并发操作。编程负责,jdk7开始

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值