在上篇文章中对BIO网络编程的相关内容进行了讲解,通过我们一步一步的优化,虽然我们通过多线程解决了并发访问的问题,但是BIO本身的一些特性造成的问题却没有得到解决。
BIO是阻塞IO,我们使用线程来进行IO的调度,我们无法确定io是否就绪,但是每个IO操作都会创建线程,这个时候如果IO未就绪,那么创建的线程也会处于阻塞状态。
在之前讲解NIO基本知识的时候我们提到过NIO通过通道选择器可以实现同时对多个通道的管理,实际上就是通过管理多个IO操作,换句话说是单线程处理多线程并发,有效的防止线程因为IO没有就绪而被挂起。
在使用NIO进行网络编程的时候需要用到的就是通道选择器,所以我们先看一下通道选择器的相关内容。
Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。
//代开ServerSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
通过 ServerSocketChannel.accept() 方法监听新进来的连接。当 accept()方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。 可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null。
Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。可以通过以下2种方式创建SocketChannel:
//打开一个SocketChannel并连接到互联网上的某台服务器。 socketChannel.connect(new InetSocketAddress("localhost",8888)); //一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。 SocketChannel socketChannel = SocketChannel.open();
在前言中提到过,NIO通过通道选择器可以实现同时对多个通道的管理,其实就是同时对多个IO操作的管理,也就是实现了单线程处理多线程并发问题。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此太多的线程会耗费大量的资源,所以使用通道选择器来对多个通道进行管理。
//通过调用Selector.open()方法创建一个Selector,如下: Selector selector = Selector.open();
//为了将Channel和Selector配合使用,必须将channel注册到selector上。通过SelectableChannel.register()方法来实现 // 创建ServerSocketChanner ServerSocketChannel ssc = ServerSocketChannel.open(); // 绑定端口号 ssc.bind(new InetSocketAddress(8888)); // 设置通道非阻塞 ssc.configureBlocking(false); // 创建通道选择器 Selector selector = Selector.open(); // 将通道注册到通道选择器中 要求:通道都必须是非阻塞的 意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式 // 第二个参数是我们需要通道选择器帮我们管理什么事件类型 注册为接受就绪 ssc.register(selector, SelectionKey.OP_ACCEPT);
register()方法的第二个参数代表注册的通道事件类型,一个通道触发一个事件就意味着该事件准备就绪了,总共有四种事件:Connect(连接就绪 ) Accept(接受就绪) Read(有数据可读的通道 读就绪) Write(写就绪)。对于选择器而言,可以针对性的找到(监听)的事件就绪的通道,进行相关的操作。
这四种事件用SelectionKey的四个常量来表示:
可以用“位或”操作符将常量连接起来
对象中包含很多的属性,譬如:
select()方法返回的int值表示有多少通道已经就绪。自上次调用select()方法后有多少通道变成就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只会有一个通道就绪。
通过调用selector的selectedKeys()方法,可以得到就绪通道的集合。遍历集合可以找到自己需要的通道进行相关的操作。
Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> iterator = keys.iterator(); while (iterator.hasNext()){ SelectionKey key = iterator.next(); //处理nio事件 if(key.isAcceptable()){ // 获取通道 ServerSocketChannel channel = (ServerSocketChannel) key.channel(); SocketChannel socketChannel = channel.accept(); // 注册读事件类型 socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } if(key.isReadable()){ SocketChannel socketChannel = (SocketChannel) key.channel(); // 读取数据 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int len = -1; while (true){ byteBuffer.clear(); len = socketChannel.read(byteBuffer); if(len == -1){ break; } byteBuffer.flip(); while (byteBuffer.hasRemaining()){ bos.write(byteBuffer.get()); } } // 打印读取到的数据 System.out.println(bos.toString()); //写数据给客户端 注册事件类型 写事件 socketChannel.register(selector,SelectionKey.OP_WRITE); } if(key.isWritable()){ SocketChannel socketChannel = (SocketChannel) key.channel(); String msg = "你好,我是服务器"; ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length); byteBuffer.put(msg.getBytes()); byteBuffer.flip(); socketChannel.write(byteBuffer); socketChannel.close(); } iterator.remove(); }
import java.io.ByteArrayOutputStream; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set; public class NIOServer { public static void main(String[] args) throws Exception{ // 创建ServerSocketChanner ServerSocketChannel ssc = ServerSocketChannel.open(); // 绑定端口号 ssc.bind(new InetSocketAddress(8888)); // 设置通道非阻塞 ssc.configureBlocking(false); // 创建通道选择器 Selector selector = Selector.open(); // 将通道注册到通道选择器中 要求:通道都必须是非阻塞的 // 第二个参数是我们需要通道选择器帮我们管理什么事件类型 注册为接受就绪 ssc.register(selector, SelectionKey.OP_ACCEPT); // 遍历通道选择器 while (true){ System.out.println("我在8888等你......"); // 返回准备就绪的通道数量 int nums = selector.select(); // 如果数量小于1说明没有通道准备就绪 跳过本次循环 if(nums<1) {continue;} // 获取所有的keys(通道 事件类型) Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> iterator = keys.iterator(); while (iterator.hasNext()){ SelectionKey key = iterator.next(); //处理nio事件 if(key.isAcceptable()){ // 获取通道 ServerSocketChannel channel = (ServerSocketChannel) key.channel(); SocketChannel socketChannel = channel.accept(); // 注册读事件类型 socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } if(key.isReadable()){ SocketChannel socketChannel = (SocketChannel) key.channel(); // 读取数据 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int len = -1; while (true){ byteBuffer.clear(); len = socketChannel.read(byteBuffer); if(len == -1){ break; } byteBuffer.flip(); while (byteBuffer.hasRemaining()){ bos.write(byteBuffer.get()); } } // 打印读取到的数据 System.out.println(bos.toString()); //写数据给客户端 注册事件类型 写事件 socketChannel.register(selector,SelectionKey.OP_WRITE); } if(key.isWritable()){ SocketChannel socketChannel = (SocketChannel) key.channel(); String msg = "你好,我是服务器"; ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length); byteBuffer.put(msg.getBytes()); byteBuffer.flip(); socketChannel.write(byteBuffer); socketChannel.close(); } iterator.remove(); } } } }
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class NIOCleint { public static void main(String[] args) throws IOException { // 创建sc SocketChannel socketChannel = SocketChannel.open(); // 连接服务器 socketChannel.connect(new InetSocketAddress("localhost",8888)); // 写数据给服务器 String msg = "你好,我是客户端"; ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length); byteBuffer.put(msg.getBytes()); byteBuffer.flip(); socketChannel.write(byteBuffer); // 关闭输出流 socketChannel.shutdownOutput(); // 读取数据 ByteBuffer readBuffer = ByteBuffer.allocate(1024); ByteArrayOutputStream bosread = new ByteArrayOutputStream(); int len = -1; while (true){ readBuffer.clear(); len = socketChannel.read(readBuffer); if(len == -1){ break; } readBuffer.flip(); while (readBuffer.hasRemaining()){ bosread.write(readBuffer.get()); } } System.out.println("我收到:"+bosread.toString()); socketChannel.close(); } }
我不能保证每一个地方都是对的,但是可以保证每一句话,每一行代码都是经过推敲和斟酌的。希望每一篇文章背后都是自己追求纯粹技术人生的态度。
永远相信美好的事情即将发生。