一、Socket 理论

Socket编程其实就是实现服务端与客户端的数据通信,不管使用任何的编程语言,

在 实现上基本上都是4个步骤: 1建立连接;2请求连接; 3回应数据; 4结束连接,这4个 步骤的流程图如图 1-3所示。

NioSocketChannel于SocketChannel区别 nio和socket有关系吗_System

 

在学习 NIO 之前,必须先学习 Socket,因 为 NIO 中的核心通道类都是基于 Socket技术的通道类 。

学习 Socket 时要着重学习 Socket Option特性,因为它会影响程序运行的效率。

在网络程序优化时,除了优化代码之外,还 要优化 Socket Option 中的参数。

 

二、NIO理论

大致来讲, NIO相比普通的I/O提供了功能更加强大、 处理数据更快的解决方案,它可 以大大提高 I/O (输入/输出) 吞 吐量 ,常用在高性能服务器上 。 随着互 联网的发展,在大 多数涉及 Java 高性能的应用软件中 , NIO 是必不可少的技术之一。

 

NIO 实现高性能处理的原理是使用较少的线程来处 理更多的任务

NioSocketChannel于SocketChannel区别 nio和socket有关系吗_ci_02

 

使用较少的 Thread线程,通过 Selector选择器来执 行不同 Channel通道中的任务,

执行的任务再结合 AIO (异步 I/O)就能发挥服务器最大的性能,大大提升软件运 行效率 。

 

常规的 I/O (如 InputStream 和 OutputStream)存在很大的缺点,就是它们是阻塞的

NIO 采用非阻塞高性能运行的方式

 

NIO 技术中的核心要点:缓冲区( Buffer)。

官方API如图:

NioSocketChannel于SocketChannel区别 nio和socket有关系吗_ci_03

从 Buffer类的 Java 文档中可以发现, Buffer类是一个 抽象类,它具有 7个 直接 子 类,分 别 是 ByteBuffer、 CharBuffer、 DoubleBuffer、 FloatBuffer、 IntBuffer、 LongBuffer、 ShortBuffer,也就是缓冲区中存储的数据类型并不像普通 I/O 流只能存储 byte 或 char数据 类型, Buffer类能存储的数据类型是多样的 。

 

Buffer 类的使用

接口方法:

抽象类 Buffer.java 的 7个子类也是抽象类,也就意味着 ByteBuffer、 CharBuffer、 DoubleBuffer、 FloatBuffer、 lntBuffer、 LongBuffer和 ShortBuffer这些类也不能被直接 new实例化。 如果 不能直接 new实例化,那么如何创建这些类的对象呢?

使用的方式是将上面 7种数据类型 的数组包装( wrap) 进缓冲区中,此时就需要借助静态方法 wrap()进行实现。

wrap()方法的 作用是将数组放入缓冲区中,来构建存储不同数据类型的缓冲区。

缓冲区为非线程安全的

包装数据与获得容量

在 NIO 技术的 缓 冲区中,存在 4 个核心技术点,分别是:

capacity (容量 )  : 包含元素的数量,不能为负, 不能修改。
limit (限制) : 返回此缓冲区 的限制 。
position (位置)  : :返回此缓冲区的位置。
mark (标记) : 在此缓冲区的位置设置标记。

 

 

这 4个技术点之间值的大小关系如下:

NioSocketChannel于SocketChannel区别 nio和socket有关系吗_ci_04

 

capacity的方法及作用

import java.nio.*;

public class BufferTest01 {

    public static void main(String[] args) {
        
        byte[] byteArray = new byte[] { 1, 2, 3 };
        short[] shortArray = new short[] { 1, 2, 3, 4 };
        int[] intArray = new int[] { 1, 2, 3, 4, 5 };
        long[] longArray = new long[] { 1, 2, 3, 4, 5, 6 };
        float[] floatArray = new float[] { 1, 2, 3, 4, 5, 6, 7 };
        double [] doubleArray = new double [] { 1, 2, 3, 4, 5, 6, 7, 8 };
        char[] charArray = new char[]{ 'a' , 'b', 't' , 'c' , 'd' };

        ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray) ;
        ShortBuffer shortBuffer = ShortBuffer.wrap(shortArray) ;
        IntBuffer intBuffer =IntBuffer.wrap (intArray) ;
        LongBuffer longBuffer = LongBuffer.wrap(longArray) ;
        FloatBuffer floatBuffer = FloatBuffer.wrap(floatArray);
        DoubleBuffer doubleBuffer = DoubleBuffer.wrap(doubleArray) ;
        CharBuffer charBuffer = CharBuffer.wrap(charArray);

