查看完整版本: [原创]介绍及应用JAVA NEW I/O

大饼先生 2006-8-30 13:25

[原创]介绍及应用JAVA NEW I/O

基于旧的I/O的传统网络程序描述如下:
1)打开一个ServerSocket监听端口
2)程序在这里阻塞,直到有连接请求
3)读取请求、发送请求
4)关闭连接
5)重复2~4步

这就是传统的阻塞I/O操作,线程没有有效地利用cup,它们将大多数时间花费在I/O的阻塞上,所以这种线程是低效率的。

而非阻塞I/O以新的方式解决了阻塞问题,可以提高系统的效率。

NIO(new I/O)引入了四个概念:
·线性排列的数据缓冲,可读写缓冲
·一个字节码与Unicode字符的字符集映射
·双向通道Channels
·选择器Selectors


下面先来说说数据缓冲Buffer。
在一个Buffer中,position代表当前Buffer即将处理字节的位置,limit代表第一个不能被读写的字节,capacity代表容量。对初始化的Buffer来说,position为0,limti=capacity。
在NIO中,通过Channel读写Buffer的数据。将一个Buffer中的数据写入Channel时必须调用Buffer对象的flip()方法。比如有以下代码:
ByteBuffer bb = ByteBuffer.allcateDirect(1024);
bb.put("this is a test".getBytes());
这两句的意思是:分配一个容量为1024的直接的ByteBuffer,通过put(byte[] b)方法将字符串存入到这个缓冲区bb里。而存在以下代码:
FileOutputStream out = new FileOutputStream("test.txt");
FileChannel channel = out.getChannel();
这时,当我们想把ByteBuffer对象里的数据写入到test.txt时,可以调用FileChannel的write(ByteBuffer)方法,但在这之前一定要先执行bb.flip(),然后再调用channel.write(bb);
flip()方法将缓冲区的position设为0,将limit设为现有数据的最后值。
如果不先调用flip(),则向test.txt里写入的东西就是空的~。

ByteBuffer是Buffer的子类,都位于java.nio包下面,该包下面还封装了其它基本数据类型的buffer,可以通过ByteBuffer的asCharBuffer()等方法得到其它基本类型的Buffer:

java.nio包如下:
ByteOrder                                  字节顺序的类型安全枚举。
Buffer                         一种用于特定的基本类型数据的容器。
ByteBuffer                 字节缓冲区。  
CharBuffer                 字符缓冲区。
DoubleBuffer                 double 缓冲区。
FloatBuffer                 float 缓冲区。
IntBuffer                 int 缓冲区。
LongBuffer                 long 缓冲区。
MappedByteBuffer         直接字节缓冲区,其内容是文件的内存映射区域。
ShortBuffer                 short 缓冲区。

以下是来自java API里关于java.nio的buffer的描绘:
[quote]
软件包 java.nio 的描述
定义作为数据容器的缓冲区,并提供其他 NIO 包的概述。

NIO API 的集中抽象为:

缓冲区,它们是数据容器;

字符集 及其相关解码器 和编码器,
它们在字节和 Unicode 字符之间进行转换;

各种类型的通道,它们表示到能够执行 IO 操作的
实体的连接;以及选择器 和选择键,它们与
可选择信道 一起定义了多路的、无阻塞的
I/O 设施。

java.nio 包定义了缓冲区类,这些类用于所有 NIO API。java.nio.charset 包中定义了字符集 API,java.nio.channels 包中定义了信道和选择器 API。每个子包都具有自己的服务提供程序接口 (SPI) 子包,SPI 子包的内容可用于扩展平台的默认实现或构造替代实现。

缓冲区
描述

  Buffer 位置,界限和容量;
清除,反转,重绕和标记/重置
  ByteBuffer Get/put,压缩,查看;分配,包装
  MappedByteBuffer   映射到文件的字节缓冲区
  CharBuffer Get/put,压缩;分配,包装
  DoubleBuffer     ' '
  FloatBuffer     ' '
  IntBuffer     ' '
  LongBuffer     ' '
  ShortBuffer     ' '
  ByteOrder 字节顺序的类型安全的枚举

缓冲区 是一个固定数据量的指定基本类型的数据容器。除内容之外,缓冲区还具有位置 和界限,其中位置是要读写的下一个元素的索引,界限是第一个应该读写的元素的索引。基本 Buffer 类定义了这些属性以及清除、反转 和重绕 方法,用以标记 当前位置,以及将当前位置重置 为前一个标记处。

每个非布尔基本类型都有一个缓冲区类。每个类定义了一系列用于将数据移出或移入缓冲区的 get 和 put 方法,用于压缩、复制 和切片 缓冲区的方法,以及用于分配 新缓冲区和将现有数组包装 到缓冲区中的静态方法。

因为字节缓冲区可以用作 I/O 操作的源缓冲区和目标缓冲区,所以可以对它们加以区分。它们还支持其他缓冲区类所没有的几个特性:

可以将字节缓冲区分配为一个直接 缓冲区,在这种情况下,Java 虚拟机将最大限度地直接在缓冲区上执行本机 I/O 操作。

可以通过 mapping 将文件区域直接包装到内存中来创建字节缓冲区,在这种情况下,可以使用 MappedByteBuffer 类中定义的几个其他文件相关的操作。

字节缓冲区提供了对其内容的访问(其内容作为任何非布尔基本类型的异类或同类二进制数据序列),访问要么是以 big-endian 字节顺序进行,要么是以 little-endian 字节顺序进行。

除非另有说明,否则向此包的任何类或接口中的构造方法或方法传递 null 变量,都会抛出 NullPointerException。[/quote]

