Java网络编程——非阻塞IO(NIO)

                          非阻塞IO(NIO)



阻塞型IO的局限

       阻塞型IO会阻塞当前线程,所以就需要创建单独的线程专门来处理这个阻塞的任务,然而Java虚拟机会为每个线程分配独立的堆栈空间以及程序计数器等资源。工作线程开得越多,系统的开销就越大,并且相应的线程之间的同步就越复杂。就算为每个阻塞的IO分配了线程,但是这些线程大部分时间都是阻塞在IO上的,Java虚拟机会频繁地进行CPU调度,而调度的就是线程,这样其实大部分的IO线程都阻塞的,不需要调度,CPU调度就会上下文切换,这样是非常浪费效率的。


非阻塞IO的思想

       非阻塞IO充分利用线程的空闲时间,比如在线程本应该阻塞的时候,非阻塞IO就可以用这段阻塞的时候做其他的事情。非阻塞IO采用轮询的方式,不断地查询有没有事件,如果有,就处理,如果没有的话,就轮询下一个事件,如果一个事件都没有,就进入阻塞状态。当有某一个事件发生,就会被唤醒进入轮询。这样,线程就像一直在工作一样,充分利用线程这一重要资源。



Java NIO管道技术

      Java从JDK1.4开始提供NIO。NIO提供了Channel接口,Channel又被称为管道,就是数据传输的通道。SelectableChannel实现了Channel接口,表示是一个可被Select的Channel。在NIO中,通过Channel向Selector注册,然后当SelectableChannel有事件发生时,就会放到Selector的一个事件集合中,在程序中通过轮询的方式去检查事件集合中有没有相应的事件,有的话就取出来处理,NIO是一个事件机制的网络编程框架,但并不是观察者回调的那种事件机制。比如在Socket服务器-客户端编程中,ServerSocketChannel继承自SelectableChannel,可以向Selector注册一个SelectionKey.OP_ACCEPT事件,也就是ServerSocket的accept方法,可以返回一个Socket连接。ByteChannel继承自Channel,表示传输字节数据的管道,SocketChannel继承自SelectableChannel并实现了ByteChannel接口,可以向Selector注册SelectionKey.OP_CONNECT表示连接就绪事件,SelectionKey.OP_READ表示管道中已经有可读的数据了,可以让程序从管道中读取数据的事件,SelectionKey.OP_WRITE写就绪事件,表示已经可以向输出流写数据了。
       SelectableChannel可以向Selector注册读写事件。SelectableChannel可以通过configBlocking(boolean block)来设置是否为阻塞模式,isBlocking()会返回SelectableChannel是不是处于阻塞模式。SelectableChannel提供了两个注册方法,SelectKey register(Selector sel,int ops)方法的第一个参数是指定注册的Selector,第二个参数是注册的事件。SelectKey register(Selector sel,int ops,Object attachment)方法的前两个参数与前一个注册方法一样,最后一个注册参数是一个对象附加到Select上去,当事件发生时,可以取得这个对象。
        在Java NIO管道技术中非常重要的一个类就是Selector,Selector会监控注册到其上的事件是否发生。每个Selector对象包含了三个SelectKey集合,分别是AllKeys集合, 表示向Selector注册了的SelectKey集合,SelectedKeys集合表示已经被Selector捕获的SelectKey集合,CanceledKeys集合是被取消的SelectKey集合。 Selector通过open静态方法来创建一个Selector实例,可以通过调用close()方法来关闭,调用isOpen()方法来判断Selector对象是否已经被关闭了。Set(SelectKey> keys()方法会返回AllKeys集合,int selectNow()方法会返回当前注册了的已经发生了事件的SelectKey的数量。int select()也可以返回已经发生事件的SelectKey的数量,不过,int select()方法是阻塞的,当一个也没有的时候,就会阻塞。Selector的wakeup()方法会唤醒执行Selector的select()方法。
       最后就是SelectKey类,SelectKey表示一个注册监听管理类,每向Selector注册一个SelectableChannel时,就把会一个SelectKey添加到AllKeys集合中,在调用SelectKey的cancel()方法后,这个SelectKey就会添加到CancelKeys集合中,并且不会再监听到相应注册的事件。SelectKey通过int类型的常量来表示自己关心的事件,并用位或(|)符号来组合,或者通过SelectKey的interestOps(int ops)方法来添加组合。前面说过注册时可以添加一个attachment对象,可以通过SelectKey的attach()来获取这个添加的对象,也可以通过attachment(Object attachment)来添加这个对象。

Java缓冲区Buffer

       NIO除了轮询实现异步编程外,另外一个非常大的特性就是提供了很多Buffer缓冲区。在计算机体系结构中,讲到过在CPU和总线通信时有缓冲,用来平衡CPU的高速计算和总线外接设备较慢的数据传输能力。Java NIO中的Buffer是用来流量整形的。如果每来一个数据就向外写一个数据,或者每来一个数据就读一个数据,不断的读与写是非常耗资源的。因此用Buffer就可以用来暂时存储要写出或读取的数据,相Buffer中的数据达到一定量的时候再进行写出或读取。这样可以减少实际牧师读写的次数,并且Buffer可以被重用,以此来减少动态分配和回收内存的次数。JDK为我们实现了ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer等等。
       Buffer是个抽象类,Buffer定义了capacity来表示缓冲区的最大容量,limit表示缓冲区的当前终点,不能对这个缓冲区超过此终点的数据进行读写操作。比如客户端将limit设置成10,将Buffer写给服务器,服务器读到这个Buffer后,读取数据时就只能读10个数据。position用来表示缓冲区下一个读写单元的位置,每次读写一个数据的时候,都会将这个position属性+1,position就相当于一个指针,指下一个要读取的数据在哪个位置。Buffer提供了clear()、flip()、rewind()三个方法,其中clear()方法是把limit设置成capacity,并把position设为0,也就是清空Buffer中的数据,并把下一个要读取的数据设置成首个数据。flip()方法把limit设置成position,并把position设置为0,比如在向一个Buffer写完数据后,就可以调用flip()方法,这样就可以把Buffer发送出去让对方去读了,对方读出来的数据就是自己写入的数据。Buffer的compact()会删除缓冲区从0到position之间的数据,然后把position到limit之间的数据复制到0到limit-position区域去,并且position和limit也作相应的设置。由于Buffer是个抽象类,不能被实例化,因此只能通过静态方法allocate(int apacity)方法来获取一个apacity大小的Buffer实例对象。在读取数据时,调用Buffer的get方法,get方法会从缓冲区读取一个单元的数据,并将position+1,get(int index)会读取index位置上的数据。在写数据的时候,调用Buffer的put方法,会把数据写到position的位置,并把position+1,put(int index)会把数据写到index的位置。

下面是基于NIO服务器-客户端通信例子
package study.io.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

public class TestNIOServer {
	public static void main(String[] args) {
		try {
			ServerSocketChannel serverChannel = ServerSocketChannel.open();
			serverChannel.socket().setReuseAddress(true);// 重用端口
			serverChannel.configureBlocking(false);// 设置为非阻塞模式
			serverChannel.bind(new InetSocketAddress("localhost", 8888));// 绑定地址与端口号
			Selector selector = Selector.open();
			serverChannel.register(selector, SelectionKey.OP_ACCEPT);// 注册接受连接事件
			while (selector.select() > 0) {// 如果有事件
				Set set = selector.selectedKeys();
				Iterator iterator = set.iterator();
				while (iterator.hasNext()) {
					SelectionKey key = iterator.next();
					iterator.remove();
					try {
						if (key.isAcceptable()) {
							serviceAccept(selector, key);
							key.cancel();
						} else if (key.isReadable()) {
							serviceRead(key);// 处理读就绪
						} else if (key.isWritable()) {
							serviceWrite(key);// 处理写就绪
						}
					} catch (Exception ioe) {
						key.cancel();
						key.channel().close();
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private static void serviceAccept(Selector selector, SelectionKey key) {
		try {
			ServerSocketChannel channel = (ServerSocketChannel) key.channel();
			SocketChannel clientChannel = channel.accept();// 取得客户端连接的通道
			clientChannel.configureBlocking(false);// 将客户端连接通道也设置为非阻塞
			Buffer buffer = ByteBuffer.allocate(1024);// 创建一个1024字节的缓冲
			clientChannel.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, buffer);
			System.out.println(clientChannel.getRemoteAddress() + " connected");
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}

	private static Charset charset = Charset.defaultCharset();

	private static void serviceWrite(SelectionKey key) {

	}

	private static void serviceRead(SelectionKey key) {
		ByteBuffer buffer = (ByteBuffer) key.attachment();
		SocketChannel clientChannel = (SocketChannel) key.channel();
		try {
			clientChannel.read(buffer);
			String str = charset.decode(buffer).toString();
			System.out.println("server receive:" + str);
			buffer.clear();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
package study.io.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;

public class TestNIOClient {
	private static ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();

	public static void main(String[] args) {
		try {
			new Thread() {
				@Override
				public void run() {
					Scanner scanner = new Scanner(System.in);
					while (true) {
						String str = scanner.nextLine();
						queue.add(str);
					}
				}
			}.start();
			SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8888));
			socketChannel.configureBlocking(false);
			Selector selector = Selector.open();
			Buffer buffer = ByteBuffer.allocate(1024);
			socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, buffer);
			while (selector.select() > 0) {
				Set set = selector.selectedKeys();
				Iterator iterator = set.iterator();
				while (iterator.hasNext()) {
					SelectionKey key = iterator.next();
					iterator.remove();
					if (key.isReadable()) {
						serviceRead(key);
					} else if (key.isWritable()) {
						serviceWrite(key);
					}
				}
			}
		} catch (IOException e) {
		}
	}

	private static void serviceWrite(SelectionKey key) {
		String str = queue.poll();
		if (str != null) {
			ByteBuffer buffer = (ByteBuffer) key.attachment();
			buffer.clear();
			buffer.put(str.getBytes());
			buffer.flip();
			SocketChannel channel = (SocketChannel) key.channel();
			try {
				channel.write(buffer);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	private static void serviceRead(SelectionKey key) {

	}
}







你可能感兴趣的:(学习)