NIO与Socket笔记 : Socket、NIO 理论

一、Socket 理论

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

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

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

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

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

二、NIO理论

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

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

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

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

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

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

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

官方API如图:

从 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个技术点之间值的大小关系如下:

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所示。

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

limit 限制获取与设置

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

注意:

当设置 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。

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

剩余空间的获取remaining

使用 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()方法的作用:判断此缓冲区是否为直接缓冲区。  默认采用间接缓冲区

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;
}

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

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&nbsp;&nbsp;<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>&nbsp;+&nbsp;<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).

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

猜你喜欢

转载自blog.csdn.net/zhanglong_4444/article/details/88869218