读Java编程艺术之笔记(非阻塞IO)
本篇重点是非阻塞IO,即java1.4提供的nio包,顺带记录一些其他信息。
在Socket技术中我们利连接时间的付出换来数据传输的可靠性。Java提供一些控制连接时间的技术,以增强其传输效率。例如超时和中断。
为防止无限制等待或为控制等待时间,可调用Socket.setSoTimeout(millies)设置超时时间。另外,在应用构造器Socket(address, port)和ServerSocket(port)时,JVM将先建立连接而后创建socket对象,无参构造器Socket()及ServerSocket()则无需建立连接,然后调用发表在JDK1.4中的connect()方法,可以改善因连接而造成的延误,并能指定连接时间。
try { Socket clientSocket = new Socket(); //无参构造函数 //其他代码 ... clientSocket.connect(address, port, timeout); ... } catch (SocketException e) { e.printStackTree(); }
超时控制有其局限性,在数据发送以及接受过程中,Socket对象无响应,不回答或延误读写的情况,不能控制。此时我们希望中断这个读写操作,可中断的Socket技术,包括在java.nio的API类SocketChannel中。利用SocketChannel创建的对象本身就具有可中断的功能,将抛出InterruptedException,例:
... try{ InetSocketAddress addr = new InetSocketAddress(IPaddress, port); SocketChannel channel = SocketChannel .open(addr); //将通道应用到Scanner Scanner inData = new Scanner(channel); while(true) { if(inData.hasNextLine()) { //得到通道中的数据 String line = inData.nextLine(); ... } else { Thread.sleep(500); } } } catch (InterruptedException e) { e.printStackTrace(); } catch (IOException e) {
e.printStackTrace();
}
具有可中断功能的输出操作如下:
... try{ InetSocketAddress addr = new InetSocketAddress(IPaddress, port); SocketChannel channel = SocketChannel .open(addr); //将通道包装在输出流对象中 OutputStream outStream = Channels.newOutputStream(channel); //刷新方式输出通道中的数据 PrintWriter outData = new PrintWriter(outStream, true); //向服务器发送请求信息 outData.println(requestMessage); ... } catch (InterruptedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } ...
针对网络编程中数据IO,java.nio与java.io相比,应用java.nio包中的API类可提高数据输入、输出的执行速度;纯化Java代码,在传统java.io中,涉及数据输入、输出的底层操作,例如缓冲器的填充和刷新,JVM必须装入本机操作系统的有关代码,完成其IO操作。在java.nio中,涉及缓冲器的操作完全交给操作系统自行,从而提高了Java程序的纯度。
数据流与数据块的比较:面向数据流的IO按照一个个有序的字节处理数据,一次只处理一个字节;面向数据块的IO一次处理整个数据块。这数据块由缓冲器对象类定义,最大数据块容量科大64KB。几乎每种基本数据类型都有其对应的缓冲器类,用来包装不同类型的数据块。数据块的优点是操作速度快;数据流的优点是易于控制和过滤传输的数据,易于编写代码。
通道Channel可看做是数据块进行输入、输出操作的传输带,具有方向性。在网络编程中经常用到的是实现了Channel这个接口的SocketChannel和ServerSocketChannel,它们的一个重要特点是可以运用选择器Selector支持非阻塞输入、输出。
缓冲Buffer是运用通道技术传输数据块的容器。值得注意的是,在应用通道进行数据传输时,必须根据数据类型,来创建匹配的缓冲对象。在java.nio中,Java提供了如下类型的缓冲类:ByteBuffer、CharBufferShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。以上每个缓冲类都是Buffer类的子类。关于缓冲Buffer,缓冲是一块连续的内存区,是nio数据读写的中转地。缓冲有两种工作模式——写模式和读模式,缓冲主要由position,limit,capacity三个变量控制读写过程,在写模式和读模式下缓冲的内部结构如图:
其中capacity,limit和position的含义分别如下表:
参数 |
写模式 |
读模式 |
position |
当前写入的单位数据数量。 |
当前读取的单位数据位置。 |
limit |
代表最多能写多少单位数据和容量是一样的。 |
代表最多能读多少单位数据,和之前写入的单位数据量一致。 |
capacity |
buffer容量 |
buffer容量 |
这三个属性的大小关系为capacity>=limit>=position>=0。缓冲类常见方法:flip()——写模式转换成读模式;rewind()——将position重置为0,一般用户重复读;clear()——清空buffer,准备再次被写入(position变为0,limit变成capacity);compact()——将未读取的数据拷贝到buffer头部;mark(),reset()——mark标记一个位置,reset可重置到该位置。
在利用通道进行数据块块传输中,经常利用java.nio.charset包中提供的Charset类进行数据块的编码和解码操作,以便提高数据块的传输效率和可靠性。除系统预设的Unicode字符集外,Charset还支持如下字符编码定义:US ASCII,ISO-8859-1,UTF-8,UTF-16。Charset是一个抽象类,在应用时必须调用其forName()方法,来返回一个指定字符集编码的对象。并且利用Charset类的encode()以及decode()方法尽心编码和解码操作:Charset forName(String charsetName)——按指定字符集名返回一个Charset对象,ByteBuffer encode(CharBuffer buffer)——对执行CharBuffer对象编码,并返回编码后的ByteBuffer对象,CharBuffer decode(ByteBuffer buffer)——对指定ByteBuffer对象解码,并返回解码后的CharBuffer对象。
阻塞式网络IO的特点:多个线程处理多个连接,每个线程拥有自己的栈空间并占用一些CPU时间,每个线程遇到外部未准备好的时候都会阻塞(accept等待连接到来,会阻塞;recieve/read等待数据,会阻塞)。阻塞会带来大量的线程上下文切换,且大部分的切换都以阻塞告终。
何为非阻塞?
下面有个隐喻:
一辆从A开往B的公共汽车上,路上有很多点可能会有人下车。司机不知道哪些点会有哪些人会下车,对于需要下车的人,如何处理更好?
1.司机过程中定时询问每个乘客是否到达目的地,若有人说到了,那么司机停车,乘客下车。(类似阻塞式)
2.每个人告诉售票员自己的目的地,然后睡觉,司机只和售票员交互,到了某个点由售票员通知乘客下车。 (类似非阻塞)
很显然,每个人要到达某个目的地可以认为是一个线程,司机可以认为是CPU。在阻塞式里面,每个线程需要不断的轮询,上下文切换,以达到找到目的地的结果。而在非阻塞方式里,每个乘客(线程)都在睡觉(休眠),只在真正外部环境准备好了才唤醒,这样的唤醒肯定不会阻塞。
非阻塞的原理
把整个过程切换成小的任务,通过任务间协作完成。由一个专门的线程来处理所有的IO事件,并负责分发。事件驱动机制:事件到的时候触发,而不是同步的去监视事件。线程通讯:线程之间通过wait,notify等方式通讯。保证每次上下文切换都是有意义的。减少无谓的进程切换。
以下是异步IO的结构:
一个简单的通道的例子(服务器端程序)
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.util.Iterator; import java.util.Set; public class SelectoServer { /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { // TODO Auto-generated method stub /* 为第一个用户创建连接 */ //指定端口地址 InetSocketAddress address1 = new InetSocketAddress(10001); //创建ServerSocketChannel ServerSocketChannel channel1 = ServerSocketChannel.open(); //设置非阻塞IO channel1.configureBlocking(false); //绑定端口 channel1.socket().bind(address1); /* 为第二个用户创建连接 */ InetSocketAddress address2 = new InetSocketAddress(10002); ServerSocketChannel channel2 = ServerSocketChannel.open(); channel2.configureBlocking(false); channel2.socket().bind(address2); /* 为第三个用户创建连接 */ InetSocketAddress address3 = new InetSocketAddress(10003); ServerSocketChannel channel3 = ServerSocketChannel.open(); channel3.configureBlocking(false); channel3.socket().bind(address3); //创建选择器 Selector selector = Selector.open(); //指定连接方式 channel1.register(selector, SelectionKey.OP_ACCEPT); channel2.register(selector, SelectionKey.OP_ACCEPT); channel3.register(selector, SelectionKey.OP_ACCEPT); //如果发生任何事件 while (selector.select() > 0) { //得到事件集合 Set keys = selector.selectedKeys(); Iterator iterator = keys.iterator(); while (iterator.hasNext()) { //得到事件源 SelectionKey key = (SelectionKey) iterator.next(); //得到通道 ServerSocketChannel channel = (ServerSocketChannel) key.channel(); //接收通道连接请求 SocketChannel socketChannel = channel.accept(); //调用自定义的请求处理方法 handleClient(socketChannel); //删除处理完的事件 iterator.remove(); } } } private static void handleClient(SocketChannel socketChannel) { // TODO Auto-generated method stub int port = socketChannel.socket().getLocalPort(); System.out.println("Listen to the client address: "+socketChannel.socket().getInetAddress()); System.out.println("Port: "+port); switch (port) { case 10001: writeClient(socketChannel, "服务器响应。使用端口为:"+port); break; case 10002: writeClient(socketChannel, "服务器响应。使用端口为:"+port); break; case 10003: writeClient(socketChannel, "服务器响应。使用端口为:"+port); break; default: writeClient(socketChannel, "服务器响应,非定义端口"+port); break; } } private static void writeClient(SocketChannel socketChannel, String string) { // TODO Auto-generated method stub //指定字符集 Charset charset = Charset.forName("UTF-8"); //自定义缓冲 ByteBuffer buffer = ByteBuffer.allocate(256); // buffer = charset.encode(string); try { socketChannel.write(buffer); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
一个简单的通道的例子(客户端程序)
import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; public class SelectorClient1 { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub try { InetSocketAddress address = new InetSocketAddress("localhost", 10001); Charset charset = Charset.forName("UTF-8"); SocketChannel channel = SocketChannel.open(address); System.out.println("address: " + address); ByteBuffer buffer = ByteBuffer.allocate(256); channel.read(buffer); buffer.flip(); CharBuffer charBuffer = charset.decode(buffer); System.out.println(charBuffer); channel.close(); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } }
三个客户端程序基本相同,这里只列出第一个用户端代码。
其他参考资料:http://www.iteye.com/topic/834447——JAVA NIO 简介