BIO:
Blocking I/O,传统的同步阻塞式网络编程;
网络编程的基本模型是C/S模型,即两个进程间的通信;服务端提供IP和监听端口,客户端通过连接向服务端发起连接请求,通过三次握手连接,若成功则双方通过套接字进行通信;
采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程,
进行链路处理,处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通宵模型。
问题是:
缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系;
线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死-掉-了
伪异步I/O编程
为了改进这种一连接一线程的模型,使用线程池来管理这些线程,实现1个或多个线程处理N个客户端的模型(但是底层还是使用的同步阻塞I/O),
通常被称为“伪异步I/O模型“。
不能使用使用CachedThreadPool线程池,建议FixedThreadPool,
如果发生大量并发请求,超过最大数量的线程就只能等待,直到线程池中的有空闲的线程可以被复用。
在读取数据较慢时(比如数据量大、网络传输慢等),大量并发的情况下,其他接入的消息,只能一直等待,这是问题。
什么是NIO?
New I/O简称,或Non-block I/O,与旧式的基于流的I/O方法相对,表示一套新的I/O标准;jdk1.4引入的;
NIO提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel,两种不同的套接字通道实现。
新增的着两种通道都支持阻塞和非阻塞两种模式,阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反
对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;
对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。
NIO是基于块(Block)的,以块为基本单位处理数据,传统I/O基于字节(Byte)的;性能比流好些;
为所有原始类型提供(Buffer)缓存支持;
增加通道(Channel)对象,作为新的I/O抽象,类似旧式的Stream;
支持文件锁,和内存映射文件的文件访问接口;使用文件来实现锁;
基于网络操作,提供了Selector的异步网络I/O;
Buffer(缓存区)&Channel(通道)
Buffer:NIO的核心部分,所有IO操作部分都要Buffer做,与Channel(通道)交互;
缓冲区实际上是一个数组,并提供了对数据结构化访问以及维护读写位置等信息。
Channel,通道是IO的抽象,另一端就是要操作的对象或socket;
通过对buffer的读取,来实现对IO的操作;
ByteBuffer、CharBuffer,DoubleBuffer,IntBuffer,LongBuffer等,实现了Buffer;
Channel:我们对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。
通道不同于流的地方就是通道是双向的,可以用于读、写和同时读写操作。
底层的操作系统的通道一般都是全双工的,所以全双工的Channel比流能更好的映射底层操作系统的API。
Channel主要分两大类:
SelectableChannel:用户网络读写(子类有:ServerSocketChannel和SocketChannel);
FileChannel:用于文件操作;
基本使用一:
FileInputStream fin = new FileInputStream(new File("d:\\temp_buffer.tmp"));
FileChannel fc=fin.getChannel();
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
fc.read(byteBuffer);
fc.close();
byteBuffer.flip();
基本使用二:NIO复制文件
public static void nioCopyFile(String resource, String destination)
throws IOException {
FileInputStream fis = new FileInputStream(resource);
FileOutputStream fos = new FileOutputStream(destination);
FileChannel readChannel = fis.getChannel(); //读文件通道
FileChannel writeChannel = fos.getChannel(); //写文件通道
ByteBuffer buffer = ByteBuffer.allocate(1024); //读入数据缓存
while (true) {
buffer.clear();
int len = readChannel.read(buffer); //读入数据
if (len == -1) {
break;
//读取完毕
}
buffer.flip();
writeChannel.write(buffer);//写入文件
}
readChannel.close();
writeChannel.close();
}
Buffer中3个重要参数:位置(position)、容量(capacity)、上限(limit);
位置:记录当前缓冲区的位置,将从此位置开始读写数据;
容量:缓存区的总容量上限;
上限:缓存区实际数据的大小,可读取的容量,总是小于等于容量;通常情况和容量相等;
Buffer的3个函数:
flip():读写转换时使用;先将limit设置为当前position的位置(实际大小),再将position重置为0,清除标志位Mark;
rewind():将position置零,并清除标志位;
clear():将position置零,同时将limit设置为capacity大小,清除标志位Mark;
实例展示:
ByteBuffer b=ByteBuffer.allocate(15); //15个字节大小的缓冲区 | |
System.out.println("limit="+b.limit()+"capacity="+b.capacity()+"position="+b.position(); |
b.put((byte)i);
}
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
b.flip(); //重置position | |
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position()); | |
for(int i=0;i<5;i++){ |
}
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
文件映射到内存实例:
RandomAccessFile raf = new RandomAccessFile("C:\\mapfile.txt", "rw");
FileChannel fc = raf.getChannel(); //将文件映射到内存中
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, raf.length());
while(mbb.hasRemaining()){
System.out.print((char)mbb.get());
}
mbb.put(0,(byte)98); //修改文件
raf.close();
多路复用器 Selector
NIO的基础;Selector提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel;
如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,
然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。
所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。
网络编程
多线程网络服务器的一般结构:
(图,N<--->N)
问题:
为每一个客户端开启一个线程,如果客户端延时异常,线程可能被长时间占用,因为数据的准备和读取都在这个线程中,占用时间;
若客户端数量众多,可能消耗大量的系统资源;
解决:
非阻塞的NIO;
数据准备好了再工作;
NIO做法:把数据准备好了再通知我;
Channel类似于流,一个Channel可以和文件或者网络Socket对应;
一个Selector对应一个线程,可以轮询多个Channel,一个Channel背后对应一个Socket,即一个客户端;
所以一个线程可以轮询了多个channel,选择准备好数据的Channel;
Selector的两个方法,
select():若没有准备好数据的Channel,则阻塞;准备好数据后返回SelectionKey,表示一对关系selector和channel的对应;
selectNow():也是选择已经准备好的channel,若没有准备好的,则直接返回,不阻塞;
代码实例:
一定要写个例子看看哦;
总结:
NIO会将数据准备好后再交给应用处理,数据读取过程依然在线程中完成;
节省数据准备时间(因为selector可以复用);
AIO
NIO是剥离了数据准备时,避免大量线程的等待操作,数据读写处理还是在线程应用中完成;
AIO更进一步,在数据准备及读写时都不处理,等数据操作完成后,加入回调函数即可;异步操作IO;
IO本身的速度是没有改变的,这几种方式只是改变里处理IO的方式,改善线程调度,看起来系统性能变高;
server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));
使用server上的accept()方法:
public abstract <A> void accept(A attachment, CompletionHandler<AsynchronousSocketChannel,?super A> handler);
还有read()、 write() 方法;AsynchronousSocketChannel.read(ByteBuffer b, ...)
为什么需要了解NIO和AIO?
AIO是真正的异步非阻塞的,所以,在面对超级大量的客户端,更能得心应手。
具体选择什么样的模型或者NIO框架,完全基于业务的实际应用场景和性能需求,
如果客户端很少,服务器负荷不重,就没有必要选择开发起来相对不那么简单的NIO做服务端;
相反,就应考虑使用NIO或者相关的框架了