        System.out.println("bytebuffer=" + bytebuffer.getClass() .getName()) ;
        System.out.println("shortBuffer=" + shortBuffer.getClass() .getName()) ;
        System.out.println("intBuffer=" + intBuffer.getClass().getName());
        System.out.println("longBuffer=" + longBuffer.getClass() .getName()) ;
        System.out.println("floatBuffer=" + floatBuffer.getClass() .getName()) ;
        System.out.println("doubleBuffer=" + doubleBuffer.getClass() .getName());
        System.out .println ("charBuffer=" + charBuffer .getClass() .getName());

        System.out.println("===============================================");

        System.out.println("bytebuffer.capacity=" + bytebuffer.capacity ()) ;
        System.out.println("shortBuffer.capacity=" + shortBuffer.capacity()) ;
        System.out.println("intBuffer.capacity=" + intBuffer.capacity());
        System.out.println("longBuffer .capacity=" + longBuffer. capacity()) ;
        System.out.println("floatBuffer capacity=" + floatBuffer.capacity()) ;
        System.out.println( "doubleBuffer.capacity=" + doubleBuffer.capacity()) ;
        System.out. println ("charBuffer .capacity=" + charBuffer .capacity()) ;



    }
}

Connected to the target VM, address: '127.0.0.1:58053', transport: 'socket'
 bytebuffer=java.nio.HeapByteBuffer
 shortBuffer=java.nio.HeapShortBuffer
 intBuffer=java.nio.HeapIntBuffer
 longBuffer=java.nio.HeapLongBuffer
 floatBuffer=java.nio.HeapFloatBuffer
 doubleBuffer=java.nio.HeapDoubleBuffer
 charBuffer=java.nio.HeapCharBuffer
 ===============================================
 bytebuffer.capacity=3
 shortBuffer.capacity=4
 intBuffer.capacity=5
 longBuffer .capacity=6
 floatBuffer capacity=7
 doubleBuffer.capacity=8
 charBuffer .capacity=5
 Disconnected from the target VM, address: '127.0.0.1:58053', transport: 'socket'Process finished with exit code 0
   
 
 
由于 ByteBuffer、 CharBuffer、 DoubleBuffer、 FloatBuffer、 IntBuffer、 LongBuffer 和 ShortBuffer是抽象类,

因此 wrap()就相当于创建这些缓冲区的工厂方法,在源代码中创建 的流程示例如图 1-8所示。

NioSocketChannel于SocketChannel区别 nio和socket有关系吗_ci_05

 

 

缓冲区存储的数据还是存储在 byte[] 字节数组中 。

NioSocketChannel于SocketChannel区别 nio和socket有关系吗_ci_06

 

limit 限制获取与设置

 

JDKAPI DOC 中对 limit 的解释是:代表第一 个不应该读取或 写入元素 的 index。

NioSocketChannel于SocketChannel区别 nio和socket有关系吗_System_07

 

注意:

当设置 limit, 插入,读取都受 limit 限制.

比如capacity 的值为4, limit 为2, 只能读取更新下标为0,1的数据.其他的无法操作!!!

 

package com.zl.nio;

import java.nio.*;

public class BufferTest02 {

    public static void main(String[] args) {


        char[] charArray = new char[]{ 'a' , 'b', 'c' , 'd' };

        CharBuffer buffer = CharBuffer.wrap(charArray) ;


        System.out. println ("capacity : " + buffer.capacity() +"   limie : "+ buffer.limit()) ;


        buffer.limit(2);

//        buffer.put(0, 'A') ;
//        buffer.put(1, 'B') ;
//        buffer.put(2, 'C') ;
//        buffer.put(3, 'D') ;
//        buffer.put(4, 'E') ;
//        buffer.put(5, 'F') ;


        buffer.get(0);
        buffer.get(1);
        buffer.get(2);
        buffer.get(3);

        System.out. println ("capacity : " + buffer.capacity() +"   limie : "+ buffer.limit()) ;



    }
}

 

