[网络]NIO学习笔记

来源:互联网 发布:三杰烧录软件视频教程 编辑:程序博客网 时间:2024/05/19 15:44

      最近为了一个开源项目,重新学习了下NIO的知识。在此分享下我的学习心得。

一、为什么引入NIO

       NIOnew IO的简称,从1.4版本后引入。传统的套接字(Socket)对于小规模的系统可以很好的运行,但是如果要同时处理上千个客户机时,服务器就需要产生上千个线程来等待用户的输入,这样就产生了严重的资源浪费,那么如何解决这个问题呢?NIO的提出正是解决了这个问题。

     NIO采用轮询的方式来查找哪个客户机需要服务,从而提供服务,这也正是NIO中的SelectorChannel抽象的关键点。一个Channel实例代表了一个“可轮询的”I/O目标。NIO另外一个重要特性是Buffer类。

 

二、信道(Channel)与套接字(Socket)的不同点

      信道需要通过调用静态工厂方法来获得实例:

SocketChannel sc = SocketChannel.open();ServerSocketChannel ssc = ServerSocketChannel.open();


 

      Channel使用的不是流,而是缓冲区来发送和读取数据。Buffer类或其任何子类的实例都可以看作是一个定长度的JAVA基本数据类型元素序列。与流不同,缓冲区具有固定的、有限的容量。还有一点需要注意,Buffer实例化是通过调用allocate()方法。

                              ByteBuffer buffer = ByteBuffer.allocate(256);//根据实际情况来定缓冲区大小

     或者通过包装一个已有的数组来创建:

                            ByteBuffer buffer = ByteBuffer.wrap(byteArray);

 

       NIO的强大功能部分来自于channel的非阻塞特性。Socket的某些操作可能会无限期的阻塞。例如,对accept()方法的调用可能会因为等待一个客户端连接而阻塞;对read()方法的调用可能会因为没有数据可读而阻塞,直到连接的另一端传来新的数据。NIOchannel抽象的一个特征就是可以通过配置它的阻塞行为,以实现非阻塞的信道。

                                cc.configureBlocking(false);

      在非阻塞式信道上调用一个方法总是会立即返回。这种调用的返回值指示了所请求的操作完成的程度。例如,在一个非阻塞式ServerSockerChannel上调用accept()方法,如果有连接请求在等待,则返回客户端SocketChannel,否则返回null

 

三、Selector介绍

      Selector类可用于避免使用非阻塞式客户端中很浪费资源的“忙等”方法。例如,考虑一个即时消息发送器。可能有上千个客户端同时连接到了服务器,但在任何时刻都只有非常少量的消息需要读取和分发。这就需要一种方法阻塞等待,直到至少有一个信道可以进行I/O操作,并指出是哪个信道。NIOSelector就实现了这个功能。一个Selector实例可以同时检查一组信道的I/O状态。

     那么如何使用Selector来监听呢?首先需要创建一个Selector实例(使用静态工厂方法open())并将其注册(一个信道可以注册多个Selector实例)到想要监听的信道上。如下:

                  Selector selector = Selector.open();DatagramChannel channel = DatagramChannel.open();channel.configureBlocking(false);channel.socket().bind(new InetSocketAddress(servPort));//绑定端口                     channel.register(selector, SelectionKey.OP_READ);//注册

最后,调用选择器上的select方法。

int num = selector.select();//获取

获取可进行I/O操作的信道数量。如果在一个单独的线程中,通过调用sleect()方法就能检查多个信道是否准备I/O操作。如果经过一段时间后任然没有信道准备好,则返回0,并允许程序继续执行其它任务。

那么如何在信道上对“感兴趣的”I/O操作进行监听呢?SelectorChannel之间的关联由一个SelectionKey实例表示。SelectionKey维护了一个信道上感兴趣的操作类型信息,并将这些信息存放在一个int型的位图(bitmap)中,该int型数据的每一位都有相应的含义。

