(二)Java中Socket的用法-读书笔记

阻塞(IO)与非阻塞(NIO)

阻塞的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待 状态, 直到有东西可读或者可写为止。

常用做法是:每建立一个Socket连接时,同时创建一个新线程对该Socket进行单独通信(采用阻塞的方式通信)。这种方式具有很高的响应速度,并且控制起来也很简单,在连接数较少的时候非常有效,但是如果对每一个连接都产生一个线程的无疑是对系统资源的一种浪费,如果连接数较多将会出现资源不足的情况。

非阻塞, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待 。

做法是:服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发现某个Socket端口上有数据可读时(读就绪),则调用该socket连接的相应读操作;如果发现某个 Socket端口上有数据可写时(写就绪),则调用该socket连接的相应写操作;如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口。这样能充分利用服务器资源,效率得到了很大提高。

名称IONIO
-面向流面向缓冲区
-阻塞IO非阻塞IO
-选择器

面向流与面向缓冲

Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。

Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

阻塞与非阻塞IO

Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

这里写图片描述

Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

选择器(Selectors)

Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

NIO简介

NIO的工作原理:

  1. 由一个专门的线程(Selector)来处理所有的 IO 事件,并负责分发。
  2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
  3. 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。

NIO主要有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。

Channel

Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的。InoutStream,OutputStream等都是单向,而Channel是双向的,可以用来读也可以写。

NIO中Channel主要实现有:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

    分别对应文件IO、UDP和TCP(Server和Client)。

Buffer

NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。

Selector

Selector运行单线程处理多个Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。例如在一个聊天服务器中。

这里写图片描述

要使用Selector, 得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。

流程分析

Java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上可以注册我们感兴趣的事件。

服务端和客户端各自维护一个管理通道的对象,我们称之 为selector,该对象能检测一个或多个通道 (channel) 上的事件。

以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。

NIO详解

客户端示例

要利用新I/O API的客户端时,首先要调用静态工厂方法SocketChannel.open()来创建一个新的java.nio.channels.SocketChannel对象。这个方法的参数是一个java.net.SocketAddress对象,知识要连接的主机和端口:

SocketAddress rama = new InetSocketAddress("rama.poly.edu",19);
SocketChannel client = SocketChannel.open(rama);

通道以阻塞模式打开,所以下一行代码在真正建立连接之前不会执行。如果连接无法建立,则会抛出一个IOException异常。

如果是传统客户端,可能会获取该Socket的输入和(或)输出流。但这不是传统的客户端。利用通道,你可以直接写入通道本身。不是写入字节数组,而是要写入ByteBuffer对象。假设输入有74个ASCII字符长,所以要使用静态方法allocate()创建一个容量为74字节的ByteBuffer:

ByteBuffer buffer = ByteBuffer.allocate(74);

将这个ByteBuffer对象传递给通道的read()方法。通道会用从Socket读取的数据填充这个缓冲区,它返回成功读取并存储在缓冲区的字节数:

int bytesRead = client.read(buffer);

默认情况,这会至少读取一个字节,或者返回-1指示数据结束,这与InputStream完全一样。如果有更多字节可以读取,它通常会读取更多字节。如果将这个客户端置于非阻塞模式,没有字节可用时会立即返回0,但这里的代码会像InputStream一样阻塞。如果读取发生错误,这个方法也会抛出一个IOException异常。

假定缓冲区中有一些数据(n>0),这些数据就被复制到System.out。有几个方法可以从ByteBuffer中提取一个字节数组,然后再写入传统的OutputStream(如System.out)。不过,坚持采用一种完全基于通道的解决方案会更有好处。这样的解决方案需要利用Channels工具类(确切地将是该工具类的newChannel()方法),将OutputStream System.out封装在一个通道中:

WritableByteChannel output = Channels.newChannel(System.out);

然后可以将读取的数据写入与System.out连接的这个输出通道中。这样做之前,必须回绕(flip)缓冲区,使得输出通道会从所读取数据的开头而不是末尾开始写入:

buffer.flip();
output.write(buffer);

