上篇简单介绍了NIO和IO的区别,以及缓冲区,通道和选择器的概念,这篇就详细介绍下这三个及具体的实现。
一、缓冲区
缓冲区时对Java原生数组的对象封装,它除了包含其数组外,还带有四个描述缓冲区特征的属性以及一组用来操作缓冲区的API。缓冲区的根类是Buffer,其重要的子类包括ByteBuffer,MappedByteBuffer、CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、FloatBuffer。从其名称可以看出这些类分别对应了存储不同类型数据的缓冲区。
1 四个属性
容量(Capacity):缓冲区能够容纳的数据元素的最大数量。初始设定后不能更改。
上界(Limit):缓冲区中第一个不能被读或写的元素位置。或者说,缓冲区内现存的元素的上界。
位置(Position):缓冲区内下一个将要被读或写的元素位置。在进行读写缓冲区时,位置会自动更新。
标记(Mark):一个备忘位置。初始时为“未定义”,调用mark时mark=position,调用reset时position=mark。
四个属性满足关系:mark<=position<=limit<=capacity
mark()是为了暂时性的记住一个position的位置,以便在恰当的时候调用reset()方法让position恢复到此位置。
2 写入和读取缓冲区的数据
所有Buffer都有get和put函数,用来写入和读取数据。注意,每put一个元素,position自动加1;而每get一个元素,position也自动加1.能够写入或读取的元素位置,必定位于position和limit之间。
CharBuffer buffer=CharBuffer.allocate(10);
//1
buffer.put("james");
buffer.clear();
System.out.println(buffer.get());
//2
buffer.put('j').put('a').put('m').put('e').put('s');
buffer.clear();
//3
char [] chars={'a','b','c'};
buffer.put(chars);
buffer.clear();
System.out.println(buffer.get());
System.out.println(buffer.get());
System.out.println(buffer.get());
3 remaining和hasRemaining
remaining()会返回缓冲区中目前存储的元素个数,在使用参数为数组的get方法中,提前知道缓冲区存储的元素个数是非常有用的。
事实上,由于缓冲区的读或写模式并不清晰,因此实际上remaining()返回的仅仅是limit-position的值。
而hasRemaining()的含义是查询缓冲区中是否还有元素,这个方法的好处是它是线程安全的。
4 Flip翻转
在从缓冲区读取数据时,get方法会从position的位置开始,依次读取数据,每次读取后position会自动加1,直至position到达limit处为止。因此,在写入数据后,开始读数据前,需要设置position和limit的值,以便get方法能够正确读入前面写入的元素。
这个设置是让limit=position,然后position=0,为了方便,Buffer类提供了一个方法flip(),来完成这个设置。
5 compact压缩
压缩compact()方法是为了将读取了一部分的buffer,其剩下的部分整体挪动到buffer的头部(即从0开始的一段位置),便于后续的写入或者读取。其含义为limit=limit-position,position=0。
6 duplicate复制
复制缓冲区,两个缓冲区对象实际上指向了同一个内部数组,但分别管理各自的属性。
7 slice缓冲区切片
缓冲区切片,将一个大缓冲区的一部分切出来,作为一个单独的缓冲区,但是他们共用同一个内部数组。切片从原缓冲区的position位置开始,至limit为止。原缓冲区和切片各自拥有自己的属性。
8 字节缓冲区
ByteBuffer是最重要的缓冲区,应用最广,使用频率最高。因为ByteBuffer中直接存储字节,所以在不同的操作系统、硬件平台、文件系统和JDK之间传递数据时不涉及编码、解码和乱码问题,也不涉及Big-Endian和Little-Endian大小端问题,所以它是使用最为便利的一种缓冲区。
ByteBuffer中存储的是字节,有时为了方便,可以使用asCharBuffer()等方法将ByteBuffer转换为存储某基本类型的视图,例如CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer和FloatBuffer。
如此转换后,这两个缓冲区共享同一个内部数组,但是对数组内元素的视角不同。以CharBuffer和ByteBuffer为例,ByteBuffer将其视为一个个的字节(1个字节),而CharBuffer则将其视为一个个的字符(2个字节)。若此ByteBuffer的capacity为12,则对应的CharBuffer的capacity为12/2=6。与duplicate创建的复制缓冲区类似,该CharBuffer和ByteBuffer也各自管理自己的缓冲区属性。
二、通道
1.通道概念
在JDK1.8官方文档中,是这样描述的:一个通道代表了一个通向某实体的连接,这些实体可能是一个硬件设备,一个文件,一个网络套接字,或者是一个程序组件,它们具有执行某种或多种独立的IO操作的能力,例如读或写。
在《Java NIO》一书中(这本书成书于JDK1.4时代)是这样描述的:一个通道是用来在字节缓冲区和另一方实体之间有效传输数据的导管
解释:1)通道是一种高效传输数据的管道,2)通道的一端(接收端或发送端)必须是字节缓冲区,3)另一端则是拥有IO能力的实体,4)通道本身不能存储数据,5)且往往通过流或套接字来创建,6)一旦创建,则通道与之形成一一对应的依赖关系。
Java的传统IO只有阻塞模式,但Java NIO却提供了阻塞和非阻塞两种IO模式,这两种模式就是通过通道来体现的。
2 通道种类
通道主要分为文件通道和Socket通道,由于文件通道只能处于阻塞模式,较为简单,因此先介绍文件通道。
2.1文件通道
创建文件通道最常用的三个类是FileInputStream、FileOutputStream和RandomAccessFile,它们均提供了一个getChannel()方法,用来获取与之关联的通道。
对于文件通道来说,FileInputStream创建的通道只能读,FileOutputStream创建的通道只能写,而RandomAccessFile可以创建同时具有读写功能的通道(使用“rw”参数创建)。
当创建了一个文件通道后,文件通道和文件流对象(FileInputStream、FileOutputStream和RandomAccessFile)共享此文件的position。文件流对象和文件通道的大部分读写操作(直接位置的读写操作不会造成position的位移)均会造成position的自动位移,这个位移对于两类对象来说是共享的。代码例子如下:
private static void testFilePosition() {
final String filepath = "D:\\tmp\\a.txt";
try {
//create a file with 26 char a~z
FileOutputStream fos = new FileOutputStream(filepath);
StringBuilder sb = new StringBuilder();
for (char c = 'a'; c <= 'z'; c++) {
sb.append(c);
}
fos.write(sb.toString().getBytes());
fos.flush();
fos.close();
//creat FileChannel
RandomAccessFile file = new RandomAccessFile(filepath, "rw");
FileChannel channel = file.getChannel();
System.out.println("file position in FileChannel is :" + channel.position());
file.seek(5);
System.out.println("file position in FileChannel is :" + channel.position());
channel.position(10);
System.out.println("file position in RandomAccessFile is :" + file.getFilePointer());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
使用FileChannel的position(long)方法时,如果参数为负值,则会抛出java.lang.IllegalArgumentException异常;不过可以把position设置到超出文件尾,这样做会把position设置为指定值而不改变文件大小。若在将position设置为超出当前文件大小时实现了一个read( )方法,那么会返回一个文件尾(end-of-file)条件;若此时实现的是一个write( )方法则会引起文件增长以容纳写入的字节,此时会造成文件空洞(file hole),即文件size扩大,但文件中间的一段并无任何内容,代码如下:
private static void testFileHole() {
final String filepath = "D:\\tmp\\filehole.txt";
try {
//create a file with 26 char a~z
FileOutputStream fos = new FileOutputStream(filepath);
StringBuilder sb = new StringBuilder();
for (char c = 'a'; c <= 'z'; c++) {
sb.append(c);
}
fos.write(sb.toString().getBytes());
fos.flush();
fos.close();
//creat FileChannel
RandomAccessFile file = new RandomAccessFile(filepath, "rw");
System.out.println("file length is:"+file.length());
FileChannel channel = file.getChannel();
//wirte a byte at position 100
channel.position(100);
channel.write((ByteBuffer) ByteBuffer.allocate(1).put((byte) 0).flip());
System.out.println("file position in RandomAccessFile is :" + file.getFilePointer());
System.out.println("file length is:"+file.length());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
文件通道的读写也是非常简单的,唯一值得注意的就是通道的读写都要通过ByteBuffer,一个文件拷贝的代码例子如下:
private static void testFileCopy() throws IOException {
RandomAccessFile source = new RandomAccessFile("D:\\tmp\\a.txt", "r");
RandomAccessFile dest = new RandomAccessFile("D:\\tmp\\b.txt", "rw");
FileChannel srcChannel = source.getChannel();
FileChannel destChannel = dest.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(8);
while (srcChannel.read(buffer) != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
destChannel.write(buffer);
}
buffer.clear();
}
srcChannel.close();
destChannel.close();
}
另外,除了仅带有一个参数ByteBuffer的read和write方法外,还有read(ByteBuffer dst, long position)以及write(ByteBuffer src, long position)方法,它们直接对文件的某个位置进行读写,并且不会导致文件position的自动位移。
从JDK1.4之后,Java终于引进了文件锁机制,用来在进程之间进行文件的共享与独占锁定。注意两点,文件锁定是在进程之间进行的,一个进程的多个线程之间,文件锁定无效;第二,锁定分为共享锁与独占锁,但是若操作系统或文件系统不支持,则锁的种类会自动升级。例如若某个操作系统没有共享锁,则Java的共享锁会被自动升级为独占锁,以保证语法的正确性。但这样会带来极大的开销,因此在使用文件锁之前,请仔细研究程序的运行环境,确保不会因为文件锁而带来难以忍受的性能开销。
下面的代码演示了文件锁的使用方法,代码需执行两次,每次使用不同的参数运行,FileLockExample –w(请先运行这个)和FileLockExample –r,其中一个进程获得文件锁以后,写入一个递增的数字至文件中的指定位置;而另一个进程获得文件锁以后从文件中读取那个数字:
public class FileLockExample {
private static String filepath = "D:\\tmp\\filelock.txt";
private static Random rand = new Random();
public static void main(String[] args) {
if (args.length < 1) {
System.out.println("Usage: [-r | -w]");
System.exit(1);
}
boolean isWriter = args[0].equals("-w");
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(filepath, (isWriter) ? "rw" : "r");
FileChannel channel = randomAccessFile.getChannel();
if (isWriter) {
lockAndWrite(channel);
} else {
lockAndRead(channel);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
private static void lockAndWrite(FileChannel channel) {
try {
ByteBuffer buffer = ByteBuffer.allocate(4);
int i=0;
while (true) {
System.out.println("Writer try to lock file...");
FileLock lock = channel.lock(0,4,false);
buffer.putInt(0,i);
buffer.position(0).limit(4);
System.out.println("buffer is :"+buffer);
channel.write(buffer,0);
channel.force(true);
buffer.clear();
System.out.println("Writer write :" + i++);
lock.release();
System.out.println("Sleeping...");
TimeUnit.SECONDS.sleep(rand.nextInt(3));
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void lockAndRead(FileChannel channel) {
try {
ByteBuffer buffer = ByteBuffer.allocate(4);
while (true) {
System.out.println("Reader try to lock file...");
FileLock lock = channel.lock(0,4,true);
buffer.clear();
channel.read(buffer,0);
buffer.flip();
System.out.println("buffer is:"+buffer);
int i = buffer.getInt(0);
System.out.println("Reader read :" + i);
lock.release();
System.out.println("Sleeping...");
TimeUnit.SECONDS.sleep(rand.nextInt(3));
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Writer进程使用channel.lock(0,4,false)方法,作用是锁住一个文件的一段内容,模式是false,意味着这是一个独占锁;而Reader进程使用channel.lock(0,4,true),意味着这是一个共享锁。解锁则都使用release()方法。
值得注意的是,lock()方法是阻塞式的,同时FileLock还提供了非阻塞式的tryLock()方法,用于非阻塞的场合。
2.2 Socket通道
Socket通道实现了与传统Socket类似的功能,其类名、API都与传统Socket非常类似。ServerSocketChannel对应ServerSocket,SocketChannel对应Socket,DatagramChannel对应DatagramSocket。
除此之外,Socket通道还同时支持“阻塞”模式与“非阻塞”模式。传统Socket仅支持“阻塞”模式,其用于连接双方套接字的accept()和connect()方法都是阻塞的;而Socket通道除了默认为阻塞模式外,同时还提供了一组非阻塞的连接方法。
首先来看一下使用Socket通道来进行“阻塞”模式的连接,其代码与传统Socket非常类似,服务端如下:
public class BlockingChannelServer {
public static void main(String[] args) {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(1234));
SocketChannel sc = ssc.accept();
System.out.println("accept connection from:" + sc.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (sc.read(buffer) != -1) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
System.out.println(new String (bytes));
buffer.clear();
}
sc.close();
ssc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端如下:
public class BlockingChannelClient {
public static void main(String[] args) {
try {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("127.0.0.1", 1234));
ByteBuffer buffer = ByteBuffer.allocate(1024);
writeString(buffer, sc,"hello");
writeString(buffer, sc,"world");
writeString(buffer, sc,"exit");
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void writeString(ByteBuffer buffer, SocketChannel sc,String str) {
buffer.clear();
buffer.put(str.getBytes()).flip();
try {
sc.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
以上代码中,所有原Socket的连接相关操作,如bind、accept、connect都使用相应的Socket通道完成,而原来使用输入输出流的读写操作也使用Socket通道来完成。被传输的数据则都是使用ByteBuffer作为输入输出的中转地。
Socket通道实现了与传统Socket类似的功能,其类名、API都与传统Socket非常类似。ServerSocketChannel对应ServerSocket,SocketChannel对应Socket,DatagramChannel对应DatagramSocket。
除此之外,Socket通道还同时支持“阻塞”模式与“非阻塞”模式。传统Socket仅支持“阻塞”模式,其用于连接双方套接字的accept()和connect()方法都是阻塞的;而Socket通道除了默认为阻塞模式外,同时还提供了一组非阻塞的连接方法。
首先来看一下使用Socket通道来进行“阻塞”模式的连接,其代码与传统Socket非常类似,服务端如下:
public class BlockingChannelServer {
public static void main(String[] args) {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(1234));
SocketChannel sc = ssc.accept();
System.out.println("accept connection from:" + sc.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (sc.read(buffer) != -1) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
System.out.println(new String (bytes));
buffer.clear();
}
sc.close();
ssc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端如下:
public class BlockingChannelClient {
public static void main(String[] args) {
try {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("127.0.0.1", 1234));
ByteBuffer buffer = ByteBuffer.allocate(1024);
writeString(buffer, sc,"hello");
writeString(buffer, sc,"world");
writeString(buffer, sc,"exit");
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void writeString(ByteBuffer buffer, SocketChannel sc,String str) {
buffer.clear();
buffer.put(str.getBytes()).flip();
try {
sc.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
以上代码中,所有原Socket的连接相关操作,如bind、accept、connect都使用相应的Socket通道完成,而原来使用输入输出流的读写操作也使用Socket通道来完成。被传输的数据则都是使用ByteBuffer作为输入输出的中转地。
3. Pipe管道
Pipe管道的概念最先应该出现在Unix系统里,用来表示连接不同进程间的一种单向数据通道,很多Unix系统都提供了支持管道的API。Java NIO借用了这个概念,发明了NIO中的Pipe,它是指同一个Java进程内,不同线程间的一种单向数据管道,其sink端通道写入数据,source端通道则读出数据,其间可以保证数据按照写入顺序到达。
<img src="https://pic2.zhimg.com/v2-ba9906eac2aee6be81e867695e220caa_b.jpg" data-rawwidth="209" data-rawheight="445" class="content_image" w
一个典型的Pipe代码如下:
public class SimplePipe {
public static void main(String[] args) throws IOException {
//创建一个管道,并拿到管道两端的channel
Pipe pipe = Pipe.open();
WritableByteChannel writableByteChannel = pipe.sink();
ReadableByteChannel readableByteChannel = pipe.source();
//创建一个线程从sink端写入数据
WorkerThread thread = new WorkerThread(writableByteChannel);
thread.start();
//主线程从source端读取数据,并组成String打印
ByteBuffer buffer = ByteBuffer.allocate(1024);
while ( readableByteChannel.read(buffer) >= 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String str = new String(bytes);
System.out.println(str);
buffer.clear();
}
readableByteChannel.close();
}
private static class WorkerThread extends Thread {
WritableByteChannel channel;
public WorkerThread(WritableByteChannel writableByteChannel) {
this.channel = writableByteChannel;
}
@Override
public void run() {
ByteBuffer buffer = ByteBuffer.allocate(1024);
for (int i = 0; i < 10; i++) {
String str = "pipe sink data " + i;
buffer.put(str.getBytes());
buffer.flip();
try {
channel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
buffer.clear();
}
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4. Scatter和Gather
在使用各种Channel类时,我们已经观察到read和write方法还有一种以ByteBuffer数组为参数的形式,这种形式其实是为了支持通道的Scatter和Gather特性。Scatter的意思是从多个ByteBuffer中依次读取数据到一个Channel中,Gather的意思则是将Channel中的数据依次写入多个ByteBuffer里。在某些特定场合,Scatter/Gather将大大减轻编程的工作量,例如将某些网络包的包头、内容分别读入不同的变量中。下面是一个简单的例子:
public class ScatterAndGatherExample {
public static void main(String[] args) throws UnsupportedEncodingException {
ByteBuffer buffer1 = ByteBuffer.allocate(5);
buffer1.put("hello".getBytes("GBK")).flip();
ByteBuffer buffer2 = ByteBuffer.allocate(5);
buffer2.put("world".getBytes("GBK")).flip();
ByteBuffer[] buffers = {buffer1, buffer2};
try {
//gather example
RandomAccessFile file = new RandomAccessFile("d:\\tmp\\scatter.txt", "rw");
FileChannel channel = file.getChannel();
channel.write(buffers);
channel.force(false);
channel.close();
showFileContent("d:\\tmp\\scatter.txt");
//scatter example
buffer1.clear();
buffer2.clear();
file = new RandomAccessFile("d:\\tmp\\scatter.txt", "r");
channel = file.getChannel();
channel.read(buffers);
String str1 = getBufferContent(buffer1);
String str2 = getBufferContent(buffer2);
System.out.println("buffer1 :" + str1);
System.out.println("buffer2 :" + str2);
channel.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private static String getBufferContent(ByteBuffer buffer) throws UnsupportedEncodingException {
buffer.flip();
System.out.println(buffer);
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
return new String(bytes,"GBK");
}
private static void showFileContent(String filepath) {
try {
FileInputStream fis = new FileInputStream(filepath);
byte[] bytes = new byte[1024];
int len = 0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = fis.read(bytes)) != -1) {
baos.write(bytes, 0, len);
}
String str = baos.toString("GBK");
System.out.println("file content:");
System.out.println(str);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意,例子中为了确保5个字符被转换成5个字节,特地指定了字符集为GBK。
三、选择器
1
在理解了Buffer和Channel之后,终于来到了最终的解决方案面前,那就是使用Selector来实现单线程控制多路非阻塞IO。Selector是如此重要,可以说它就是NIO异步IO的核心控制器。Selector需要其他两种对象配合使用,即SelectionKey和SelectableChannel,它们之间的关系如下图所示:
SelectableChannel是一类可以与Selector进行配合的通道,例如Socket相关通道以及Pipe产生的通道都属于SelectableChannel。这类通道可以将自己感兴趣的操作(例如read、write、accept和connect)注册到一个Selector上,并在Selector的控制下进行IO相关操作。 Selector是一个控制器,它负责管理已注册的多个SelectableChannel,当这些通道的某些状态改变时,Selector会被唤醒(从select()方法的阻塞中),并对所有就绪的通道进行轮询操作。 SelectionKey是一个用来记录SelectableChannel和Selector之间关系的对象,它由SelectableChannel的register()方法返回,并存储在Selector的多个集合中。它不仅记录了两个对象的引用,还包含了SelectableChannel感兴趣的操作,即OP_READ,OP_WRITE,OP_ACCEPT和OP_CONNECT。
1.1 register方法
在展示例子代码之前,必须对一些概念和操作进行简要的介绍。首先是SelectableChannel的register方法,它的正式定义为:
SelectionKey register(Selector sel, int ops)
第一个参数指明要注册的Selector,第二个参数指明本通道感兴趣的操作,此参数的取值可以是SelectionKey.OP_ACCEPT等四个,以及它们的逻辑值,例如SelectionKey.OP_READ & SelectionKey.OP_WRITE。方法的返回值是一个SelectionKey,这个对象会被自动加入Selector的keys集合,因此不必特意保留这个SelectionKey的对象引用,需要时可以使用Selector的keys()方法得到所有的SelectionKey对象引用。 注册完成后,该通道就与Selector保持关联了。当通道的状态改变时,其改变会自动被Selector感知,并在Selector的三个集合中反应出来。
1.2 Selector的三个集合
如上图所示,Selector对象会维持三个SelectionKey集合,分别是keys集合,存储了所有与Selector关联的SelectionKey对象;selectedKeys集合,存储了在一次select()方法调用后,所有状态改变的通道关联的SelectionKey对象;cancelledKeys集合,存储了一轮select()方法调用过程中,所有被取消但还未从keys中删除的SelectionKey对象。 其中最值得关注的是selectedKeys集合,它使用Selector对象的selectedKeys()方法获得,并通常会进行轮询处理。
1.3 select方法
Selector类的select()方法是一个阻塞方法,它有两种形式:
int select()
int select(long timeout)
不带参数的方法会一直阻塞,直到至少有一个注册的通道状态改变,才会被唤醒;带有timeout参数的方法会一直阻塞,直到时间耗尽,或者有通道的状态改变。
1.4 轮询处理
在一次select()方法返回后,应对selectedKeys集合中的所有SelectionKey对象进行轮询操作,并在操作完成后手动将SelectionKey对象从selectedKeys集合中删除。
2. Selector代码实例
<img src="https://pic1.zhimg.com/v2-115158c58f49b2cec7af24b14acefc12_b.jpg" data-rawwidth="3025" data-rawheight="1902" class="origin_image zh-lightbox-thumb" width="3025" data-original="https://pic1.zhimg.com/v2-115158c58f49b2cec7af24b14acef
服务端代码:
public class SelectorServer {
private static final int PORT = 1234;
private static ByteBuffer buffer = ByteBuffer.allocate(1024);
public static void main(String[] args) {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(PORT));
ssc.configureBlocking(false);
//1.register()
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("REGISTER CHANNEL , CHANNEL NUMBER IS:" + selector.keys().size());
while (true) {
//2.select()
int n = selector.select();
if (n == 0) {
continue;
}
//3.轮询SelectionKey
Iterator<SelectionKey> iterator = (Iterator) selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//如果满足Acceptable条件,则必定是一个ServerSocketChannel
if (key.isAcceptable()) {
ServerSocketChannel sscTemp = (ServerSocketChannel) key.channel();
//得到一个连接好的SocketChannel,并把它注册到Selector上,兴趣操作为READ
SocketChannel socketChannel = sscTemp.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("REGISTER CHANNEL , CHANNEL NUMBER IS:" + selector.keys().size());
}
//如果满足Readable条件,则必定是一个SocketChannel
if (key.isReadable()) {
//读取通道中的数据
SocketChannel channel = (SocketChannel) key.channel();
readFromChannel(channel);
}
//4.remove SelectionKey
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void readFromChannel(SocketChannel channel) {
int count ;
buffer.clear();
try {
while ((count = channel.read(buffer)) > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
System.out.println("READ FROM CLIENT:" + new String(bytes));
}
if (count < 0) {
channel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
首先注册了一个ServerSocketChannel,它用来监听1234端口上的连接;当监听到连接时,把连接上的SocketChannel再注册到Selector上,这些SocketChannel注册的是SelectionKey.OP_READ事件;当这些SocketChannel状态变为可读时,读取数据并显示。 客户端代码:
public class SelectorClient {
static class Client extends Thread {
private String name;
private Random random = new Random(47);
Client(String name) {
this.name = name;
}
@Override
public void run() {
try {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(1234));
while (!channel.finishConnect()) {
TimeUnit.MILLISECONDS.sleep(100);
}
ByteBuffer buffer = ByteBuffer.allocate(1024);
for (int i = 0; i < 5; i++) {
TimeUnit.MILLISECONDS.sleep(100 * random.nextInt(10));
String str = "Message from " + name + ", number:" + i;
buffer.put(str.getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
channel.write(buffer);
}
buffer.clear();
}
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(new Client("Client-1"));
executorService.submit(new Client("Client-2"));
executorService.submit(new Client("Client-3"));
executorService.shutdown();
}
}
客户端创建了三个线程,每个线程创建一个SocketChannel通道,并连接到服务器,并向服务器发送5条消息。
3. 小结
Selector是Java NIO的核心概念,以至于一些人直接将NIO称之为Selector-based IO。要学会Selector的使用首先是要明白其相关的多个概念,并多多动手去写。 至此《Java NIO编程实例》系列的三篇就写完了,接下来应该好好介绍一下Netty了,毕竟它才是在具体的Java服务端编程用得最多的框架。Netty克服了NIO中一些概念和设计上的不足之处,提供了更加优雅的解决方案。但是,要学好用好Netty,学习NIO是必经之路,有了NIO的基础,才能真正学好Netty。