[[i] 本帖最后由 大饼先生 于 2006-8-30 14:21 编辑 [/i]]

大饼先生 2006-8-30 14:56

字节码与Unicode字符的字符集映射

当需要将CharBuffer的存放的数据编码成ByteBuffer,再将ByteBuffer里存的数据解码成CharBuffer,这时候就要用到java.nio.charset.CharsetEncoder和java.nio.charset.CharsetDecoder了~~
(其实ByteBuffer有个asCharBuffer可以直接将ByteBuffer里的字节数据转换成字符数据的,但Charset类提供各种字符集比如utf-8、iso-8895-1等字符集的映射,可以将CharBuffer里的数据编码成各种格式的编码,然后再由CharsetDecoder解码)!下面是一个完整的程序代码,简单的展示了CharBuffer和ByteBuffer之间编码的转换:

import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
public class CharsetTest {
        private ByteBuffer bb = null;
        private String s = "this is a test";
        public ByteBuffer encoder(){
                CharBuffer cb = CharBuffer.wrap(s);
                Charset cs = Charset.forName("UTF-8");
                CharsetEncoder ce = cs.newEncoder();
                try {
                        bb = ce.encode(cb); //将CharBuffer编码成ByteBuffer
                } catch (CharacterCodingException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                }
                return bb;
        }
        public static void main(String[] args){
                CharsetTest ct = new CharsetTest();
                ByteBuffer bb = ct.encoder();
                Charset c = Charset.forName("UTF-8");
                CharsetDecoder cd = c.newDecoder();
                CharBuffer cb = null;
                try {
                        cb = cd.decode(bb);
                } catch (CharacterCodingException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                }
                System.out.println(cb);
        }
}

值得注意的是编码的转换顺序。encode方法将unicode编码转换为指定编码,而decode方法将指定编码转换为unicode编码。
在Java中,String类型和char类型都是使用的unicode编码

[[i] 本帖最后由 大饼先生 于 2006-8-30 14:58 编辑 [/i]]

网络幽灵 2006-8-30 17:05

看斑竹这么辛苦,支持一下.

大饼先生 2006-8-31 13:40

NIO的核心技术:Selector、SelectableChannel和SelectionKey

说上面的三个类是NIO的核心技术我觉得一点都不为过~~
因为缓冲技术、字符集映射都是为它们准备的~~
三者位于java.nio.channels这个包里


Selector选择器,它将为我们监听所有的I/O操作,可以通过select()阻塞方法来确定什么时候在Channel中有数据流需要处理。可以由Selector类的静态方法Selector.open()获得一个Selector实例。Selector对象和SelectableChannel 对象互为多路复用器。
可以通过某个通道的register()方法注册到已经存在的选择器Selector。register()方法的声明如下:
public final SelectionKey register(Selector sel, int ops):向给定的选择器注册此通道,返回一个选择键。
其中,sel - 要向其注册此通道的选择器,ops - 所得键的可用操作集 。
ops可以有四个选择:
SelectionKey.OP_ACCEPT:                用于套接字接受操作的操作集位。
SelectionKey.OP_CONNECT:    用于套接字连接操作的操作集位。
SelectionKey.OP_READ:                 用于读取操作的操作集位。
SelectionKey.OP_WRITE:                用于写入操作的操作集位。

register()是SelectableChannel中定义的方法,SelectableChannel是一个抽象类,实现它的子类有DatagramChannel, Pipe.SinkChannel, Pipe.SourceChannel, ServerSocketChannel, SocketChannel。
要生成一个SelecableChannel实例,可以调用其子类的静态方法open(),比如说生成一个ServerSocketChannel实例,可以调用:
ServerSocketChannel ssc = ServerSocketChannel.open();
再通过调用ssc.socket().bind(InetSocketAddress)将ssc绑定到指定ServerSocket。
SelectableChannel的configureBlocking(boolean b)方法,设置为false时即将该通道配置为非阻塞的。
必须将通道配置成非阻塞的,才可以向Selector注册:
ssc.configureBlocking(false);
SelectionKey sk = ssc.register(selector,SelectionKey.OP_ACCEPT);
注册过以后,就可以调用selector.select()方法来选择一组相应的通道已为 I/O 操作准备就绪的键了。

下面有二组代码,一组是基于NIO的,另一种是旧I/O的,大家可以对比参考下:
旧I/O客户端:
Socket s = new Socket("localhost",1111);
BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
String str;
while((str=in.readLine())!=null){
        System.out.println(str);
}
旧I/O服务端:
ServerSocket ss = new ServerSocket(1111);
Socket c = ss.accept();
PrintWriter out = new PrintWriter(c.getOutputStream(),true);
out.write("hello,client".getBytes());


NIO客户端:
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("localhost",1111));
ByteBuffer buffer = ByteBuffer.allocateDirect(512);
channel.read(buffer);

NIO服务端:
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(1111));
ssc.configureBlocking(false);
ssc.register(selector,SelectionKey.OP_ACCEPT);
ssc.select();
java.util.Set keys = ssc.selectedKeys();
java.util.Iterator i = keys.iterator();
while(i.hasNext()){
  SelectionKey key = i.next;
  i.remove();

  ServerSocketChannel serverSocket = (ServerSocketChannel)key.channel();
  SocketChannel socket = serverSocket.accept();
  ByteBuffer buf = ByteBuffer.wrap("hello,client\n".getBytes());
  socket.write(buf);
}

上面的NIO还保留了旧I/O的痕迹,还有待进一步改进~~
页: [1]
查看完整版本: [原创]介绍及应用JAVA NEW I/O