SelectionKey类中的常量定义了信道上可能感兴趣的操作类型,每个这种常量都是只有一位设置为1的位掩码。在API文档中,我们查知:

OP_ACCEPT      16             10000

OP_CONNECT   8               01000

OP_WRITE          4               00100

OP_READ          1                00001

通过对OP_ACCEPT,OP_CONNECT,OP_READ以及OP_WRITE中适当的常量进行按位OR,我们可以构造一个位向量来指定一组操作。例如,一个包含了读和写的操作集合可由表达式(OP_READ|OP_WRITE)来指定。

通过Channel类中的validOps()方法,我们可以知道该信道可以监听哪些I/O操作。如果定义了OP_READ|OP_WRITE,则validOps()方法的返回值为500101);定义了上述四种操作,则其值应该为2911101)。

下面笔者将实际使用兴趣集中常见的错误进行下汇总(一般使用OP_WRITEOP_READ是不会发生错误的):

Error1:

Exception in thread "main" java.lang.IllegalArgumentException

at java.nio.channels.spi.AbstractSelectableChannel.register(Unknown Source)

at java.nio.channels.SelectableChannel.register(Unknown Source)

at UdpServer.main(UdpServer.java:20)

 

错误使用OP_CONNECT,其正确使用方法应该是先建立连接。正确的一个例子如下(参考自:http://blog.csdn.net/zhouhl_cn/article/details/6582420

TCPNIO实现网络上很多,笔者在网络发现很少有关于UDPNIO实现。下面给出笔者亲测的NIO版本的UDP实现。

 

/** * 服务器的实现 */import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.CharBuffer;import java.nio.channels.DatagramChannel;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.charset.Charset;import java.nio.charset.CharsetDecoder;import java.util.Iterator;public class UdpServer {public static void main(String args[]) throws IOException {int servPort = 999;Selector selector = Selector.open();DatagramChannel channel = DatagramChannel.open();channel.configureBlocking(false);channel.socket().bind(new InetSocketAddress(servPort));channel.register(selector, SelectionKey.OP_READ);//channel.register(selector, 1);与上句子同效果while (true) {int num = selector.select();if (num == 0) {continue;}Iterator<SelectionKey> Keys = selector.selectedKeys().iterator();while (Keys.hasNext()) {SelectionKey k = Keys.next();if ((k.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {DatagramChannel cc = (DatagramChannel) k.channel();// 非阻塞cc.configureBlocking(false);ByteBuffer buffer = ByteBuffer.allocateDirect(255);// 接收数据并读到buffer中buffer.clear();channel.receive( buffer ) ;buffer.flip();byte b[] = new byte[buffer.remaining()];for (int i = 0; i < buffer.remaining(); i++) {b[i] = buffer.get(i);}Charset charset = Charset.forName("UTF-8");CharsetDecoder decoder = charset.newDecoder();CharBuffer charBuffer = decoder.decode(buffer);System.out.println("The imformation recevied:"+charBuffer.toString());Keys.remove(); //一定要remove}}}}}


 

/** * 客户端的实现 */import java.io.IOException;import java.net.InetSocketAddress;import java.net.SocketAddress;import java.nio.ByteBuffer;import java.nio.channels.DatagramChannel;import java.util.Scanner;public class UdpClient {@SuppressWarnings("resource")public static void main(String agrs[]) throws IOException {String s = null;while ((s =  new Scanner(System.in).nextLine()) != null) {DatagramChannel dc = null;dc = DatagramChannel.open();SocketAddress address = new InetSocketAddress("localhost", 999);ByteBuffer bb = ByteBuffer.allocate(255);byte[] b = new byte[130];b = s.getBytes();bb.clear();bb.put(b);bb.flip();dc.send(bb, address);}}}


      如对NIO有兴趣的朋友,可以参考下《JAVA TCP/IP Socket编程》(原书第二版),笔者也有部分地方是参考该本书的,欢迎各位同行的批评指正!

0 0
原创粉丝点击