NIO/BIO

NIO/BIO
    BIO网络通信
        概述
            网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。
            在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。
            BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答通信模型。
        该模型最大的问题就是缺乏弹性伸缩能力,
            当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。
            BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,这种模型显然无法满足高性能、高并发接入的场景。
        引入线程池
            为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化——后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N。
            通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
            由于线程池和消息队列都是有界的,而且避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题
            因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出。相比于传统的一连接一线程模型,是一种改良。
        BIO通信框架的弊病
            引入线程池避免了创建大量线程,但是由于底层的通信依然采用同步阻塞模型,其实并未从根本上解决问题
            当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生如下三种事件。
                1)有数据可读;
                2)可用数据已经读取完毕;
                3)发生空指针或者I/O异常。
            这意味着当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方要60s才能够将数据发送完成,读取一方的I/O线程也将会被同步阻塞60s,在此期间,其他接入消息只能在消息队列中排队。
            BIO的网络通信,读和写操作都是同步阻塞的,阻塞的时间取决于对方I/O线程的处理速度和网络I/O的传输速度。
            本质上来讲,我们无法保证生产环境的网络状况和对端的应用程序能足够快,如果我们的应用程序依赖对方的处理速度,它的可靠性就非常差
            无论是否引入线程池技术,如果底层用的是BIO通信框架,都是无法从根本上解决通信线程阻塞问题。
        BIO 4种产生阻塞的方法:
            1-4
                 //accept方法会产生阻塞,直到有客户端连接
                 //传统的BIO会产生阻塞:
                 //1.服务端accept()方法会产生阻塞
                //当有客户端接入时,accpet()方法不阻塞
                //但是客户端没有任何的流输入,所以产生了阻塞
                //2.即read()方法也会产生阻塞
                //3.如果服务端未启动,客户端就连接的话会报错,Connection refused
                //但是,需要留意的是,这个异常在程序启动后,并不是马上抛出的
                //而是卡顿了一秒钟才出现的
                //这个现象的原因:
                //客户端程序启动=》尝试连接服务端=》等待服务端的链接响应=》服务端没有启动=》
                //客户端收到服务端的响应,报出错误提示
                //实际上,对应客户端,socket.connect()这个方法也会产生阻塞
                4.//结果证明,当不断向outputStream里写数据时,写到一定大小后,会产生阻塞
                //即Write方法也会产生阻塞
    NIO
        NIO概述
            Non-Blocking I/O,是一种非阻塞通信模型。不同的语言或操作系统都有其不同的实现。
            java.nio是jdk1.4版本引入的一套API,我们可以利用这套API实现非阻塞的网络编程模型。
            目前无论是何种应用,都是分布式架构,因为分布式架构能够抗高并发,实现高可用,负载均衡以及存储和处理海量数据,而分布式架构的基础是网络通信。
            随着当前大数据和实时计算技术的兴起,高性能 RPC 框架与网络编程技术再次成为焦点。
                Fackebook的Thrift框架
                scala的 Akka框架
                实时流领域的 Storm、Spark框架
                开源分布式数据库中的 Mycat、VoltDB
                 Java 领域里大名鼎鼎的 NIO 框架——Netty,则被众多的开源项目或商业软件所采用。
    BIO和NIO的对比
        BIO
            1.阻塞通信模型,典型代表是ServerSocket 和Socket accept connect  read write 会产生阻塞。所以 BIO通信模型的弊端在于:如果有大量请求,会创建大量线程,一是可能造成内存溢出,此外,线程多了之后,会造成cpu的负载过高,因为要做线程管理和上下文切换。 虽然引入线程池,也未能解决根本问题,因为底层还是同步阻塞模型。 同步阻塞模型一是性能低,二是不可靠,取决于对端环境。
            2.面向流处理,即阻塞最根本的原子在于流的read和wirte方法是阻塞方法
            Java NIO 管道是2个线程之间的单向数据连接。
            Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取
        BIO的使用场景:访问量少,长请求(比如下载一个大文件等场景)
        NIO的适用场景:高并发,高访问量,短请求
        NIO
            1.非阻塞通信模型
            2.面向缓冲区(Buffer)
            3.NIO传输数据的方式:把数据放到缓冲区,然后通过Channel进行传输。
    Buffer
        Buffer的子类,对应了8种基本数据类型里的七种(没有boolean类型),分别是:
            1.ByteBuffer
            2.CharBuffer
            3.DoubleBuffer
            4.FolatBuffer
            5.IntBuffer
            6.LongBuffer
            7.ShortBuffer
        1.创建缓冲区
            static  ByteBuffer allocate(int capacity)参数:capacity 缓冲区的容量,以字节为单位
        2.向缓冲区里写数据
            put(byte b)
            put(byte[] src)
            putXxx()
        3.从缓冲区里读数据
            get()
            当向缓冲区里写入字节时,比如1,当调用get()方法时,得到的却是0
        4.Buffter缓冲区的4个关键元素
            ①capacity,缓存区总容量
            ②limit的大小<=capacity的大小,创建缓冲区时,默认=capacity
            ③position,初始位置在缓存区的0位,当写入数据时,数据的写入位置就是position的位置
            写完后,position的位置+1。
            ④mark,标记
        5.filp()方法:
            flip()反转缓冲区,作用相当于buffer.limit(buffer.position());+    buffer.position(0);
            HasRemaining方法:
                告知当前位置(position)和限制位(limit)之间是否还有元素
        6.rewind()重绕缓冲区
            将position置为0
        7.clear()清空缓冲区
            //clear的作用是清空缓冲区,但并不真正的清除数据,而是把postion置为0,limit置为容量上限
            //所以,当get(0)时,是可以得到原缓冲区数据的。但是我们一般都是在clear()方法之后,写数据,然后filp()
            //所以,并不影响缓冲区的重用。
    Channel
        Channel:通道,面向缓冲区,进行双向传输
        总接口:Channel
        其中重点关注他的4个子类
            操作TCP的SocketChannel
                SocketChannel默认也是非阻塞的,需要个更改下configureBlocking(false);
                ServerSocketChannel是一个抽象类,不能直接new,所以调用其静态方法open()
                    创建出来之后,默认是阻塞的,如果要设置成非阻塞模式,需要设置:
                    ssc.configureBlocking(false); 属性为false表示非阻塞
            和ServerSocketChannel
                SocketChannel默认也是非阻塞的,需要个更改下configureBlocking(false);
            操作UDP的DatagramChannel
            操作文件的FileChannel
                 * 这个方法用来测试FileChannel,FileChannel只能通过FileInputStream,FileOutputStream和
                 * RandomAccessFile的getChannel()方法得到。
                 * FileChannel在文件操作上,性能上没什么差别。读或写都是通过缓冲区来操作。此外还提供了一些额外方法,比如可以指定从文件的某个位置开始读或写
                 * 如果FileChannel是通过FileInputStream得到,那他只能读文件,不能写文件。
                 * 通过RandomAccessFile得到的FileChannel,既可以对指定文件读也可以写。并且都可以指定开始读或写的位置
    Selector
        Selector 多路复用器:
            可以理解为路由器和交换机
            在一个Selector上可以同时注册多个非阻塞的通道,从而只需要很少的线程数既可以管理许多通道。特别适用于开启许多通道但是每个通道中数据流量都很低的情况
       

 


        //select()是选择器selector查询是否有事件触发的方法,比如accpet事件是否触发,如果accpet事件触发:
        //就意味着有客户端接入了。注意,select()是一个阻塞方法。当有事件被触发时,阻塞放开。
        //引入selector的好处是:线程不必每时每刻都去工作、去查询客户端是否有新事件,没有事件的时候,线程就睡觉,休息
        //有事件发生,selector会知道,线程再醒来工作。这样一来,可以避免了线程无意义的空转,节省cpu资源,同时也不影响工作
    Zero-Copy
        原理概述
            zero copy(零复制)是一种特殊形式的内存映射,它允许你将Kernel内存直接映射到设备内存空间上。其实就是设备可以通过直接内存访问(direct memory access,DMA)方式来访问Kernal Space。
        我们拿Kafka举例,Kafka会对流数据做持久化存储以实现容错
            这意味着当Consumer从Kafka消费数据时,会有很多data从硬盘读出之后,会原封不动的通过socket传输给用户。
            数据都被拷贝了四次!
        Zero Copy的具体实现
            实际上第二次和第三次copy是毫无意义的。应用程序仅仅缓存了一下data就原封不动的把它发回给socket buffer。实际上,data应该直接在read buffer和socket buffer之间传输,
        为什么要使用kernel buffer做中介
            使用kernel buffer做中介(而不是直接把data传到user buffer中)看起来比较低效(多了一次copy)。
            然而实际上kernel buffer是用来提高性能的。
            在进行读操作的时候,kernel buffer起到了预读cache的作用。当写请求的data size比kernel buffer的size小的时候,这能够显著的提升性能
            在进行写操作时,kernel buffer的存在可以使得写请求完全异步。
            但悲剧的是,当读请求的data size远大于kernel buffer size的时候,这个方法本身变成了性能的瓶颈。
       

 

posted @ 2019-03-19 13:00  Striver。  阅读(199)  评论(0编辑  收藏  举报