1,IO
IO即input和output的缩写,在java语境里,通常表达的是数据的流入和流出。
流是指数据的无结构化传递,以无结构字节序列或者字符序列进入输入和输出。IO流即是进行输入和输出操作的流。
字节流的最小操作单位是一个字符,字符即是字节加上编码表,单个字符占用1到多个字节。
2,BIO
BIO是最传统的同步阻塞IO模型,服务器端的实现是一个连接只有一个线程处理,线程在发起请求后,会等待链接返回。传统IO线程大部分时间可能都是在等待read方法返回,正是由于这个读数据的方法是阻塞调用的,因此每个线程只能处理一个连接,如果请求量非常大,那么这种方式就需要创建大量的线程。而系统的资源都是有限的,可能允许创建最大的线程数远远小于要处理的连接数,而且就算线程能被创建出来,大量的线程也会降低系统的性能。
3,NIO
NIO是指New I/O,既然有New I/O,那么就Old I/O,Old I/O是指基于流的I/O方法。从名字NIO是在Java1.4中被纳入到JDK中的,它最主要的特点是:提供了基于Selector的异步网络I/O,使得一个线程可以管理多个连接。
Buffer(缓冲区)
InputStream把读取到的数据存放在了byte数组中,如果用OutputStream写数据,那么也可以把byte数组中的数据写到文件中。而在NIO中,数据只能被写到Buffer中,同理读取到的数据也只能放到Buffer中,由此可见Buffer是Channel用来读写的非常重要的一个东西。
Channel(通道)
InputStream其实就是一个用来读取文件的通道。只不过InputStream是一个单向的通道,只能用来读取数据。而NIO中的Channel是一个双向的通道,不仅能读取数据,而且还能写数据。
Selector(选择器)
Selector是NIO中最重要的部分,是实现一个线程管理多个连接的关键,它的作用就是轮询所有被注册的Channel,一旦发现被注册的事件发生,就可以对这个事件进行处理。
NIO之Buffer
在JavaNIO中,Buffer主要的作用就是与Channel进行交互。它本质上是一块可读写的数据内存,这块内存中有很多可以存储byte、int、char等的小单元。这块内存被包装成NIOBuffer对象,并提供了一组方法,来简化数据的读写。在JavaNIO中,核心的Buffer有7个,ByetBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、ByteBuffer、FloatBuffer。
一般来说,读写buffer需要四个步骤:
1) 准备Buffer数据头。
2) 将数据读取到Buffer中。
3) 调用Buffer的flip()方法,将Buffer归零。
4) 写Buffer(包括数据头和数据内容)
Buffer中有几个重要属性:capacity、position、limit。
1) capacity用来表示Buffer的容量,也就是刚开始申请的Buffer大小。
2) position表示下一次读写位置,在写数据到Buffer中时,position表示当前可写的位置。初始的position值为0,当写入一个数据后,例如int或short到Buffer后,position会向前移动到下一个可插入数据的buffer单元。position最大的值为capacity-1。在读取数据的时候,也是从某个位置开始读,当从Buffer的positon处读取数据完成时,positon也会从向前位置移动到下一个可读位置。buffer从写入模式变为读取模式时,positoin会归零,每次读取后,position向后移动。
3) limit表示本次读、写的极限位置,在写数据时,limit表示最多能往Buffer里写入多少数据。它等同于buffer的容量。在读取数据的时候,limit表示最多能读到多少数据,也就是说positon移动到limit时读操作会停止。它的值等同于写模式下的position位置。
在写模式中,position表示写一个可写入的位置,一旦切换到读模式,position就会置0(可以从Buffer最开始的地方读数据),而此时这个Buffer的limit就是在读模式下的position,因为在position之后是没有数据的。
Buffer的使用步骤:
申请Buffer
在使用Buffer前必须先申请一块固定大小的内存空间来供Buffer使用,这个工作可以通过Buffer类提供的allocate()方法来实现。
向Buffer中写数据
可以通过Buffer中的put方法来写入数据,也可以通过Channel向Buffer中写数据。
读写模式的转换
Buffer的flip方法用来把Buffer从写模式转换为读模式,flip方法的底层实现原理为:把position置0,并把Buffer的limit设置为当前的position值。
从Buffer中读取数据
与写数据类似,读数据也有两种方式,分别是:通过Buffer的get方法读取,或从buffer中读取数据到Channel中。当完成数据的读取后,需要调用clear或compact方法来清空Buffer,从而实现Buffer的复用。这两个方法的实现原理为:clear方法会把position置0,把limit设置我capacity;由此可见,如果Buffer中还没有未读的数据,那么clear方法会清理这部分数据,如果想保留这部分未读的数据,那么就需要调用compact方法。将缓冲区当前位置和界限之间的int复制到缓冲区的开始处。即将索引p=position处的int复制到索引0处,将索引p+1处的int复制到索引1处,因此类推,直到将索引limit-1处的int复制到索引n=limit-1-p处,然后将缓冲区的位置设置为n+1,并将其界限设置为其容量。如果已定义了标记,那么丢弃它。
重复读取数据
Buffer还有另外一个重要的方法,rewind,它的实现原理如下:只把position的值置0,而limit保持不变,使用rewind方法可以实现对数据进行重复的读。
因此可见在NIO中使用Buffer的时候,通常都需要遵循以下四个步骤:
1) 向Buffer中写入数据。
2) 调用flip方法把Buffer从写模式切换到读模式。
3) 从Buffer中读取数据。
4) 调用clear方法或compact方法来清空Buffer
标记与复位
Buffer中还有两个非常重要的方法:mark和reset,mark方法用来标记当前的position,一旦标记完成,在任何时刻都可以使用reset方法来把position恢复到标记的值。
NIO之Channel
在NIO中,数据的读写都是通过Channel来实现的。Channel与传统的流非常类似,只不过Channel不能直接访问数据,而只能与Buffer进行交互,也就是说Channel只能通过Buffer来实现数据的读写。虽然通道与流有很多相似的地方,但是它们也有很多区别:
1) 通道是双向的,既可以读也可以写,但是大部分流都是单向的,只能读或者写。
2) 通道可以实现异步的读写,大部分流只支持同步的读写。
3) 通道的读写只能通过Buffer来完成。
在java语言中,主要有以下4个常见的channel的实现:
FileChannel:用来读写文件
DatagramChannel:用来对UDP的数据进行读写。
SocketChannel:用来对TCP的数据进行读写,一般用作客户端的实现。
ServerSockerChannel:用来监听TCP的连接请求,然后针对每个请求会创建一个SocketChannel,一般被用作服务器的实现。
1 public class Test {
2 public static void writeFile() {
3 RandomAccessFile randomAccessFile = null;
4 FileChannel fileChannel = null;
5 try {
6 randomAccessFile = new RandomAccessFile("input.txt", "rw");
7 fileChannel = randomAccessFile.getChannel();
8 } catch (Exception e) {
9 e.printStackTrace();
10 }
11 try (RandomAccessFile raf = randomAccessFile; FileChannel inChannel = fileChannel) {
12 //创建一个写线程的buffer
13 ByteBuffer writeBuf = ByteBuffer.allocate(24);
14 //写入数据
15 writeBuf.put("output text".getBytes());
16 //把buffer变为读模式
17 writeBuf.flip();
18 //从Buffer中读取数据写入Channel
19 inChannel.write(writeBuf);
20 } catch (Exception e) {
21 e.printStackTrace();
22 }
23 }
24
25 public static void readFile() {
26 RandomAccessFile randomAccessFile = null;
27 FileChannel fileChannel = null;
28 try {
29 randomAccessFile = new RandomAccessFile("input.txt", "rw");
30 fileChannel = randomAccessFile.getChannel();
31 } catch (Exception e) {
32 e.printStackTrace();
33 }
34 try (RandomAccessFile raf = randomAccessFile; FileChannel inChannel = fileChannel) {
35 //创建用来读取数据的buffer
36 ByteBuffer readBuf = ByteBuffer.allocate(24);
37 //从channel中把数据读取到Buffer中
38 int bytesRead = inChannel.read(readBuf);
39 while(bytesRead != -1) {
40 System.out.println("Read" + bytesRead);
41 //把Buffer调整为读模式
42 readBuf.flip();
43 //如果还有未读内容
44 while(readBuf.hasRemaining()) {
45 System.out.print((char)readBuf.get());
46 }
47 //清空缓存区
48 readBuf.clear();
49 bytesRead = inChannel.read(readBuf);
50 }
51 } catch (Exception e) {
52 e.printStackTrace();
53 }
54 }
55
56 public static void main(String args[]) {
57 writeFile();
58 readFile();
59 }
60 }
运行结果
NIO之selector
Selector表示选择器或者多路复用器,它主要的功能为轮询检查多个通道状态,判断通道注册的事件是否发生,也就是说判断通道是否可读或可写。然后根据发生事件的类型对这个通道做出对应的响应。由此可见,一个Selector完全可以用来管理多个连接,由此大大提高了系统的性能。
创建Selector
Selector的创建非常简单,只需要调用selector的静态方法open就可以创建一个selector。一旦selector被创建出来,接下来就需要把感兴趣的Channel的时间注册给Selector了。
注册Channel的事件到Selector
由于Selector需要轮询多个Channel,因此注册的Channel必须是阻塞的,在注册前需要使用代码把Channel注册为非阻塞,然后再注册感兴趣的事件。
1 channel.configureBlocking(false);
2 SelectionKey key = channel.register(selector, SelectionKey.OP_WRITE);
需要注意的是,只有继承了SelectableChannel或AbstractSelectableChannel的类才有configureBlocking这个方法,常用的SocketChannel和ServerSockerChannel都是继承自AbstractSelectableChannel的,因此他们都有configureBlocking方法,可以注册到Selector上。
register方法用来向给定的选择器注册此通道,并返回一个选择键。第一个参数表示要向其注册此通道的选择器;第二个参数表示的感兴趣的键的可用操作集。
SelectionKey
1) SelectionKey.OP_CONNECT 表示connect事件,Channel建立了与服务器的连接
2) SelectionKey.OP_ACCEPT 表示accept事件,Channel准备好了接受新的连接
3) SelectionKey.OP_READ 表示read事件,通道中有数据可以读
4) SelectionKey.OP_WRITE 表示write事件,可以向通道写数据
向Selector注册Channel的时候,register方法会返回一个SelectionKey的对象,这个对象表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。
interset集合表示Selector对这个通道感兴趣的事件的集合,通常会使用为操作来判断Selector对哪些事感兴趣。
ready集合是通道已经准备就绪的操作的集合。在一次选择selection之后,会首先访问这个ready集合,可以使用位操作来检查某一个事件是否就绪。
附加对象,可以把一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。
使用Selector选择Channel
如果对Selector注册了一个或多个渠道,那么就可以使用select方法来获取哪些准备就绪的通道,如果对读事件感兴趣,那么会返回读就绪的通道;如果对写事件感兴趣,那么会获取写通道,select方法主要有下面三个重要方法重载的实现:
select:选择一组键,其相应的通道已为I/O操作准备就绪。此方法执行处于阻塞模式的选择操作。仅在至少选择一个通道、调用此选择器的wakeup方法,或者当前线程已中断,以先到者为准,后此方法才返回。
select(long timeout):此方法执行处于阻塞模式的选择操作。仅在至少选择一个通道、调用此选择器的wakeup方法、当前的线程已中断,或者给定的超时期满,后方法才返回。
int selectNow():此方法执行非阻塞的选择操作,如果自从前一次选择操作后,没有通道变成可选择的,那么此方法直接返回0;一旦select()方法的返回值表示有通道就绪了,此时就可以通过selector的selectedKeys方法来获取那些就绪的通道。
1 public class Server {
2 public static void main(String args[]) {
3 Selector selector = null;
4 try {
5 ServerSocketChannel ssc = ServerSocketChannel.open();
6 ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8080));
7 //设置为非阻塞模型
8 ssc.configureBlocking(false);
9 selector = Selector.open();
10 //注册channel,同时指定感兴趣的事件是Accept
11 ssc.register(selector, SelectionKey.OP_ACCEPT);
12 ByteBuffer readBuff = ByteBuffer.allocate(1024);
13 ByteBuffer writeBuff = ByteBuffer.allocate(1024);
14 writeBuff.put("Helo client".getBytes());
15 writeBuff.flip();
16 while(true) {
17 int readyNum = selector.select();
18 if(readyNum == 0) {
19 continue;
20 }
21 Set<SelectionKey> keys = selector.selectedKeys();
22 Iterator<SelectionKey> it = keys.iterator();
23 //遍历就绪的通道
24 while(it.hasNext()) {
25 SelectionKey key = it.next();
26 if(key.isAcceptable()) {
27 //创建新的链接,并且把新的链接注册到selector上,且直对读操作感兴趣
28 SocketChannel socketChannel = ssc.accept();
29 socketChannel.configureBlocking(false);
30 socketChannel.register(selector, SelectionKey.OP_READ);
31 } else if(key.isReadable()) {
32 SocketChannel socketChannel = (SocketChannel) key.channel();
33 readBuff.clear();
34 socketChannel.read(readBuff);
35 readBuff.flip();
36 System.out.println("Server receive:" + new String(readBuff.array()));
37 //一旦读完数据后,只对写感兴趣,因为要给client发送数据
38 key.interestOps(SelectionKey.OP_WRITE);
39 } else if(key.isWritable()) {
40 writeBuff.rewind();
41 SocketChannel socketChannel = (SocketChannel) key.channel();
42 socketChannel.write(writeBuff);
43 //发送完以后又指对读事件感兴趣
44 key.interestOps(SelectionKey.OP_READ);
45 }
46 //处理完事件之后需要从就绪的keys中删除
47 it.remove();
48 }
49 }
50 } catch (Exception e) {
51 e.printStackTrace();
52 } finally {
53 if(selector != null) {
54 try {
55 selector.close();
56 } catch (Exception e) {}
57 }
58 }
59 }
60 }
1 public class Client {
2 public static void main(String args[]) {
3 SocketChannel channel = null;
4 try {
5 channel = SocketChannel.open();
6 channel.connect(new InetSocketAddress("127.0.0.1", 8080));
7 ByteBuffer writeBuf = ByteBuffer.allocate(1024);
8 ByteBuffer readBuf = ByteBuffer.allocate(1024);
9 writeBuf.put("Hello Server".getBytes());
10 writeBuf.flip();
11 while(true) {
12 writeBuf.rewind();
13 channel.write(writeBuf);
14 readBuf.clear();
15 channel.read(readBuf);
16 System.out.println("Client received:" + new String(readBuf.array()));
17 }
18 } catch (Exception e) {
19 e.printStackTrace();
20 } finally {
21 if(channel != null) {
22 try {
23 channel.close();
24 } catch (Exception e) {
25 e.printStackTrace();
26 }
27 }
28 }
29 }
30 }