JavaIO的演变之路 IO模型:就是用什么样的通道或者说是通信模式和架构进行数据的传输和接受,很大程度上决定了程序通信的性能,在Java当中一种支持3种IO模型。 BIO、NIO、AIO 在实际通信需求下,要根据不同的业务场景和性能需求决定选择不同的IO模型。IO模型 JavaBIO:同步并阻塞的(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有链接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销。 BIO方式主要适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1。4之前的唯一选择,但程序方面简单易理解。 JavaNIO:同步非阻塞。服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理。 NIO方式只要适用于连接数目比较多且连接数据比较短(轻操作)的架构。比如:聊天服务器,弹幕系统,服务器间通信,JDK1。4开始支持这方面的了。 JavaAIO:异步非阻塞(NIO。2),服务器实现模式为一个有效情趣一个线程,客户端的IO请求都是由OS先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数目较多并连接时间较长的应用。 AIO方式主要适用于连接数目多且连接数据比较长(重操作)的结构。比如:相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7之后开始支持。基本介绍以及工作机制 JavaBIO就是传统的JavaIO编程,其相关的类和接口是在java。io包中。 BIO(blockingIO)同步阻塞,服务器实现模式为一个连接一个线程,即客户端游连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销,可以通过线程池机制来进行改善(实现多个客户连接服务器) 网络编程的基本模型是ClientServer模型,也就是两个进程之间进行相互的通信,其中服务端提供位置信息(绑定IP地址和端口),客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。 传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址,启动监听端口;客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。 基于BIO模式下的通信,客户端服务端是完全同步的,完全耦合的。 BIO模式下多发和多收模式 代码演示,代码说明一切 服务端:importjava。io。BufferedRimportjava。io。InputSimportjava。io。InputStreamRimportjava。net。ServerSimportjava。net。S服务端可以反复的接收消息,客户端可以反复的发送消息publicclassServer{publicstaticvoidmain(String〔〕args){try{System。out。println(服务端启动);1。定义一个ServerSocket对象进行服务端的端口注册ServerSocketssnewServerSocket(9999);2。监听客户端的Socket连接请求Socketsocketss。accept();3。从socket管道中得到一个字节输入流对象InputStreaminputStreamsocket。getInputStream();4。把字节输入改为字符输入流InputStreamReaderreadernewInputStreamReader(inputStream);4。把字符输入流包装成一个缓存字符输入流BufferedReaderbrnewBufferedReader(reader);Swhile((msgbr。readLine())!null){System。out。println(服务端接收到:msg);}}catch(Exceptione){e。printStackTrace();}}}复制代码 客户端:importjava。io。OutputSimportjava。io。PrintSimportjava。net。Simportjava。util。S客户端用来发送数据publicclassClient{publicstaticvoidmain(String〔〕args){try{1。创建Socket对象请求服务端的连接SocketsocketnewSocket(127。0。0。1,9999);2。从Socket对象中获取一个字节输出流OutputStreamossocket。getOutputStream();3。把字节输出流包装成一个打印流PrintStreampsnewPrintStream(os);ScannerscnewScanner(System。in);while(true){System。out。print(请说:);Stringmsgsc。nextLine();ps。println(msg);ps。flush();}}catch(Exceptione){e。printStackTrace();}}}复制代码 在上面的通信当中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将会一直处于阻塞状态。 同时服务端是按照行获取信息的,这就意味着客户端也必须按照进行消息的发送,否则服务端将进入等待消息的阻塞状态。 同时在这,若先关闭客户端,那么服务端就将会报错。Connectionreset(连接重置的意思) BIO模式下接受多个客户端 上面的是一个服务端只能结束一个客户端的通信请求,那么如果服务端需要很多个客户端的信息通信请求应该如何处理,在这个时候我们就可以引入线程,也就是说客户端没发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型了。 代码说明: 服务端代码:importjava。net。ServerSimportjava。net。S服务端接受客户端发来的请求因为服务端要求接受多个客户端的连接,所以以现场操作进行publicclassServer{publicstaticvoidmain(String〔〕args){try{System。out。println(服务端启动);1。注册端口ServerSocketssnewServerSocket(9999);2。定义一个死循环,负责不断的接收客户端的Socket的连接请求while(true){阻塞式的,当遇到一个客户端请求连接,通过,当没有的时候,阻塞Socketsocketss。accept();3。创建一个独立的线程来处理与这个客户端的socket通信的具体需求newServerThread(socket)。start();}}catch(Exceptione){e。printStackTrace();}}}复制代码 服务端需要接受多个客户端,就要开启多线程。多线程代码:importjava。io。BufferedRimportjava。io。InputSimportjava。io。InputStreamRimportjava。net。S服务端创建的线程,服务端后执行的东西主要就是在这进行执行publicclassServerThreadextendsThread{privateSpublicServerThread(Socketsocket){this。}Overridepublicvoidrun(){在这执行具体的客户端的具体操作try{从socket对象中得到字节的输入流InputStreaminputStreamsocket。getInputStream();使用缓存字符输入流包装字节的输入流BufferedReaderreadernewBufferedReader(newInputStreamReader(inputStream));Swhile((msgreader。readLine())!null){System。out。println(客户端接收到:msg);}}catch(Exceptione){e。printStackTrace();}}}复制代码 客户端代码,和上面比,没有什么变化:importjava。io。PrintSimportjava。net。Simportjava。util。S客户端向服务端发送数据的方向publicclassClient{publicstaticvoidmain(String〔〕args){try{System。out。println(客户端启动);1。请求与服务端的Socket对象连接SocketsocketnewSocket(127。0。0。1,9999);2。得到一个打印流PrintStreampsnewPrintStream(socket。getOutputStream());3。使用循环不断的发送消息给服务端接收ScannerscnewScanner(System。in);while(true){System。out。print(请说:);Stringmsgsc。nextLine();ps。println(msg);ps。flush();}}catch(Exceptione){e。printStackTrace();}}}复制代码 运行结果: 在这我们实现服务端接受多个客户端,使用的是多线程的方式。 而每个Socket接受的时候,都会创建一个线程,而线程的竞争、切换上下文都会影响性能。 每个线程都会占用栈的空间和CPU资源。 在这我们接受的是多个客户端,但并不是每个socket都在进行IO操作,所以这些无意义的线程也会造成性能的占用。 客户端的并发访问增加的时候,服务端将呈现的是1:1的线程开销,访问量越大,系统就将会发生线程栈溢出,线程创建失败,最终导致进程宕机或者死机,从而不能再提供服务。伪异步IO编程 在上面的使用的接受多个客户端并发访问增加的时候。服务端将呈现1:1的现开销,访问量越大,系统发生线程栈溢出,最终导致栈溢出或者僵死,从而不能对外提供服务。所以在这我们就可以使用伪异步IO的通信模式 采用线程池和任务队列实现,当客户端接入的时候,将客户端的Socket封装成一个Task(该任务实现java。lang。Runnable(线程任务接口))交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池中可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,它会通过自身机制的处理从而不对导致资源的耗尽和宕机。 戳一下:》了解线程池 代码说明:服务端:importjava。net。ServerSimportjava。net。S服务端publicclassServer{publicstaticvoidmain(String〔〕args){try{System。out。println(服务端启动);1。注册端口ServerSocketserverSocketnewServerSocket();2。定义一个死循环,负责不断循环接受客户端的请求初始化一个线程池对象HandlerSocketServerPoolpoolnewHandlerSocketServerPool(3,3,10);while(true){阻塞式的,当遇到一个客户端请求连接,通过,当没有的时候,阻塞SocketsocketserverSocket。accept();把socket对象交给一个线程池进行处理将socket封装为一个任务对象,然后交给线程池ServerRunnablerunnablenewServerRunnable(socket);pool。execute(runnable);}}catch(Exceptione){e。printStackTrace();}}}复制代码 Socket任务类importjava。io。BufferedRimportjava。io。InputSimportjava。io。InputStreamRimportjava。net。SpublicclassServerRunnableimplementsRunnable{privateSpublicServerRunnable(Socketsocket){this。}Overridepublicvoidrun(){try{处理接受到客户端socket需求从socket管道中得到一个字节输入流对象InputStreamstreamsocket。getInputStream();把字节输入流包装成一个缓存字符输入流BufferedReaderreadernewBufferedReader(newInputStreamReader(stream));Swhile((msgreader。readLine())!null){System。out。println(服务端接受到:msg);}}catch(Exceptione){e。printStackTrace();}}}复制代码 线程池处理类将任务放到线程池里面publicclassHandlerSocketServerPool{privateExecutorServiceexecutorSpublicHandlerSocketServerPool(intcorePoolSize,intmaxThreadNum,intqueueSize){定义线程池publicThreadPoolExecutor(intcorePoolSize,核心线程池的大小intmaximumPoolSize,线程池中最大的线程数量longkeepAliveTime,线程在没有任务执行时最多保持多久时间会终止TimeUnitunit,keepAliveTime的时间单位BlockingQueueRunnableworkQueue任务队列,也叫阻塞队列,用来存储等待执行任务){this(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,Executors。defaultThreadFactory(),defaultHandler);executorServicenewThreadPoolExecutor(corePoolSize,maxThreadNum,120,TimeUnit。SECONDS,newArrayBlockingQueue(queueSize));}提供一个方法来提交任务给线程池的任务队列当中暂存,等待线程池来处理publicvoidexecute(Runnablerunnable){executorService。execute(runnable);}}复制代码 可以理解为Runnable用于将socket请求封装,但是Runnable线程暂时不执行,然后将封装好的线程交给线程池进行执行。 客户端还是上面代码的客户端:importjava。io。PrintSimportjava。net。Simportjava。util。S客户端向服务端发送数据的方向publicclassClient{publicstaticvoidmain(String〔〕args){try{System。out。println(客户端启动);1。请求与服务端的Socket对象连接SocketsocketnewSocket(127。0。0。1,9999);2。得到一个打印流PrintStreampsnewPrintStream(socket。getOutputStream());3。使用循环不断的发送消息给服务端接收ScannerscnewScanner(System。in);while(true){System。out。print(请说:);Stringmsgsc。nextLine();ps。println(msg);ps。flush();}}catch(Exceptione){e。printStackTrace();}}}复制代码 结果演示: 当我们关闭客户端2号的时候。 伪异步采用了线程池的设计思想,因此避免了为每一个独立线程造成线程资源耗尽的问题,但终归到底它的底层依然采用的是同步阻塞模型,在更本上是解决这个问题。 如果单个消息处理的缓慢,或者是服务器线程池中的全部线程都被阻塞,那么后续的socket的IO消息都将被堵塞在消息队列中。新的Socket请求堵在消息队列当中,使得客户端发生大量的连接超时。NIO和BIO比较 BIO是以流的方式处理数据的,而NIO是以块的方式处理数据的,块IO的效率是比流IO要高很多的。 BIO是阻塞的,NIO则是非阻塞的。 BIO是基于字节流的字符流进行操作的; 而NIO是基于Channel(通道)和Buffer(缓冲区)进行操作的,数据总是从缓冲区写入到通道中,或者从通道读取到缓冲区中。Selector(选择器)是用于监听多个通道的事件(比如:连接请求、数据到达等),因此使用单个线程就可以监听多个客户端通道。 NIO BIO 面向缓存区(Buffer) 面向流(Stream) 非阻塞(NonBlockingIO) 阻塞IO(BlockingIO) 选择器(Selector) NIO(重点)基本介绍(三大核心) JavaNIO(newIO)也有人称为javanonblockingIO是java1。4版本开始引入的一个新的IOAPI,它是可以替代标准的javaIOAPI。 NIO与原来的IO是由同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。 NIO是可以以更高效的方式进行文件的续写操作,NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞的进行,线程在读写期间是不能受其他事情干扰的。比如在执行socket。read()方法的时候,如果服务器一直没有数据传输过来,线程就一直处于阻塞,而NIO中就可以配置socket为非阻塞模式。 NIO的相关类都是放在java。nio包以及子包之下的,并且是对原java。io包中的很多类进行改写的。 JavaNIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用的话,就什么都不会获取,而是保持线程的阻塞,所以直到数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞的写操作也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时是可以去做别的事情的。 NIO有着三大核心部分:Channel(通道)、Buffer(缓冲区)、Selecot(选择器) Channel:通道。JavaNIO的通道类似流,但又有一些不同,是既可以从通道中读取数据,又可以写数据到通道里的。但留的(input或output)读写通常是单向的。通道可以以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写。 Buffer:缓冲区。缓冲区本质上就是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIOBuffer对象,并提供一组方法,用来方便的访问该块内存。相比较之下对数组的操作。BufferAPI更加容易操作和管理。 Selecot:选择器。它是JavaNIO的组件,可以检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样一来一个单独的线程就可以管理多个Channel,从而达到管理多个网络的连接,提高效率。 每个Channel(通道)都有一个它对应的Buffer(缓存区)。一个线程对应一个Selector(选择器),一个Selector对应多个Channel(通道)。程序切换到那个Channel是由事件所决定的。Selector会根据不同的事件在在各个通道上进行切换。Buffer(缓存区)就是一个内存块,底层是数组数据的读取写入是通过Buffer完成的,BIO中要么是输入流,或者是输出流,是不能双向的,但是NIO的Buffer即可以读也可以写的。JavaNIO系统的核心就是通道(Channel)和缓存区(Buffer)。通道表示打开到IO设备(例如:文件、套接字)的连接。若需要使用NIO系统,那么就需要获取用于连接IO设备的通道以及用于容纳数据的缓存区,然后对缓存区进行操作以达到对数据的处理。简而言之,Channel负责传输,Buffer负责存取数据。三大核心:Buffer(缓存区) 就是一个用于特定基本数据类型的容器。是由java。nio包所定义的。所有具体的缓存区都是Buffer抽象类的子类。 JavaNIO中的Buffer主要用于与NIO的通道进行交互,数据是从通道读物缓存区或者从缓存区写入到通道中的。 Buffer就像一个数组,可以保存庽相同类型的数据。根据数据类型的不同,可以分为好几个Buffer的子类。(例如:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer)这些子类都采用了相似的方法进行数据的管理,只是管理的数据类型不同。缓存区的基本属性容量(capacity):作为一个内存块,Buffer在初始化的时候就具有一定的固定大小,也称为容量,缓存区容量不能为负,并且创建好后就不能再更改了。限制(limit):表示缓存区中可以操作数据的大小,是在容量大小里面动态变化的。在limit后的数据是不能再进行读写的。在写入模式下,limit等于Buffer的容量。在读取模式下,limit等于容量里面实际数据的大小。位置(position):表示下一个要读取或写入的数据索引。标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中的一个特定的position,之后可以通过调用reset()方法直接恢复到这个position。 标记、位置、限制、容量遵守以T不变式:0markpositionlimitcapacity Buffer中常见的方法 Bufferclear()清空缓存区并返回对缓存区的引用 Bufferflip()将缓存区的界限设置为当前位置,并将当前pasition位置防雨起始读写位置,将limit放与数据结束位置,就是改为可读模式 intcapacity()返回Buffer中的capacity大小 booleanhasRemaining()判断缓存区中是否还有元素 intlimit()返回Buffer的界限(limit)的位置 Bufferlimit(intn)将设置缓存区界限为n,并返回一个具有新limit的缓存区对象 Buffermark()对缓存区设置标记 intposition()返回缓存区的当前位置position Bufferposition(intn)将设置缓存区的当前位置为n,并返回修改后的Buffer对象 intremaining()返回position和limit之间的元素个数 Bufferreset()将位置position转到以前设置的mark所在的位置 Bufferrewind();将位置设为为0。取消设置的mark Buffer所有子类提供了两个用于数据操作的方法:get()put()方法 获取Buffer中的数据 get():读取单个字节 get(byte〔〕dst):批量读取多个字节到dst中 get(intindex):读取指定索引位置的字节(不会移动position) 加入数据到Buffer中 put(byteb):将给定单个字节写入缓冲区的当前位置 put(byte〔〕src):将src中的字节写入缓存区的当前位置 put(intindex,byteb)将指定字节写入缓存区的索引位置(不会移动position) 代码演示:importjava。nio。ByteB目标:对缓存区Buffer的常用API进行实现publicclassDemo01{publicstaticvoidmain(String〔〕args){分配了一个缓冲区,将容量设置为10ByteBufferbufferByteBuffer。allocate(10);System。out。println(起始位置为:buffer。position());0System。out。println(限制位置为:buffer。limit());10System。out。println(缓冲区容量:buffer。capacity());10System。out。println();put方法往缓冲区里面添加数据StringnameXiCbuffer。put(name。getBytes());System。out。println(起始位置为:buffer。position());6System。out。println(限制位置为:buffer。limit());10System。out。println(缓冲区容量:buffer。capacity());10System。out。println();flip方法是将缓冲区的界限设置为当前位置0可读模式buffer。flip();System。out。println(起始位置为:buffer。position());0System。out。println(限制位置为:buffer。limit());6System。out。println(缓冲区容量:buffer。capacity());10System。out。println();get数据的读取charch(char)buffer。get();System。out。println(ch);System。out。println(起始位置为:buffer。position());1System。out。println(限制位置为:buffer。limit());6System。out。println(缓冲区容量:buffer。capacity());10System。out。println();读取数据定义一个byte数组长为2的数组byte〔〕bytesnewbyte〔2〕;buffer。get(bytes);StringsnewString(bytes);System。out。println(s);System。out。println(起始位置为:buffer。position());3System。out。println(限制位置为:buffer。limit());6System。out。println(缓冲区容量:buffer。capacity());10System。out。println();mark标记此刻读取到的位置buffer。mark();此处标记的位置是3byte〔〕bsnewbyte〔2〕;buffer。get(bs);StringstnewString(bs);System。out。println(st);hasRemaining返回Boolean值,表示缓存区是否还有数据if(buffer。hasRemaining()){remaining返回int值,表示缓存区还有多少个数据System。out。println(剩余元素为:buffer。remaining());1}回到标记位置buffer。reset();if(buffer。hasRemaining()){System。out。println(剩余元素为:buffer。remaining());3}System。out。println();clear清除缓存区所有的数据buffer。clear();System。out。println(起始位置为:buffer。position());0System。out。println(限制位置为:buffer。limit());10System。out。println(缓冲区容量:buffer。capacity());10clear清除数据后,再读取buffer里面的数据,还能读取到System。out。println((char)buffer。get());X说明clear并不是将数据清除了,而只是改变了索引的指向位置在后面进行添加数据的时候,才会覆盖这写数据}}复制代码直接缓存与非直接缓存 根据官方文档,btyeBuffer是可以分为两种类型的,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。 对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作; 而非直接内存,也就是堆内存,如果要作IO操作,会先从本进程内存复制到直接内存,在利用本地IO操作。 从数据流角度来分析来看至直接缓存和非直接缓存 本地IO直接内存本地IO 本地IO直接内存非直接内存直接内存本地IO 通过查看数据流,在做IO操作的时候,比如网络发送大量的数据时,直接内存会具有更高的效率。直接内存使用allocateDirect创建,但是它比申请普通的堆内存来说是需要消耗更高的性能。不过,在这部分的数据是在JVM之外的,因此它不会占用应用的内存。只是一般来说,如果在不能带来很明显的性能提升下,还是推荐直接使用非直接内存模式。 字节缓存区是直接缓存区还是非直接缓存区可以通过调用isDirect()方法来调用。三大核心:Channel(通道) 通道:由java。nio。channels包定义的。Channel表示IO源与目标打开的连接。Channel类似于传统的流。只不过Channel本身是不能直接访问数据的,Channel只能与Buffer进行交互。 NIO的通道类似于流,但又有一些区别:通道可以同时进行读写,而流只能读或者只能写。通道可以实现异步读写数据。通道可以从缓冲区读数据,也可以写数据到缓冲区。 BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。 Channel在NIO中实际只是一个接口。 常见的实现Channel类的子类: FileChannel:用于读取、写入、映射和操作文件的通道 DatagramChannel:通过UDP来读写网络中的数据通道。 SocketChannel:通过TCP来读写网络中的数据通道。 ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都创建一个SocketChannel。【ServerSocketChannel类似ServerSocket,SocketChannel类似Socket】。 获取通道 在获取通道的一种方式是对支持通道的对象调用getChannel()方法。支持的通道的类都有: FileInputStream、FileOutputStream、RandomAccessFile、DatagramSocket、Socket、ServerSocket。 获取通道的其他方式是使用Files类的静态方法newByteChannel()获取字节通道。或通过通道的静态方法open()打开并返回指定通道。 FileChannel通道常用的方法: intread(ByteBufferdst)从Channel当中读取数据至 ByteBufferlongread(ByteBuffer〔〕dsts)将channel当中的数据分散至ByteBuffer〔〕 intwrite(Bytesuffersrc)将ByteBuffer当中的数据写入到Channel longwrite(ByteBuffer〔〕srcs)将Bytesuffer〔〕当中的数据聚集到Channel longposition()返回此通道的文件位置 FileChannelposition(longp)设置此通道的文件位置longsize()返回此通道的文件的当前大小 FileChanneltruncate(longs)将此通道的文件截取为给定大小 voidforce(booleanmetaData)强制将所有对此通道的文件更新写入到存储设备中 代码演示: 写入数据:importjava。io。FileOutputSimportjava。nio。ByteBimportjava。nio。channels。FileCpublicclassDemo02{publicstaticvoidmain(String〔〕args){try{1。字节输出流通向目标文件FileOutputStreamfosnewFileOutputStream(demo。txt);2。得到字节输出流对应的通道ChannelFileChannelchannelfos。getChannel();3。分配缓存区ByteBufferbufferByteBuffer。allocate(1024);buffer。put(星辰是个大帅比!。getBytes());4。把缓存区切换为写模式buffer。flip();通过通道写入缓存区的数据channel。write(buffer);关闭通道channel。close();System。out。println(写数据到文件中!);}catch(Exceptione){e。printStackTrace();}}} 成功生成了这个文档: 读取数据:importjava。io。FileInputSimportjava。nio。ByteBimportjava。nio。channels。FileCpublicclassDemo03{publicstaticvoidmain(String〔〕args){try{定义一个文件字节输入流与源文件接通FileInputStreaminputStreamnewFileInputStream(demo。txt);需要得到文件字节输入流的文件通道FileChannelchannelinputStream。getChannel();定义一个缓存区ByteBufferbufferByteBuffer。allocate(1024);读取数据到缓存区channel。read(buffer);buffer。flip();归位读取出缓存区中的数据并输出即可StringstringnewString(buffer。array());StringstringnewString(buffer。array(),0,buffer。remaining());System。out。println(string);}catch(Exceptione){e。printStackTrace();}}} 三大核心:Selector(选择器) 选择器(Selector)是SelectableChannle对象的多路复用器,Selector是可以同时监控多个SelectorChannel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel。 Selector是非阻塞IO的核心。 Java的NIO,用非阻塞的IO方式,可以用一个线程,处理多个的客户端连接,就会使用到的Selector(选择器) Selector能够坚持到多个注册的通道上若有事件发生(注意:多个Channel以事件的方式可以注册到同一个Select(),如果有事件发生,便获取事件后然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。) 只有在连接通道真正有读写事件发生的时候,才会进行读写,就大大减少了系统的开销,并且不必为每个连接都创建一个线程,不用于维护多个线程,避免了多线程之间的上下文导致的开销。 选择器的应用 创建Selector:通过Selector。open()方法创建出一个Selector。SelectorselectorSelector。open(); 向选择器注册通道:SelectableChannle。register(Selectorsel,intops);1。获取通道ServerSocketChannelsocketChannelServerSocketChannel。open();2。切换非阻塞模式socketChannel。configureBlocking(false);3。绑定连接socketChannel。bind(newInetSocketAddress(9898));4。获取选择器socketChannelselectorSelector。open();5。将通道注册到选择器上,并且指定监听接收事件socketChannel。register(select,SelectionKey。OPACCEPT); 当调用register(Selectorsel,mtops)将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数。ops指定。可以监听的事件类型(用可使用SelectionKey的四个常量表示): 读:SelectionKey。OPREAD(1) 写:SelectionKey。OPWRITE(4) 连接:SelectionKey。OPCONNECT(8) 接收:SelectionKey。OPACCEPT(16) 若注册时不止监听一个事件,则可以使用‘位或操作符连接intinterestSetselectionKey。OPREADSelectionKey。OPWERITE JavaAIO(也叫做NIO。2):异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由OS先完成了再通知服务器应用去启动线程进行处理。 AIO是异步非阻塞,基于NIO,所以也将其称之为NIO2。0。 BIO NIO AIO Socket SocketChannel AsynchronousSocketChannel ServerSocket ServerSocketChannel AsynchronousServerSocketChannel 与NIO不同的是,当AIO进行读写操作时,只须直接调用API的read或write方法即可,这两种方法均为异步的,对于读操作而言,当有流可读的时候,操作系统会将可读的流传入read方法的缓冲区,对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,readwrite方法都是异步的,完成后会主动调用回调函数。在JDK1。7中,这部分内容被称作NIO。2,主要在java。nio。channel包下增加了下面四个异步通道:AsynchronousSocketChannelAsynchronousServerSocketChannelAsynchronousFileChannelAsynchronousDatagramChannelBIO、NIO、AIO总结 BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。 BlO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1。4以前的唯一选择,但程序直观简单易理解。 NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。 NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1。4开始支持。 AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由OS先完成了再通知服务器应用去启动线程进行处理。 AlO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。 作者:马小屑 链接:https:juejin。cnpost7120431269833343007