快速掌握NIO和BIO的区别

编程入门 行业动态 更新时间:2024-10-09 05:14:56

快速掌握NIO和BIO的<a href=https://www.elefans.com/category/jswz/34/1769972.html style=区别"/>

快速掌握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:限制位(用在获取元素时限制获取的边界)	*/@Testpublic void testByteBuffer(){ByteBuffer buf = ByteBuffer.allocate(10);System.out.println();}/**put(byte bt)向缓存区中添加一个字节*   每调用一次该方法position的值会加一。*/@Testpublic 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属性的值。*/@Testpublic void testGet(){ByteBuffer buf = ByteBuffer.allocate(10);byte b1 = 1;byte b2 = 2;buf.put(b1);//1buf.put(b2);//2//设置position的值为0buf.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*/@Testpublic void testFlip(){ByteBuffer buf = ByteBuffer.allocate(10);byte b1 = 1;byte b2 = 2;buf.put(b1);//1buf.put(b2);//2/*buf.limit(buf.position());buf.position(0);*/buf.flip();}/**clear():"清除缓存区"* 底层源代码:*  position = 0;limit = capacity;通过数据覆盖的方式达到清除的目的。*/@Testpublic void testClear(){ByteBuffer buf = ByteBuffer.allocate(10);byte b1 = 1;byte b2 = 2;buf.put(b1);//1buf.put(b2);//2buf.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;}*/@Testpublic void testClear12(){ByteBuffer buf = ByteBuffer.allocate(10);byte b1 = 1;byte b2 = 2;buf.put(b1);//1buf.put(b2);//2buf.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.ConnectException: Connection refused: no further informationat 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_WRITEsc.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()){//获取SocketChannelSocketChannel 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社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)

更多推荐

快速掌握NIO和BIO的区别

本文发布于:2024-03-12 23:52:21,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1732712.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:区别   快速   NIO   BIO

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!