现在的位置: 首页 > 综合 > 正文

[网络]NIO学习笔记

2018年02月15日 ⁄ 综合 ⁄ 共 4938字 ⁄ 字号 评论关闭

      最近为了一个开源项目,重新学习了下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编程》(原书第二版),笔者也有部分地方是参考该本书的,欢迎各位同行的批评指正!

抱歉!评论已关闭.