JAVA NIO

第一章 IO介绍

1.1 IO概念

1.1.1 Buffer Handler

内存分为用户空间(User space),内核空间(Kernel space)。
用户空间的数据读写需要通过内存空间传递。
如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。

为什么要区分用户空间和内核空间呢?
每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。

为什么用户空间不能直接从disk读数据呢?

  • 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。
  • 硬件设备通常不允许直接操作虚拟内存,即用户空间

DMA是什么?
DMA(Direct Memory Access,直接存储器访问)。
在DMA之前通过中断CPU来传输数据。CPU响应中断,控制总线传输数据。
DMA不需要CPU参与数据传输。DMA可以和CPU交互请求控制总线传输数据。

1.1.1 scatter/gather

分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。
聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。

比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。
使用scatter示例代码:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
ByteBuffer tail   = ByteBuffer.allocate(128);

ByteBuffer[] bufferArray = { header, body,tail };
channel.read(bufferArray);


注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。
所以。

scatter不适合处理动态消息,相反,gather适合处理动态消息。
buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。

1.1.2 虚拟内存

虚拟内存是指使用虚拟内存地址代替物理内存地址。

  • 会有多个虚拟内存指向同一个物理内存
  • 虚拟内存可能会大于物理内存
    上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。
    如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。

1.1.3 内存页

操作系统将内存按固定字节分页。
内存读取的基本单位是页。