Connected to the target VM, address: '127.0.0.1:58306', transport: 'socket'
 capacity : 4   limie : 4
 Exception in thread "main" java.lang.IndexOutOfBoundsException
     at java.nio.Buffer.checkIndex(Buffer.java:540)
     at java.nio.HeapCharBuffer.get(HeapCharBuffer.java:139)
     at com.zl.nio.BufferTest02.main(BufferTest02.java:30)
 Disconnected from the target VM, address: '127.0.0.1:58306', transport: 'socket'Process finished with exit code 1


 

位置获取与设置position

 

 什么是位置呢?它代表“下一个”要读取或写入元素的 index (索引),缓冲区的 position (位置)不能为负 ,并且 position不能大于其 limit。 如果 mark 已定义且大于新的 position, 则丢弃该 mark。

NioSocketChannel于SocketChannel区别 nio和socket有关系吗_java_08

position对应的 ind巳x是 3, 说明从此 。位置处开始写人或读取,直到 limit结束。

 

剩余空间的获取remaining

NioSocketChannel于SocketChannel区别 nio和socket有关系吗_ci_09

 

使用 Buffe mark()方法处理标记

在此缓冲区的位置设置标记。

 

 

总结

1).缓冲区的 capacity不能为负数,缓冲区的 limit不能为负数,缓冲区的 position不能为负数 。
2).position不能大于其 limit。
3).limit 不能大于其 capacity。
4).如果定义了 mark,则在将 position或 limit调整为小于该 mark的值时,该 mark被丢弃。
5).如果未定义 mark,那么调用 reset() 方法将导致抛出 InvalidMarkException 异 常 。
6).如果 position 大于新的 limit,则 position 的值就是新 limit 的值 。
7).当 limit和 position值一样时,在指定的 position写入数据时会出现异常,因为此位置是被限制的 。

 

判断只读: isReadOnly()

 

告知此缓冲区是否为只读缓冲区。 默认 false

 

直接缓冲区

boolean isDirect()方法的作用:判断此缓冲区是否为直接缓冲区。  默认采用间接缓冲区

NioSocketChannel于SocketChannel区别 nio和socket有关系吗_System_10

 

 

bytebuffer.isDirect=false
 shortBuffer.isDirect=false
 intBuffer.isDirect=false
 longBuffer .isDirect=false
 floatBuffer isDirect=false
 doubleBuffer.isDirect=false
 charBuffer .isDirect=false
  这个可以使用直接缓冲区
 
ByteBuffer bb = ByteBuffer.allocateDirect(10);


 

 

还原缓冲区的状态、clear

还原缓冲区到初始的状态, 包含将位置设置为0,将限制设置为容量,并丢弃标记, 即“一切为默认”。

但是不清理已经保存的数值.

 

public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

 

import java.nio.CharBuffer;

public class BufferTest03 {

    public static void main(String[] args) {


        CharBuffer buffer = CharBuffer.allocate(100) ;

        buffer.put(0, 'A') ;
        buffer.put(1, 'B') ;
        buffer.put(2, 'C') ;


        System.out. println ("capacity : " + buffer.capacity() +"   limit : "+ buffer.limit()) ;
        buffer.clear();
        System.out.println(buffer.get(0));
        System.out. println ("capacity : " + buffer.capacity() +"   limit : "+ buffer.limit()) ;


    }
}

 

Connected to the target VM, address: '127.0.0.1:58465', transport: 'socket'
 capacity : 100   limit : 100
 A
 capacity : 100   limit : 100
 Disconnected from the target VM, address: '127.0.0.1:58465', transport: 'socket'Process finished with exit code 0

对缓冲区进行反转

 

final Buffer flip()方法的作用:反转此缓冲区 。

首先将限制设置为当前位置,然后将位 置设置为 0。 如果已定义了标记,则丢弃该标记 。

final Buffer flip() 方法常用在向缓冲区中写入一些数据后, 之前调用,以改变 limit 与 position 的值.

 

 

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

 

 

