快速掌握NIO和BIO的区别

NIO和BIO对比

NIO(non blocking I/O)非阻塞I/O,jdk1.4引入的新I/O,平时接触的文件的I/O操作是BIO,即阻塞I/O

BIO API使用

具体流程:

A.测试accept()方法的阻塞
public void testAccept() throws IOException{
	ServerSocket ss = new ServerSocket();
	ss.bind(new InetSocketAddress(9999));
	Socket sk = ss.accept();
	System.out.println("有连接连入");
}复制代码
JUnit测试,“有连接接入”没有输出,说明accept()方法产生阻塞了。
B.然后添加connect()方法测试的代码:
public void testContect() throws Exception{
	Socket sk = new Socket();
	sk.connect(new InetSocketAddress(
			"127.0.0.1", 9999));
	System.out.println("连接成功");
}复制代码
先运行服务器端方法(testAccept()),再运行客户端方法,发现accept()方法阻塞释放了。另外“连接成功”正确输出。如果不先启动服务器端方法,而直接运行客户端方法,发现先是阻塞了一下,然后JUnit测试抛出异常。
总结:connect()方法会产生阻塞,指定连接成功,阻塞才释放。
accept()方法产生的阻塞,直到服务器获得到连接后,阻塞才释放。
C.测试read()方法的阻塞性
C1. 再次修改testAccept()方法
InputStream  in= sk.getInputStream();
byte bts[] = new byte[1024];
in.read(bts);
System.out.println("读取到了数据:"+new String(bts));复制代码
C2.为了不让连接中断,需要修改testConnect()
whiletrue);复制代码

总结:read()方法会产生阻塞,直到读取到内容后,阻塞才被释放。
D.测试write()方法的阻塞性
D1.修改testAccept()方法
for(int i =1;i<100000;i++){
	out.write("HelloWorld".getBytes());
	System.out.println(i);
}
System.out.println("数据写完了。。。");
}复制代码
先运行服务器端方法,再运行客户端方法;发现i输出值为65513,阻塞了。
        for(int i =1;i<200000;i++){
		out.write("Hello".getBytes());
		System.out.println(i);
	}复制代码
微调代码,输出到131026阻塞了。
总结:write()方法也会产生阻塞,write()一直往出写数据,但是没有任何一方读取数据,直到写出到一定量(我的是655130B,不同电脑可能不同)的时候,产生阻塞。向网卡设备缓冲区中写数据。

NIO 相关API

Channel查看API
ServerSocketChannel, SocketChannel基于NIO的(基于tcp实现的,安全的基于握手机制)
DatagramChannel基于UDP协议,不安全

NIO-Channel API(上)

accept和connect使用

/**ServerSocketChannel.open()创建服务器端对象
 * nio提供两种模式:阻塞模式和非阻塞模式
 * 默认情况下是阻塞模式。
 * 通过ssc.configureBlocking(false)设置为非阻塞模式
 * @throws Exception
 */
@Test
public void testAccept() throws Exception{
	//创建服务器端的服务通道
	ServerSocketChannel ssc = 
			ServerSocketChannel.open();
	//绑定端口号
	ssc.bind(new InetSocketAddress(8888));
	//设置非阻塞模式
	ssc.configureBlocking(false);
	//调用accpet方法获取用户请求的连接通到
	SocketChannel sc = ssc.accept();
	System.out.println("有连接连入");
}复制代码
运行发现,并没有输出“有连接接入”,通道提供阻塞和非阻塞两种模式,默认为阻塞模式。可以在bind port之前添加ssc.configureBlocking(false);设置通道的非阻塞模式。再次运行“有连接接入”便输出了。
public void testConnect() throws Exception{
	SocketChannel sc = SocketChannel.open();
	sc.configureBlocking(false);
	sc.connect(new InetSocketAddress("127.0.0.1", 8888));
	System.out.println("连接成功");
}复制代码
为加sc.configureBlocking(false);之前,运行该方法抛出异常,并没有输出“连接成功”,通道的connect()方法也是阻塞的;使用方法sc.configureBlocking(false);可以将客户端连接通道设置为非阻塞模式。

read()、write()方法测试(过度)

sc.read(ByteBuffer dst)
sc.write(ByteBuffer src)复制代码
由于这两个方法都需要ByteBuffer对象作为参数,所以我们需要先讲ByteBuffer缓冲区。