MMU(Memory Management Unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在CPU和内存之间,起到将虚拟内存转换为物理内存的作用。

1.1.4 文件IO

文件系统:文件IO发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。
文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。

1.1.5 IO流

上面说的IO都是面向块的IO。还有面向流的IO。
大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。
网络IO一般都是流IO。
多路复用:用一个进程管理多个非阻塞IO流的状态。

第二章 Buffer

标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。

2.1 Buffer Basic

2.1.1 Buffer的几个属性

  • capacity
  • limit
  • position
  • mark

2.1.2 Buffer API

package java.nio;
public abstract class Buffer {
public final int capacity()
public final int position()
public final Buffer position (int newPosition) public final int limit()
  }
public final Buffer limit (int newLimit)
public final Buffer mark()
public final Buffer reset()
public final Buffer clear()
public final Buffer flip()
public final Buffer rewind()
public final int remaining()
public final boolean hasRemaining()
public abstract boolean isReadOnly();

2.1.3 访问Buffer

public abstract class ByteBuffer
       extends Buffer implements Comparable
 {
// This is a partial API listing
public abstract byte get();
public abstract byte get (int index);
public abstract ByteBuffer put (byte b);
public abstract ByteBuffer put (int index, byte b);
}

2.1.4 write

Figure 2-2的buffer经过下面这个代码会变成Figure 2-3的样子:

buffer.put((byte)'H').put((byte)'e').put((byte)'l')
.put((byte)'l').put( (byte)'o');


也可以按绝对位置写入:

buffer.put(0, (byte)'M').put((byte)'w');

2.1.5 Flipping

当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:

buffer.limit(buffer.position()).position(0);

这等同于buffer.flip();
调用flip()后2-4会变成2-5

说白了,flip()改变position和limit两个指针。
rewind()只改变position指针,等同于:

buffer.position(0);

2.1.6 Draining

hasRemaining()可以判断是否到达limit指针
下面是一个实现,将buffer内容读到一个数组中

for (int i = 0; buffer.hasRemaining(), i++) {
       myByteArray [i] = buffer.get();
}

remaining()会返回position到limit的数量。
将buffer内容读到一个数组中另一种实现方式:

int count = buffer.remaining();
for (int i = 0; i < count, i++) {
       myByteArray [i] = buffer.get();
}

2.1.7 Compacting

当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。
buffer.compact();可以完成这个工作。
调用buffer.compact()的效果就是2-6到2-7

2.1.9 Mark

mark()方法会用mark指针指向position的位置。

buffer.position(2).mark().position(4);


reset()方法会把position指针指向mark的位置。

buffer.reset();

2.1.9 Comparing

public abstract class ByteBuffer
       extends Buffer implements Comparable
  {
  // This is a partial API listing
public boolean equals (Object ob)
public int compareTo (Object ob)
}

equals()返回true需要满足三个条件:

  • Buffer包含相同类型的元素
  • 两个Buffer的remaining()返回值相同
  • remaining data的序列相同
    下面是两个例子:

2.2 创建Buffer

创建新buffer的两种方式:allocation or wrapping

public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable
{
}
// This is a partial API listing
public static CharBuffer allocate (int capacity)
public static CharBuffer wrap (char [] array)
public static CharBuffer wrap (char [] array, int offset, int length)
public final boolean hasArray()
public final char [] array()
public final int arrayOffset()

通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。
wrapping创建的buffer是创建一个buffer对象指向一个array空间。

2.3 Duplicating Buffers

public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable
   {
}
// This is a partial API listing
public abstract CharBuffer duplicate();
public abstract CharBuffer asReadOnlyBuffer(); 
public abstract CharBuffer slice();

duplicate()创建一个新的buffer,和原buffer一样。
两个buffer共享数据元素
但是他们有独立的position, limit, and mark
一个buffer更改了数据元素对另一个buffer可见
只读、直接内存这两个属性也可以通过duplicate()继承
举个例子:

CharBuffer buffer = CharBuffer.allocate (8); 
buffer.position (3).limit (6).mark().position (5); 
CharBuffer dupeBuffer = buffer.duplicate(); buffer.clear();


asReadOnlyBuffer()和duplicate()功能类似,只是asReadOnlyBuffer()返回一个只读的视图。
slice() 返回一个新的buffer视图,不过
new_position = original_position
new_capacity = original_limit - original_position
举个例子来看slice():

CharBuffer buffer = CharBuffer.allocate (8); 
buffer.position (3).limit (5);
CharBuffer sliceBuffer = buffer.slice();

2.4 Byte Buffers

2.4.1 Byte Ordering

原始数据类型在内存中的存储方式是多个字节的连续序列。
举个例子一个32-bit int 值为 0x037FB4C7的内存存储序列可能为2-14也可能为2-15:

这就是所谓的大端设计和小端设计。
采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。
IP协议定义的字节顺序是大端。所有使用IP协议的多字节数值必须在网络字节序和本地主机字节序做转换。

package java.nio;
public final class ByteOrder
{
public static final ByteOrder BIG_ENDIAN public static final ByteOrder LITTLE_ENDIAN
public static ByteOrder nativeOrder()
public String toString()
}

除了ByteBuffer之外,其他buffer的order()适中返回同一个值,ByteOrder.nativeOrder()。

2.4.2 Direct Buffers

public abstract class ByteBuffer extends Buffer implements Comparable {
// This is a partial API listing
public static ByteBuffer allocate (int capacity) ;
public static ByteBuffer allocateDirect (int capacity) ;
public abstract boolean isDirect();
}

allocateDirect可以直接在堆外开辟一个buffer。
堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。

2.4.3 View Buffers

收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到ByteBuffer的视图API。

public abstract class ByteBuffer extends Buffer implements Comparable {
// This is a partial API listing
public abstract CharBuffer asCharBuffer(); 
public abstract ShortBuffer asShortBuffer(); 
public abstract IntBuffer asIntBuffer();
 public abstract LongBuffer asLongBuffer();
  public abstract FloatBuffer asFloatBuffer();
   public abstract DoubleBuffer asDoubleBuffer();
   }

视图在直接内存buffer上操作更高效。
当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。

2.4.4 Data Element Views


当getInt() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。
int value = buffer.getInt();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。
int value = buffer.order (ByteOrder.BIG_ENDIAN).getInt();
返回0x3BC5315E。
int value = buffer.order (ByteOrder.LITTLE_ENDIAN).getInt();
返回0x5E31C53B。

第三章 Channels

3.1 basic

在NIO接口中我们需要使用Channel和Buffer进行IO操作,Channel模拟了流的概念,但是又有不同。数据总是从一个Channel读到一个buffer中,或者从一个buffer中写到channel中。
channel接口的主要实现类如下:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

JavaNIO Channels和流有一些相似,但是又有些不同:

你可以同时读和写Channels,流Stream只支持单向的读或写(InputStream/OutputStream)

  • Channels可以异步的读和写,流Stream是同步的
  • Channels总是读取到buffer或者从buffer中写入

下面分别介绍一下Channel最重要的一些实现类:

  • FileChannel : 可以读写文件中的数据
  • DatagramChannel:可以通过UDP协议读写数据
  • SocketChannel:可以通过TCP协议读写数据
  • ServerSocketChannel:允许我们像一个web服务器那样监听TCP链接请求,为每一个链接请求创建一个SocketChannel

3.1.1 Open channel

FileChannel只能通过getChannel()得到。
SocketChannel有工厂方法可以得到。

SocketChannel sc = SocketChannel.open();
sc.connect (new InetSocketAddress ("somehost", someport));

ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind (new InetSocketAddress (somelocalport));
DatagramChannel dc = DatagramChannel.open();

RandomAccessFile raf = new RandomAccessFile ("somefile", "r"); FileChannel fc = raf.getChannel();

3.1.2 Using Channels

public interface ReadableByteChannel extends Channel {
public int read (ByteBuffer dst) throws IOException;
}

public interface WritableByteChannel extends Channel {
public int write (ByteBuffer src) throws IOException;
 }

Channels 可以是单向或者双向的。
如果一个class实现了上面两个接口,就是双向的。
Channels可以在阻塞模式和非阻塞模式工作。
只有socket和pipes可以工作在非阻塞模式。

3.1.3 close Channels

3.2 Scatter/Gather

Scatter:将多个buffer按照顺序合成一个
Gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。

3.3 File Channels

File channels 只能以阻塞模式工作。

3.3.1 Accessing Files

public abstract class FileChannel extends AbstractChannel 
implements ByteChannel, GatheringByteChannel, ScatteringByteChannel{
// This is a partial API listing
//获取position位置
public abstract long position()
//设置position
public abstract void position (long newPosition)
//从position位置开始读
public abstract int read (ByteBuffer dst)
//从position位置开始读
public abstract int read (ByteBuffer dst, long position)
//从position位置开始写
 public abstract int write (ByteBuffer src)
//从position位置开始写
public abstract int write (ByteBuffer src, long position)
public abstract long size()
//截取文件保留size大小,并移动position到size的位置
public abstract void truncate (long size) 
//强制刷新缓存到磁盘文件
public abstract void force (boolean metaData)
}

3.3.2 File Locking

3.4 Memory-Mapped Files

3.5 Socket Channels

The new socket channels can operate in nonblocking mode and are selectable. These two capabilities enable tremendous scalability and flexibility in large applications.
ServerSocketChannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建SocketChannel。

a channel is a conduit to an I/O service and provides methods for interacting with that service

每个socket channels都有一个socket与之对应,调用socket()可以获得。
但并不是所有的sockets都有channels。socket的getChannel()可能但会null;

3.5.1 Nonblocking Mode

调用configureBlocking (false);设置channel为非阻塞模式。
但是只有持有blockingLock() 返回的对象锁的线程才能修改阻塞模式。

Socket socket = null;
Object lockObj = serverChannel.blockingLock();
// have a handle to the lock object, but haven't locked it yet
// may block here until lock is acquired
synchronize (lockObj){
// This thread now owns the lock; mode can't be changed boolean prevState = serverChannel.isBlocking();
serverChannel.configureBlocking (false); socket = serverChannel.accept(); serverChannel.configureBlocking (prevState);
}
// lock is now released, mode is allowed to change
if (socket != null) {
doSomethingWithTheSocket (socket);
}

3.5.2 ServerSocketChannel

The ServerSocketChannel class is a channel-based socket listener.
它负责处理socket任务并添加channel。

public abstract class SocketChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
{
//SocketChannel被创建但是并没有连接,open()只是创建一个SocketChannel,还需要调用connect()连接到一个地址
public static SocketChannel open() throws IOException
//创建并连接
public static SocketChannel open(SocketAddress remote) throws IOException
//支持的操作(读、写、连接)
public final int validOps()
、、
public abstract Socket socket();
//是否连接到远程
public abstract boolean isConnected();
//Connect()方法是耗时的,因为需要数据包对话(TCP握手)。如果SocketChannel身上有并发、connect(),isConnectionPending返回true
public abstract boolean isConnectionPending();
//建立连接
public abstract boolean connect(SocketAddress remote) throws IOException;
//任何时候都可以安全的调用
public abstract boolean finishConnect() throws IOException;

Socket channels是线程安全的。
Socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。

3.5.4 DatagramChannel

socketchannel为面向连接的流协议(如tcp/ip)
datagramchannel为面向无连接的数据包协议(如udp/ip)

3.5.5 Pipes

Pipe用于同一个JVM内不同线程之间的数据交换问题
不同JVM之间的数据交换问题应该使用SocketChannel
Pipe类创建了一对channel对象,它们提供了一种回送机制。
Pipe实现两个进程之间的单向数据连接。
Pipe有一个SinkChannel和一个SourceChannel
数据会从SourceChannel读取、被写到SinkChannel

public abstract class Pipe {
    public static abstract class SourceChannel
        extends AbstractSelectableChannel
        implements ReadableByteChannel, ScatteringByteChannel
    {
        protected SourceChannel(SelectorProvider provider) {
            super(provider);
        }
        public final int validOps() {
            return SelectionKey.OP_READ;
        }
    }

    public static abstract class SinkChannel
        extends AbstractSelectableChannel
        implements WritableByteChannel, GatheringByteChannel
    {
        protected SinkChannel(SelectorProvider provider) {
            super(provider);
        }
        public final int validOps() {
            return SelectionKey.OP_WRITE;
        }

    }
    protected Pipe() { }
    //从管道读取数据,要访问source通道
    public abstract SourceChannel source();
    //向管道写入数据,要访问Sink通道
    public abstract SinkChannel sink();
    //管道创建
    public static Pipe open() throws IOException {
        return SelectorProvider.provider().openPipe();
    }
}

第四章 Selectors

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。

4.1 基础

  • Selector:一个selector可以注册多个channel,一个线程通过管理Selector实现管理多个channel。
  • SelectableChannel:提供channle selectability的通用方法。
  • SelectionKey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。

4.2 示例

//创建Selector
Selector selector = Selector.open();
channel.configureBlocking(false);
//注册Selector
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
//单线程阻塞检查Selector中就绪的事件
  int readyChannels = selector.select();
  if(readyChannels == 0) continue;
  Set selectedKeys = selector.selectedKeys();
  Iterator keyIterator = selectedKeys.iterator();
  //循环处理就绪的事件
  while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值