NioSocketChannel于SocketChannel区别 nio和socket有关系吗_java_11

 

判断是否再底层实现的数组

final boolean hasArray()方法的作用:判断此缓冲区是否具有可访问的底层实现数组。

 

final char[] hb;
public final boolean hasArray() {
    return (hb != null) && !isReadOnly;
}

判断当前位置与限制之间是否有剩余元素

final boolean hasRemaining()方法的作用:判断在当前位置和限制之间是否有元素。

 

/**
 * Tells whether there are any elements between the current position and
 * the limit.
 *
 * @return  <tt>true</tt> if, and only if, there is at least one element
 *          remaining in this buffer
 */
public final boolean hasRemaining() {
    return position < limit;
}

重绕缓冲区

final Buffer rewind()方法的作用:重绕此缓冲区,将位置设置为 0并丢弃标记。

public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

 

rewind():使缓冲区为“重新读取”已包含的数据做好准备,它使限制保持不变,将位置设置为 0。

clear():使缓冲区为一系列新的通道读取或相对 put(value)操作做好准备,即它将限 制设置为容量大小,将位置设置为 0。

flip() : 使缓冲区为-系列新的通道写入或相对 get(value)操作做好准备,即它将限制 设置为当前位置,然后将位置设置为 0。

 

这 3 个方法的侧重点在于:
1) rewind()方法的侧重点在“重新", 在重新读取,写入时使用, 只是更改 position 指针的位置,capacity , limit 不变
2) clear()方法的侧重点在"还原一切状态";

3) flip()方法的侧重点在 substring截取。

 

获得偏移量arrayOffset

final int arrayOffset()方法的作用: 返回此缓冲区的底层实现数组中第一个缓冲区元素 的偏移量,

这个值在文档中标注为“可选操作”, 也就是子类可以不处理这个值。

 

/**
 * Returns the offset within this buffer's backing array of the first
 * element of the buffer  <i>(optional operation)</i>.
 *
 * <p> If this buffer is backed by an array then buffer position <i>p</i>
 * corresponds to array index <i>p</i> + <tt>arrayOffset()</tt>.
 *
 * <p> Invoke the {@link #hasArray hasArray} method before invoking this
 * method in order to ensure that this buffer has an accessible backing
 * array.  </p>
 *
 * @return  The offset within this buffer's array
 *          of the first element of the buffer
 *
 * @throws  ReadOnlyBufferException
 *          If this buffer is backed by an array but is read-only
 *
 * @throws  UnsupportedOperationException
 *          If this buffer is not backed by an accessible array
 */
public final int arrayOffset() {
    if (hb == null)
        throw new UnsupportedOperationException();
    if (isReadOnly)
        throw new ReadOnlyBufferException();
    return offset;
}

使用 List.toArray(T[])转成数组类型

如果 List中存储 ByteB曲 r数据类型, 则可以使用 List中的 toArray()方法转成 ByteBuffer[]数组类型,

public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}

 

ByteBuffer 类的使用

ByteBuffer类是 Buffer类的子类, 可以在缓冲区中以 字节为单位对数据进行存取,而 且它也是比较常用和 重要的缓 冲 区类 。

在使用 NIO 技术时,有很大的概率使用 ByteBuffer 类来进行数据的处理 。

 

ByteBuffer类提供了 6类操作。
1 )以绝对位置和相对位置读写单个字节的 get()和 put()方法。
2 )使用相对批 量 get(byte[] dst)方法可以将缓冲 区 中的连续字节传输到 byte[] dst 目标数组中 。
3 )使用相对批量 put(byte[] src)方法可以将 byte[]数组或其他字节缓冲区中的连续字节存储到此缓冲区中 。
4 ) 使用绝对和相对 getType 和 putType 方法可以按照字节顺序在字节序列中读写其他基本数据类型的值,方法 getType 和 putType 可以进行数据类型 的自动转换 。
5 )提供了创建视图缓冲区的方法,这些方法允许将字节缓冲区视为包含其他基本类型值的缓冲区,这些方法有 asCharBuffer()、 asDoubleBuffer()、 asF!oatBuffer()、 aslntBuffer()、 asLongBuffer()和 asShortBuffer()。
6 )提供了对字节缓冲区进行压缩( compacting)、复 制 ( duplicating) 和 截取( slicing) 的方法 。
 

 