NIO-ByteBuffer缓冲区API

public class DemoByteBuffer {
	/**ByteBuffer缓冲区类,有三个重要的属性
	 * capacity	10:容量,该缓冲区可以最多保存10个字节
	 * position	0:表示位置
	 * limit 10:限制位(用在获取元素时限制获取的边界)	
	 */
	@Test
	public void testByteBuffer(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		System.out.println();
	}
	/**put(byte bt)向缓存区中添加一个字节
	 *   每调用一次该方法position的值会加一。
	 */
	@Test
	public void testPut(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		byte b1 = 1;
		byte b2 = 2;
		buf.put(b1);
		buf.put(b2);
		buf.putInt(3);
		System.out.println();
	}
	/**get()获取position指定位置的一个字节内容。
	 * 每调用一次该方法,position++;
	 * 如果在调用get()时,position>=limit,
	 * 则抛出异常BufferUnderflowException
	 * 
	 * position(int pt):设置position的值为pt
	 * position():获取当前缓冲区的position属性的值
	 * limit(int):设置限制为的值
	 * limit():获取当前缓冲区的limit属性的值。
	 */
	@Test
	public void testGet(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		byte b1 = 1;
		byte b2 = 2;
		buf.put(b1);//1
		buf.put(b2);//2
		//设置position的值为0
		buf.position(0);
		//设置限制位(不想让用户获取无用的信息)
		buf.limit(2);
		System.out.println(buf.get());//
		System.out.println(buf.get());
		System.out.println(buf.get());
	}
	/**flip()方法:反转缓存区,一般用在添加完数据后。
	 * limit = position;将limit的值设置为当前position的值
       position = 0;再将position的值设置为0
	 */
	@Test
	public void testFlip(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		byte b1 = 1;
		byte b2 = 2;
		buf.put(b1);//1
		buf.put(b2);//2
		/*buf.limit(buf.position());
		buf.position(0);*/
		buf.flip();
	}
	/**clear():"清除缓存区"
	 * 底层源代码:
	 *  position = 0;
        limit = capacity;
       	通过数据覆盖的方式达到清除的目的。
	 */
	@Test
	public void testClear(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		byte b1 = 1;
		byte b2 = 2;
		buf.put(b1);//1
		buf.put(b2);//2
		buf.clear();
		byte b3=33;
		buf.put(b3);
		buf.flip();
		for(int i = 0;i<buf.limit();i++){
			System.out.println(buf.get());
		}
	}
	/**hasRemaining()判断缓冲区中是否还有有效的数据,有返回
	 * true,没有返回false
	 * public final boolean hasRemaining() {
	        return position < limit;
	   }
	 */
	@Test
	public void testClear12(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		byte b1 = 1;
		byte b2 = 2;
		buf.put(b1);//1
		buf.put(b2);//2
		buf.clear();
		byte b3=33;
		buf.put(b3);
		buf.flip();
		/*for(int i = 0;i<buf.limit();i++){
			System.out.println(buf.get());
		}*/
		/*int i =0;
		while(i<buf.limit()){
			System.out.println(buf.get());
			i++;
		}*/
		while(buf.hasRemaining()){
			System.out.println(buf.get());
		}
	}
}复制代码

NIO-Channel API(下)

1、read()方法
修改ChanelDemo类的testAccept方法:
ByteBuffer buf = ByteBuffer.allocate(10);
sc.read(buf);
System.out.println("有数据读入:"+buf.toString());复制代码
testConnect()方法不做任何修改,先运行testAccept()方法,发现在sc.read(buf)行抛出了空指针异常。buf对象不可能为null,所以sc为null.
非阻塞编程最大的问题:不知道是否真正的有客户端接入,所以容易产生空指针;所以需要人为设置阻塞。
将SocketChannel sc = ssc.accept();改为:
while(sc==null){
	sc = ssc.accept();
}复制代码
再次运行testAccept()方法,空指针的问题解决了;然后再运行testConnect()方法,发现连接能够正常建立,但是“有数据读入了。。”并没有输出,说明即使ssc服务通道设置了非阻塞,也没有改变得到的通道sc默认为阻塞模式,所以sc.read(buf)阻塞了。要不想让read()方法阻塞,需要在调用read()之前加sc.configureBlocking(false);这样即使没有读到数据,“有数据读入了。。”也能打印出来。