你不必告诉输出通道要写入多少字节。缓冲区会记住其中包含多少字节。不过,一般情况,输出通道不能保证会写入缓冲区中的所有字节。不过,这个例子中,它是阻塞通道,要么全部写入,要么抛出IOException异常。

不要每次读/写都创建一个新的缓冲区,这样降低性能。相反,要重用现有的缓冲区。在再次读取之前要清空缓冲区:

buffer.clear();

这与回绕有些不同。回绕可以保证缓冲区中数据不变,知识准备写入而不是读取。清空则把缓冲区重置回初始状态。

如下是完整地客户端代码:

//一个基于通道的chargen客户端
public class ChargenClient{
    public static int DEFAULT_PORT = 19;

    public static void main(String[] args){
        if(args.length == 0)        
            return;

        int port;
        try{
            port = Integer.parseInt(args[1]);
        }catch(RuntimeException ex){
            port = DEFAULT_PORT;
        }

        try{
            SocketAddress address = new InetSocketAddress(args[0],port);
            SocketChannel client = SocketChannel.open(address);

            ByteBuffer buffer = ByteBuffer.allocate(74);
            WritableByteChannel out = Channels.newChannel(System.out);

            while(client.read(buffer)!= -1){
                buffer.flip();
                out.write(buffer);
                buffer.clear();
            }
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }
}

这个程序很简单,完全可以使用流来编写。只有当你希望客户端有更多功能时,才会真正提现出新特性。你可以在阻塞或非阻塞模式下运行这个连接,在非阻塞模式下,即使没有任何可用数据,read()也会立即返回。这就允许程序在试图读取前做其他操作。它不必等待慢速的网络连接。要改变阻塞模式,可以向configureBolcking()方法传入true(阻塞)或false(不阻塞)。

client.configureBlocking(false);

在非阻塞模式下,read()可能因为读不到任何数据而返回0,因此循环需要有些差别:

while(true){
    //把每次循环都要运行的代码放在这里
    //无论有没有读到数据
    int n = client.read(buffer);
    if(n>0){
        buffer.flip();
        out.write(buffer);
        buffer.clear();
    }else if(n == -1){
        //这不应当发生,除非服务器发生故障
        break;
    }
}

对于这样一个单连接客户来说,这么做意义不是很大。可能你会查看用户是否做了某些操作,例如取消输入。不过,下面会看到当程序处理多个连接时,这种做法会使代码在快速连接上运行得很快,而在慢速连接上运行慢一些。每个连接都以自己的速度运行,不会像在单行道上那样呗最慢的驾驶员挡在后面。

服务器示例

客户端使用通道和缓冲区是可以的,不过实际上通道和缓冲区主要用于需要高速处理很多并发连接的服务器系统。要处理服务器,除了用于客户端的缓冲区通道外,还需要第三个新的部分。具体来讲,需要有一些选择器,允许服务器查找所有准备好接收输出或发送输入的连接。

实现利用新I/O API服务器时,首选要调用表静态工厂方法ServerSocketChannel.open()创建一个新的ServerSocketChannel对象。

ServerSocketChannel serverChannel = ServerSocketChannel.open();

开始时,这个通道并没有具体监听任何端口。要把它绑定到一个端口,可以用socket()方法获取其ServerSocket对等端(peer)对象,然后使用bind()方法绑定到这个对等端:

ServerSocket ss = serverChannel.socket();
ss.bind(new InetSocketAddress(19));

在Java 7及以后版本中,可以直接绑定而不用获取底层java.net.ServerSocket:

serverChannel.bind(new InetSocketAddress(19));

与正常服务器Socket一样,要绑定端口19。在UNIX(包括Linux和Mac OS X)上必须是root用户。非root用户只能绑定1024及以上的端口。

服务器Socket通道现在在端口19监听入站连接。要接受连接,可以调用accept()方法,它会返回一个SocketChannel对象:

SocketChannel clientChannel = serverChannel.accept();

在服务端,希望客户端通道处于非阻塞模式,以允许服务器处理多个并发连接:

clientChannel.configureBlocking(false);

你还会希望ServerSocketChannel也处于非阻塞模式。默认,accept()方法会阻塞,直到有一个入站连接为止,这与ServerSocket的accept()方法类似。为了改变这一点,只需在调用accept()之前调用configureBlocking(false):

serverChannel.configureBlocking(false);

如果没有入站连接,非阻塞的accept()几乎会立即返回null。要确保对此进行检查,否则当试图使用这个socket时,会得到一个NullPointerException异常。

现在有两个打开的通道:服务器通道和客户端通道。两个通道都需要处理。它们都会无限运行下去。此外,处理服务器通道会创建更多打开的客户端通道,在传统方法中,要为每个连接分配一个县城,线程数目会随着客户端连接迅速攀升。相反,在新的I/O API中,可以创建一个Selector,允许程序迭代处理所有准备好的连接。要构造一个新的Selector,只需要调用Selector.open()静态工厂方法:

Selector selector = Selector.open();

接下来,需要使用每个通道的register()方法向监视这个通道的选择器进行注册。在注册时,要使用SelectionKey类提供的命名常量指定所关注的操作。对于服务器Socket,唯一关心的操作就是OP_ACCEPT,也就是服务器Socket通道是否准备好接受一个新连接?

serverChannel.register(selector,SelectionKey.OP_ACCEPT);

对于客户端通道,希望知道是否已经准备好数据可以写入通道,为此,要使用OP_WRITE:

SelectionKey key = clientChannel.register(selector,SelectionKey.OP_WRITE);

两个register()方法都返回一个SelectionKey对象。不过,只需使用对应客户端通道的键,因为可能会有多个这样的键。每个SelectionKey都有一个任意Object类型的“附件”。它通常用于保存一个指示当前连接状态的对象。这里,可以将通道要写入网络的缓冲区存储在这个对象中。一旦缓冲区完全排空(drain),将重新填满。要用将复制到各缓冲区的数据来填充数组。并不是写到缓冲区末尾,而是要回转到缓冲区开始位置重新写入。先从两段顺序的数据开始会容易一些,这样每一行可以作为数组中的一个连续序列:

byte[] rotaion = new byte[95*2];
for(byte i = '';i<='~';i++){
    rotaion[i-''] = i;
    rotaion[i+95-''] = i;
}

因为此数据在初始化之后只用于读取,所以可以重用于多个通道。不过,每个通道都会用这个数组的内容填充其自己的缓冲区。我们会用这个循环数组的前72字节填充缓冲区,然后加上回车/换行对来分割各行。接下来要回绕缓冲区,从而可以进行排空,并附加到通道的键上:

ByteBuffer buffer = ByteBuffer.allocate(74);
buffer.put(rotation,0,72);
buffer.put((byte)'\r');
byffer.put((byte)'\n');
buffer.flip();
key2.attach(buffer);

为了检查是否有可操作的数据,可以调用选择器的select()方法。对于长时间运行的服务器,这一般要放在一个无限循环中:

while(true){
    selector.select();
}

假定选择器确实找到了一个就绪的通道,其selectedKeys()方法会返回一个java.util.Set,其中对应各个就绪通道分别包含一个SelectionKey对象。否则它会返回一个空集。两种情况下,都可以通过一个java.util.Interator循环处理:

Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator iterator = readyKeys.iterator();
while(iterator.hasNext()){
    SelectionKey key = iterator.next();
    //从集合中删除这个键,从而不会处理两次
    iterator.remove();
    //处理通道...
}

如果就绪的通道是服务器通道,程序就会接受一个新Socket通道,将其添加到选择器。如果就绪的通道是Socket通道,程序就会向通道写入缓冲区中尽可能多的数据。如果没有通道就绪,选择器就会等待。一个线程(主线程)可以同时处理多个连接。

这里可以很容易地判断所选择的通道是客户端通道还是服务器通道,因为服务器通道只准备接受,而客户端只准备写入。二者都是I/O操作,由于多种原因,它们都可能会抛出IOException异常,所以需要将它们都包围在try块中:

try{
    if(key.isAcceptable()){
        ServerSocketChannel server = (ServerSocketChannel)key.channel();
        SocketChannel connection  = server.accept();
        connection.configureBlocking(false);
        connection.register(selector,SelectionKey.OP_WRITE);
        //为客户端建立缓冲区...
    }else if(key.isWritable()){
        SocketChannel client = (SocketChannnel)key.channel();
        //向客户端写入数据......
    }
}

向通道写入数据很简单。首先获取键的附件,将它转换为ByteBuffer,调用hasRemaining()检查缓冲区中是否还剩余未写的数据。如果有,就写入到通道。否则,用rotation数组中的下一行数据重新填充缓冲区,并写入通道。

ByteBuffer buffer = (ByteBuffer)key.attachment();
if(!buffer.hasRemaining()){
    //用下一行数据重新填写缓冲区
    //确定最后一行从哪里开始
    buffer.rewind();
    int first = buffer.get();
    //递增到下一个字符
    buffer.rewind();
    int position = first - '' + 1;
    buffer.put(rotation,position,72);
    buffer.put((byte)'\r');
    buffer.put((byte)'\t');
    buffer.flip();
}
client.write(buffer);

要确定从哪里获取下一行数据,这个算法依赖于以ASCII字符顺序存储在rotation数组中的字符。buffer.get()从缓冲区中读取第一个数据字节。这个数字减去空格字符(32),因为空格是rotation数组中的第一个字符。由此可以知道缓冲区当前从数组的哪个索引开始。要加1来得到下一行的开始索引,并重新填充缓冲区。

//一个非阻塞的chargen服务器
public class ChargenServer{
    int port;
    try{
        port = Integer.parseInt(args[0]);
    }catch(RuntimeException){
        port = DEFAULT_PORT;
    }