创建堆缓冲区与直接缓冲区

字节缓冲区分为直接字节缓冲区与非直接字节缓冲区。

如果字节缓冲区为直接字节缓冲区, 则 JVM会尽量在直接字节缓冲区上执行本机 I/O 操作,

也就是直接对内核 空 间进行访问,以 提高运行效率 。

 

提高运行效率的原理就是在每次 调用基于操作系统的 1/0 操作之前或之后,

JVM 都会尽量避免将缓 冲区的内容复制到中间 缓冲区中,

或者从中间缓冲区中复制内容,这样就节省了一个步骤。

 

工厂方法 allocateDirect()可以创建直接字节缓冲区,

通过工厂方法 allocateDirect()返回 的缓冲区进行内存的分配和释放所需 的时间成本通常要高 于非直接缓 冲区 。

直接缓冲区操作 的数据不在 JVM 堆中 , 而是在内 核空 间中,根据这个结构可以分析出 ,直接缓 冲区善于保 存那些易受操作系统本机I/O操作影响的大量、 长时间保存的数据。

 

allocateDirect(int capacity)方法的作用:分配新的直接字节缓 冲 区。新缓 冲 区 的位置将 为零 ,其界限将为其容量 , 其标记是不确定的 。 无论它是否具有底层实现数组,其标记都是 不确定的 。

 

allocate(int capacity)方法 的作用 : 分配一个新的非 直接字节缓冲区 。 新缓冲区的位置为 零 ,其界限将为其容量,其标记是不确定的。 它将具有一个底层实现数组,且其数组偏移量 将为零。

 

使用 allocateDirect()方法创建出来的缓冲区类型为 DirectByteBuffer, 使用 unsafe 创建,

使用 allocate()方 法创建出来的缓冲区类型为 HeapByteBuffier。

 

 


// Primary constructor
//
DirectByteBuffer(int cap) {                   // package-private

    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;



}

直接缓冲区与非直接缓冲区的运行效率比较

直接缓冲区会直接作用于本地操作系统的I/O,处理数据的效率相比非直接缓冲区会快一些.

 

直接缓冲区( DirectByteBuffer)在内部使用 sun.misc.Unsafe 类 进行值的处理 。(测试的时候,稳定性并不是太好,处理数据时间不稳定.)

 

非直接缓冲区的实现类 HeapByteBuffer 进行处理.  对字节数组进行处理 .

 

包装 wrap 数据的处理

wrap(byte[] array)方法的作用:将 byte 数组包装到缓冲区中 。

内部是基于数组进行操作的.

 

put(byte b)和 get()方法的使用与 position 自增特性

Buffer类的每个子类都定义了两种 get (读)和 put (写)操作, 分别对应相对位置操作 和绝对位置操作。

相对位置操作是指在读取或写入一个或多个元素时’, 它从 “当前位置开始”,然后将 位置增加所传输的元素数。 如果请求的传输超出限制,则相对 g巳t操作将抛出 BufferUnder flowException异常,相对 put操作将抛出 BufferOverflowException异常 , 也就是说, 在这 两种情况下,都没有数据传输 。

绝对位置操作采用显式元素索引,该操作不影响位置。 如果索引 参数超出限制 , 则绝 对 get操作和绝对 put操作将抛出 IndexOutOfBoundsException异常。

 

put(byte[] src, int offset, int length)和 get(byte[] dst, int offset, int length)方法的使用

put(byte[] src, int offset, int length) 方法的作用 :相对批量 put方法,此方法将把给定源 数组中的字节传输到此缓冲区当前位置中。

如果要从该数组中复制的字节多于此缓冲区中的 剩余字节(即 length > remaining()),则不传输字节且将抛出 BufferOverflowException 异常 。

否则,此方法将给定数组中的 length个字节复制到此缓冲区中 。 将数组中给定 off偏移量位 置的数据复制到缓冲区的当前位置,从数组 中复制的元素个数为 length。 换句话说,调用此 方法的形式为 dst.put(src, offset, length).

 

 

其他方法略.................