2、write()方法
修改testContect()方法,追加以下代码:
ByteBuffer buf = ByteBuffer.wrap("HelloWorld".getBytes());
sc.write(buf);复制代码
测试bug,先不运行服务器端方法,直接运行客户端方法testConnect(),输出“连接成功”,但是sc.write(buf)行抛出NotYetConnectException异常。sc为何抛出该异常?非阻塞模式很坑的地方在于不知道连接是否真正的建立。修改testConnect():
ByteBuffer buf = ByteBuffer.wrap("HelloWorld".getBytes());
while(!sc.isConnected()){
	sc.finishConnect();
}
sc.write(buf);复制代码
再次运行testConnect(),之前的异常解决了,但是有出现了新的异常:
java.net.ConnectException: Connection refused: no further information
	at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)复制代码
先启动服务器端(testAccept()),后启动客户端(testConnect())即可。
手写NIO非阻塞模式难度较大,代码不是重点,重要在于引出设计思想。

Selector设计思想

问题的引入


使用BIO编写代码模拟一下
(编写一个服务器端和客户端程序,运行一次服务器程序,运行四次客户端程序模拟四个用户线程)
public class BIOServer {
	public static void main(String[] args) throws Exception {
		ServerSocket ss = new ServerSocket();
		ss.bind(new InetSocketAddress(7777));
		while(true){
			Socket sk = ss.accept();
			new Thread(new ServiceRunner(sk)).start();
		}
	}
}
class ServiceRunner implements Runnable{
	private Socket sk;
	public ServiceRunner(Socket sk){
		this.sk = sk;
	}
	public void run(){
		System.out.println("提供服务的线程id:"+
				Thread.currentThread().getId());
		try {
			Thread.sleep(Integer.MAX_VALUE);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
public class BIOClient {
	public static void main(String[] args) throws Exception {
		Socket sk = new Socket();
		sk.connect(new InetSocketAddress("127.0.0.1", 7777));
		while(true);
	}
}复制代码

服务器启动
负责为客户端提供服务,当前线程的id:9
负责为客户端提供服务,当前线程的id:10
负责为客户端提供服务,当前线程的id:11
负责为客户端提供服务,当前线程的id:12



分析该模式的缺点:
缺点1:每增加一个用户请求,就会创建一个新的线程为之提供服务。当用户请求量特别巨大,线程数量就会随之增大,继而内存的占用增大,所有不适用于高并发、高访问的场景。
缺点2:线程特别多,不仅占用内存开销,也会占用大量的cpu开销,因为cpu要做线程调度。
缺点3:如果一个用户仅仅是连入操作,并且长时间不做其他操作,会产生大量闲置线程。会使cpu做无意义的空转,降低整体性能。
缺点4:这个模型会导致真正需要被处理的线程(用户请求)不能被及时处理。

解决方法

针对缺点3和缺点4,可以将闲置的线程设置为阻塞态,cpu是不会调度阻塞态的线程,避免了cpu的空转。所以引入事件监听机制实现。
Selector多路复用选择器,起到事件监听的作用。
监听哪个用户执行操作,就唤醒对应的线程执行。那么都有哪些事件呢?
事件:1.accept事件、2.connect事件、3.read事件、4.write事件


针对缺点1和缺点2,可以利用非阻塞模型来实现,利用少量线程甚至一个线程来处理多用户请求。但是注意,这个模型是有使用场景的,适用于大量短请求场景。(比如用户访问电商网站),不适合长请求场景(比如下载大文件,这种场景,NIO不见得比BIO好)


扩展知识
惊群现象,隐患:cpu的负载会在短时间之内聚升,最严重的情况时出现短暂卡顿甚至死机。第二个问题就是性能不高。

Selector服务通道API

accept事件

编写服务器端程序:
public class NIOServer {
	public static void main(String[] args) throws Exception {
		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.bind(new InetSocketAddress(6666));
		//设置为非阻塞
		ssc.configureBlocking(false);
		//定义多路复用选择器
		Selector sel = Selector.open();
		//注册accept事件
		ssc.register(sel, SelectionKey.OP_ACCEPT);
		while(true){
			//select()在没有收到相关事件时产生阻塞,直到
			//有事件触发,阻塞才会得以释放
			sel.select();
			//获取所有的请求的事件
			Set<SelectionKey> sks = sel.selectedKeys();
			Iterator<SelectionKey> iter = sks.iterator();
			while(iter.hasNext()){
				SelectionKey sk = iter.next();
				if(sk.isAcceptable()){
					ServerSocketChannel ssc1= 
						(ServerSocketChannel)sk.channel();
					SocketChannel sc = ssc1.accept();
					while(sc==null){
						sc = ssc1.accept();
					}
					sc.configureBlocking(false);
					//为sc注册read和write事件
					//0000 0001  OP_READ
					//0000 0100  OP_WRITE
					//0000 0101  OP_READ和OP_WRITE
					sc.register(sel, SelectionKey.OP_WRITE|SelectionKey.OP_READ);
					System.out.println("提供服务的线程id:"+
						Thread.currentThread().getId());
				}
				if(sk.isWritable()){
				}
				if(sk.isReadable()){
				}
                                iter.remove();
			}
		}
	}
}

编写客户端代码:
public static void main(String[] args) throws Exception {
		SocketChannel sc = SocketChannel.open();
		sc.connect(new InetSocketAddress("127.0.0.1", 6666));
		//sc.configureBlocking(false);
		System.out.println("客户端有连接连入");
while(true);
	}
}复制代码
服务器端启动一次,客户端启动三次,服务器端的控制台输出:
服务器端启动
有客户端连入,负责处理该请求的线程id:1
有客户端连入,负责处理该请求的线程id:1
有客户端连入,负责处理该请求的线程id:1
处理多个请求使用同一个线程。
该设计架构只适用的高并发短请求的场景中。

read事件

修改Server类
if(sk.isReadable()){
	//获取连接对象
	SocketChannel sc = (SocketChannel)sk.channel();
	ByteBuffer buf = ByteBuffer.allocate(10);
	sc.read(buf);
	System.out.println("服务器端读取到:"+new String(buf.array()));
	//0000 0101  sk.interestOps()获取原事件
	//1111 1110   !OP_READ
	//0000 0100  OP_WRITE
	//sc.register(sel, SelectionKey.OP_WRITE);
	sc.register(sel, sk.interestOps()&~SelectionKey.OP_READ);
}复制代码

修改Client类
System.out.println("客户端连入");
ByteBuffer buffer = ByteBuffer.wrap(
"helloworld".getBytes());
sc.write(buffer);
while(true);复制代码

write事件

修改Servet
if(sk.isWritable()){
	//获取SocketChannel
	SocketChannel sc = (SocketChannel)sk.channel();
	ByteBuffer buf = ByteBuffer.wrap("get".getBytes());
	sc.write(buf);
	//去掉写事件
	sc.register(sel, sk.interestOps()&~SelectionKey.OP_WRITE);
}复制代码
修改Client类
public class NIOClient {
	public static void main(String[] args) throws Exception {
		SocketChannel sc = SocketChannel.open();
		sc.configureBlocking(false);
		sc.connect(new InetSocketAddress("127.0.0.1", 6666));
		while(!sc.isConnected()){
			sc.finishConnect();
		}
		System.out.println("客户端有连接连入");
		ByteBuffer buf = ByteBuffer.wrap(
				"helloworld".getBytes());
		sc.write(buf);
		System.out.println("客户端信息已经写出");
		ByteBuffer readBuf = ByteBuffer.allocate(3);
		sc.read(readBuf);
		System.out.println("客户端读到服务器端传递过来的信息:"
		      +new String(readBuf.array()));
		while(true);
	}
}复制代码
public class Client2 {
public static void main(String[] args) throws IOException {
	SocketChannel sc = SocketChannel.open();
	sc.configureBlocking(false);
	sc.connect(new InetSocketAddress("127.0.0.1", 9999));
	//对于客户端,最开始要注册连接监听
	Selector selector = Selector.open();
	sc.register(selector, SelectionKey.OP_CONNECT);
        while(true){
		selector.select();
		Set<SelectionKey> set = selector.selectedKeys();
		Iterator<SelectionKey> iter = set.iterator();
		while(iter.hasNext()){
			SelectionKey sk = iter.next();
			if(sk.isConnectable()){
			}
			if(sk.isWritable()){
			}
			if(sk.isReadable()){
			}
			iter.remove();
		}
	}
}
}复制代码

猜你喜欢

转载自juejin.im/post/5d8206f7e51d45620d2cb991
今日推荐