快速掌握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()while(true);

总结: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 set = selector.selectedKeys();
Iterator iter = set.iterator();
while(iter.hasNext()){
SelectionKey sk = iter.next();
if(sk.isConnectable()){
}
if(sk.isWritable()){
}
if(sk.isReadable()){
}
iter.remove();
}
}
}
}

在这里插入图片描述

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();
		}
	}
}
}


(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值