    byte[] rotation = new byte[95*2];
    for(byte i='';i<='~';i++){
        rotation[i-''] = i;
        rotation[i+95-''] = i;
    }

    ServerSocketChannel serverChannel;
    Selector selector;
    try{
        serverChannel = ServerSocketChannel.open();
        ServerSocket ss = serverChannel.socket();
        InetSocketAddres address = new InetSocketAddress(port);
        ss.bind(address);
        serverChannel.configureBlocking(false);
        selector = Seletor.open();
        serverChannel.register(selector,SelectionKey.OP_ACCEPT);
    }catch(IOException ex){
        ex.printStackTrace();
        return;
    }

    while(true){
        try{
            selector.select();
        }catch(IOException ex){
            ex.printStackTrace();
            break;
        }

        Set<SelectKey> readyKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = readyKeys.iterator();
        while(iterator.hasNext()){
            SelectionKey key = iterator.next();
            iterator.remove();
            try{
                if(key.isAcceptable()){
                    ServerSocketChannel server = (ServerSocketChannel)key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    SelectionKey key2 = client.register(selector,SelectionKey.OP_WRITE);
                    ByteBuffer buffer = ByteBuffer.allocate(74);
                    buffer.put(rotaion,0,72);
                    buffer.put((byte)'\r');
                    buffer.put((byte)'\n');
                    buffer.flip();
                    key2.attach(buffer);
                }else if(key.isWritable()){
                    SocketChannel client = (SocketChannel)key.channel();
                    ByteBuffer buffer = (ByteBuffer)key.attachment();
                    if(!buffer.hasRemaining()){
                        ...
                    }
                    client.write(buffer);
                }
            }catch(IOException ex){
                key.cancel();
                key.channel().close();
            }
        }
    }
}

这个例子只是用了一个线程。还有一些情况下可能仍要使用多个线程。

缓冲区

在新的I/O模型中,不再向输出流写入数据和从输入流读取数据,而是要从缓冲区读写数据。像在缓冲流一样,缓冲区可能就是字节数组。不过,原始实现可以将缓冲区直接与硬件或内存连接,或者使用其他高效的实现。

从编程角度,流和通道之间的关键区别在于是基于字节的,而通道是基于块的。流设计为按顺序一个字节接一个字节得传递数据。出于性能考虑,也可以传送字节数组。不过,基本概念都是一次传递一个字节的数据。不同的是,通道会传送缓冲区中的数据块。可以读写通道的字节之前,这些字节必须已经存储在缓冲区中,而且一次会读/写一个缓冲区的数据。

流和通道/缓冲区之间的第二个关键区别是,通道和缓冲区支持同一对象的读/写。

不考虑底层细节,可以把缓冲区看作是固定大小的元素列表,如数组,这些元素为某种特定类型,一般是基本数据类型。在后台不一定是数组,有时候确实是数组,有时候则不是。除了boolean外,Java的所有基本数据类型都有特定的Buffer子类:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer和DoubleBuffer。每个子类中的方法都有相应类型的返回值和参数列表。网络程序中几乎只会使用ByteBuffer。

除了数据列表外,每个缓冲区都记录了信息的4个关键部分。无论缓冲区是何种类型,都有相同的方法来获取和设置这些值:

  • 位置(position)
    缓冲区中将读取或写入的下一个位置,这个位置值从0开始计,最大值等于缓冲区的大小。可以用下面两个方法获取和设置:
public final int position()
public final Buffer position(int newPosition)
  • 容量(capacity)
    缓冲区可以保存元素的最大数目。容量值在创建缓冲区时设置,此后不能改变。
    可以用以下方法读取:
public final int capacity()
  • 限度(limit)

    缓冲区中可访问数据的末尾位置。只要不改变限度,就无法读/写超过这个位置的数据,即使缓冲区有更大的容量也无用。限度可以用如下良妃方法来获取和设置:

public final int limit()
public final Buffer limit(int newLimit)

标记(mark)
缓冲区中客户端指定的索引。通过调用mark()可以将标记设置为当前位置。调用reset()方法可以将当前位置设置为所标记的位置。

标记,使缓冲区能够记住一个位置并在之后将其返回。缓冲区的标记在mark( )函数被调用之前是未定义的,调用时标记被设为当前位置的值。reset( )函数将位置设为当前的标记值。如果标记值未定义,调用reset( )将导致InvalidMarkException异常。一些缓冲区函数会抛弃已经设定的标记(rewind( ),clear( ),以及flip( )总是抛弃标记)。如果新设定的值比当前的标记小,调用limit( )或position( )带有索引参数的版本会抛弃标记。

public final Buffer mark()
public final Buffer reset()

如果将位置设置为低于现有标记,则丢弃这个标记。

与读取InputStream不同,读取缓冲区实际上不会以任何方式改变缓冲区中的数据。只可能向前或向后设置位置,从而可以从缓冲区某个特定位置开始读取。类似,程序可以调整限度,从而控制将要读取的数据的末尾。只有容量是固定的。

公共的Buffer超类还提供了另外几个方法,可以通过这些公共属性的引用来进行操作。

clear()方法将位置设置为0,并将限度设置为容量,从而将缓冲区“清空”。这样一来,就可以完全重新填充缓冲区了:

public final Buffer clear()

不过,clear()方法没有删除缓冲区中的老数据。这些数据仍然存在,还可以使用绝对get方法或者再改变限度和位置进行读取。

rewind()将位置设置为0,但不改变限度:

public final Buffer rewind()

这允许重新读取缓冲区。

flip()方法将限度设置为当前位置,位置设置为0:

public final Buffer flip()

希望排空刚刚填充的缓冲区时就可以调用这个方法。

最后,还有两个方法可以返回缓冲区的信息,但不改变这些信息。remaining()方法返回缓冲区中当前位置与限度之间的元素数。如果剩余元素大于0,hasRemaining()方法返回true:

public final int remaining()
public final boolean hasRemaining()
创建缓冲区

缓冲区类的层次是基于继承的,而不基于多态,至少在顶层是如此。一般需要知道你要处理的是IntBuffer、ByteBuffer、CharBuffer还是其他类型。要用其中一个子类编写代码,而不是一般的Buffer超类。

每种类型的缓冲区类都有几个工厂方法,以各种方式创建这个类型的特定于实现的子类。空的缓冲区一般由分配(allocate)方法创建。预填充数据的缓冲区由包装(wrap)方法创建、分配方法通常用于输入,而包装方法一般用于输出。

分配

基本的allocate()方法只返回一个有指定固定容量的新缓冲区,这是一个空缓冲区。例如,下面几行代码创建了字节和整型缓冲区,大小都为100:

ByteBuffer buffer1 = ByteBuffer.allocate(100);
IntBuffer buffer2 = IntBuffer.allocate(100);

游标位于缓冲区开始位置(就是说,位置为0)。用allocate()创建的缓冲区基于Java数组实现,可以通过array()和arrayOffset()方法来访问。例如,可以使用通道将一大块数据读入缓冲区。然后从缓冲区获取这个数组,传递给其他方法:

byte[] data1 = buffer1.array();
int[] data2 = buffer2.array();

array()实际上暴露了缓冲区的私有数据,所以要谨慎使用。修改后备数组会反映到缓冲区中,反之亦然。这里一般模式是使用数据填充缓冲区,获取其后备数组,然后操作这个数组。开始处理数组之后就不要再写缓冲区,只要做到这一点,就不会有问题。

直接分配

ByteBuffer类(但不包括其他缓冲区类)有另外一个allocateDirect()方法,这个方法不为缓冲区创建后备数组。VM会对以太网卡、核心内存或其他位置上的缓冲区使用直接内存访问,以此实现直接分配的ByteBuffer。这不是必须的,但是允许的。可以提升I/O操作的性能。从API角度看,allocateDirect()的使用与allocate()完全相同:

ByteBuffer buffer = ByteBuffer.allocateDirect(100);

在直接缓冲区上调用array()和arrayOffset()会抛出一个UnsupportedOperationException异常。直接缓冲区在一些虚拟机上会更快,尤其是缓冲区很大时(大约1MB或更多)。不过,创建直接缓冲区比间接缓冲区代价更高,所以只能在缓冲区可能值持续较短时间时才分配这种直接缓冲区。其细节非常依赖VM。与大多数性能建议一样,除非经过测量后发现性能确实是个问题,否则不应考虑使用直接缓冲区。

包装

如果已经有了要输出的数据数组,一般要用缓冲区进行包装,而不是分配一个新缓冲区,然后一次一部分的复制到这个缓冲区:

byte[] data = "Some data".getBytes("UTF-8");
ByteBuffer buffer1 = ByteBuffer.wrap(data);
char[] text = "Some text".toCharArray();
CharBuffer buffer2 = CharBuffert.wrap(text);

这里,缓冲区包含数组的一个引用,这个数组将作为它的后备数组。由包装创建的缓冲区肯定不是直接缓冲区。再次说明,修改数组会反映到缓冲区,反之亦然,所以对数组操作结束前不要包装数组。

填充和排空

缓冲区是为顺序访问而设计的。每个缓冲区都有一个当前位置,由position()方法标识,这是0到缓冲区元素个数之间的某个数(可以为0或缓冲区元素个数)。从缓冲区读取或向其写入一个元素时,缓冲区的位置将增1,例如,你想分配一个12的CharBuffer,并在其中放置5个字符:

CharBuffer buffer = CharBuffer.allocate(12);
buffer.put('H');
buffer.put('e');
buffer.put('l');
buffer.put('l');
buffer.put('o');

缓冲区的位置现在为5,这称为填充缓冲区。

缓冲区至多只能填充到其容量大小。如果试图填充的数据超出了初始设置的容量,put()方法会抛出一个BufferOverflowException异常。

如果现在试图使用get()从缓冲区中获取数据,你会得到null字符(\u0000),Java初始化字符缓冲区时位置5就是这个null字符。在次读取写入的数据之前,需要回绕缓冲区:

buffer.flip();

这会把缓冲区的限度设置为其位置(这个例子中为5),并将位置重新设置为0,就是缓冲区的开始位置。现在可以将其排空到一个新的字符串:

String result = "";
while(buffer.hasRemaining()){
    result += buffer.get();
}

每个get()调用都会将位置前移一个元素。位置达到限度时,hasRemaining()返回false。这称为排空缓冲区。

Buffer类还有一些绝对(absolute)方法,可以在缓冲区大的指定位置填充和排空,而无需更新位置。例如:

public abstract byte get(int index)
public abstract ByteChar put(int index,byte b);

通过使用绝对方法,可以将同样这个文本放入缓冲区:

CharBuffer buffer = CharBuffer.allocate(12);
buffer.put(0,'H');
buffer.put(1,'e');
buffer.put(2,'l');
buffer.put(3,'l');
buffer.put(4,'o');

不过,读出前不需要再回绕,因为绝对方法不改变位置。另外,顺序不再有任何影响。

批量方法

即使是使用缓冲区,操作数据块通常也比一次填充和排空一个元素要快。不同的缓冲区类都有一些批量方法(bulk method)来填充和排空相应元素类型的数组。

例如,ByteBuffer有put()和get()方法,可以用现有的字节数组或子数组填充和排空一个ByteBuffer:

public ByteBuffer get(byte[] dst,int offset,int length);
public ByteBuffer get(byte dst);
public ByteBuffer put(byte[] array,int offset,int length);
public ByteBuffer put(byte[] array);

这些put方法从当前位置开始插入指定数组或子数组的数据。get方法从当前位置将数据读取到参数指定的数组或子数组中。put和get都会使位置增加数组或子数组的长度。如果缓冲区没有足够的空阿金容纳这个数组或子数组,put方法会抛出一个BufferOverflowException异常。如果缓冲区没有足够剩余诗句来填充这个数组或子数组,get方法就抛出BufferUnderflowException异常,这些都是运行时异常。

数据转换

Java中的所有数据最终都解析为字节。所有基本数据类型–int、double、float等都可以写为字节。任何适当长度的字节序列都可解释为基本类型数据。例如,任何4字节的序列都可以对应一个int或float。8字节的序列对应于一个long或者double。ByteBuffer类(只有ByteBuffer类)提供了相对和绝对的put方法,可以用简单类型(boolean除外)参数的相应字节填充缓冲区;ByteBuffer类还提供了相对和绝对的get方法,可以读取适当数量的字节来形成一个新的基本类型数据:
这里写图片描述
这里写图片描述

在新I/O世界里,这些方法完成了传统I/O中由DataOutputStreram和DataInputStream完成的任务,还有一些所没有的额外能力。你可以选择将字节序列解释为big-endain或little-endian的int、float、double等。默认情况下,所有值都以big-endian方式读/写,即最高字节在前。另外还提供了两个order()方法,可以使用ByteOrder类的命名常量来检查和设置缓冲区的字节顺序。例如,可以将缓冲区改为little-endian:

if(buffer.order().equals(ByteOrder.BIG_ENDIAN)){
    buffer.order(ByteOrder.LITTILE_ENDIAN);
}
视图缓冲区

如果你知道从SocketChannel读取的ByteBuffer只包含某种简单数据类型,那么就有必要创建一个视图缓冲区(view buffer)。这是用于DoubleBuffer、IntBuffer等相应类型的新的Buffer对象,它将数据从底层ByteBuffer提取出来(从当前位置开始)。修改视图缓冲区会反应到底层缓冲区,反之亦然。但是,每个缓冲区都有自己独立的限度、容量、标记和位置。视图缓冲区用以下ByteBuffer的六个方法之一创建:

public abstract ShortBuffer asShortBuffer()
public abstract CharBuffer asCharBuffer()
public abstract IntBuffer asIntBuffer()
public abstract LongBuffer asLongBuffer()
public abstract FloatBuffer asFloatBuffer()
public abstract DoubleBuffer asDoubleBuffer()

通道

通道将缓冲区的数据块移入或移出到各种I/O源,如文件、socket、数据报等等。

SocketChannel

SocketChannel类可以读写TCP socket。数据读写时必须编码在ByteBuffer对象中。每个SocketChannel都与一个对等端(peer)Socket对象相关联。

ServerSocketChannel

ServerSocketChannel类只有一个目的:接受入站连接。ServerSocketChannel是无法读取,写入或连接的。它所支持的唯一操作是接受一个新的入站连接。

Channels 类

Channels是一个简单工具类,可以将传统的基于I/O的流。

就绪选择

新I/O API的第二部分是就绪选择,即你能够选择读写时不阻塞的socket。

为完成就绪选择,要将不同的通道注册到一个Selector对象。每个通道分配有一个SelectionKey。然后程序可以询问Selector对象,哪些通道已经准备就绪,可以无阻塞地完成你所希望的操作,可以向Selector对象查询相应的键集合。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值