NIO基础知识点整理---selector除外

x33g5p2x  于2021-12-30 转载在 其他  
字(23.4k)|赞(0)|评价(0)|浏览(188)

JVM读取数据模型

程序执行效率更多是由I/O效率决定的

需要等待数据传输

是JVM在I/O方面的效率不足,导致程序效率降低。在操作系统中,可以从硬件上直接读取大块的数据,而JVM的I/O更喜欢小块的数据读取,相当于操作系统视同大卡车运来很多数据,JVM的I/O就喜欢一铲子一铲子的加工这些数据。

在JDK4中引入了NIO,可以最大限度的满足Java程序I/O的需求

  • java.nio包,定义了各种与Buffer相关的类.
  • java.nio.channel包,包含了与Channel和Selector相关的类.
  • java.nio.charset包,与字符集相关的类

在NIO中有三大核心组件:Channel,Buffer,Selector

NIO是什么

传统的IO是面向流的,每次可以从流中读取一个或多个自己,只能向后读取,不能向前移动,NIO是面向缓冲区的,把数据读取到一个缓冲区中,可以在缓冲区中向前/向后移动,增加了程序的灵活性。

在NIO中,所有的数据都需要通过Channel传输,通道可以直接将一块数据映射到内存中,Channel是双向的,不仅可以读取数据。还可以保存数据。程序不能直接读取Channel通道,Channel只与Buffer缓冲区进行交互。

IO流时线程阻塞的,在调用read()/write()读写数据时,线程阻塞,直到数据读取完毕或者数据完全写入,在读写过程中,线程不能做其他的任务。

NIO不是线程阻塞的,当线程从Channel中读取数据时,如果通道中没有可用的数据,线程不阻塞,可用做其他的任务

Buffer

buffer属性

Buffer缓冲区实际上就是一个数组,把数组的内容与信息包装成一个Buffer对象,它提供了一组访问这些信息的方法

缓冲区的重要属性:

1.capacity容量: 指缓冲区可用存储多少个数据,容量在创建Buffer缓冲区时指定大小,创建后不能再修改,如果缓冲区满了,需要清空后才能继续写数据。

2.position表示当前位置,即缓冲区写入/读取的位置,刚刚创建Buffer对象后,position初始化为0,写入一个数据,position就向后面移动一个单元,它的最大值是capacity-1.

当Buffer从写模式切换到读模式,position会被重置为0,从Buffer的开始位置读取数据,每读一个数据,position就向后面移动一个单元.

2.limit上限: 指第一个不能被读出或写入的位置.limit上限后面的单元既不能读也不能写,在Buffer缓冲区的写模式下,limit表示能够写入多少个数据;在读取模式下,limit表示最多可以读取多少个数据。

3.mark标记:设置一个标记位置,可以调用mark方法,把标记设置在position位置,当调用reset()方法时,就把position设置为mark标记的位置。

0<=mark<=position<=limit<=capacity

Buffer常用API

在NIO中关键的Buffer有:ByteBuffer,CharBuffet,DoubleBuffer,FloatBuffer,IntBufer,LongBuffer,ShortBuffer。

这些Buffer覆盖了能够通过I/O发送的所有基本类型:byte,char,double,flaot.int.long,short等。实际上使用较多的是ByteBuffer和CharBuffer。

  • 每个缓冲区都有一个静态方法allocate(capacity)可以创建一个指定容量的缓冲区;
  • put()方法用于向缓冲区中存储数据;
  • get()方法可以从缓冲区中读取数据;
  • 当缓冲区中还有没有读完的数据,可以调用compact方法进行压缩,将所有未读取的数据复制到Buffer的起始位置,把position设置到最后一个未读元素的后面.limit属性设置为capacity.
  • capacity()方法返回缓冲区的大小
  • hasRemaining()判断当前position后面是否还有可以被处理的数据,即判断position与limit之间是否还有数据可处理。
  • limit()返回limit上限的位置
  • mark()设置缓冲区的标志位置,这个值只能在0–position之间,以后可以通过reset()将position置为mark标记的位置.
  • position()可以返回position当前位置
  • remaining()返回当前position位置与limit之间的数据量
  • reset()方法可以将position设置为mark标记位
  • rewind():将position设置为0,取消mark标志位
  • clear()清空缓冲区,仅仅是修改position标志位0,设置limit为capacity,缓冲区中数据还是存在的
  • flip()方法可以把缓冲区由写模式切换到读取模式,先把limit设置position位置,再把position设置为0;

Buffer的API使用演示

public class BufferAPI
{
    public static void main(String[] args) {
        //使用字符缓冲区--一个中文字符和英文字符都是一个字符大小,只是所占字节大小不一样
        CharBuffer charBuffer=CharBuffer.allocate(12);
        curPosition(charBuffer);//position=0,limit=12

        //向缓冲区中存储数据
        charBuffer.put("大");
        charBuffer.put("忽");
        charBuffer.put("悠");
        curPosition(charBuffer);//position=3,limit=12

        //调用flip,把缓冲区从写模式切换为读取模式
        charBuffer.flip();
        curPosition(charBuffer);//position=0,limit=3

        //从缓冲区读取数据
        System.out.print(charBuffer.get());
        System.out.print(charBuffer.get());
        System.out.print(charBuffer.get());
        curPosition(charBuffer);//position=3,limit=3

        charBuffer.clear();
        curPosition(charBuffer);//position=0,limit=12,mark=-1

        //往缓冲区中放入数据
        charBuffer.put("小");
        charBuffer.put("朋");
        charBuffer.put("友");
        curPosition(charBuffer);//position=3,limit=12

        //切换为写模式
        charBuffer.flip();//position=0,limit=3,mark=-1

        //设置标记
        charBuffer.mark();//mark=0

        //再读取一个字符
        System.out.println(charBuffer.get());
        curPosition(charBuffer);//position=1,limit=3

        //调用reset()将position重置为mark标记位置
        charBuffer.reset();
        curPosition(charBuffer);//position=0,limit=3

        //调用compact,将没有读取的数据复制到position为0的位置
        charBuffer.compact();
        curPosition(charBuffer);

        //调用clear清空,只是将position=0,limit=capacity,数据依然存在
        charBuffer.clear();
        curPosition(charBuffer);

        //读取数据
        System.out.println(charBuffer);
        curPosition(charBuffer);

        //挨个读取--position与limit之间内容逐个打印
        while(charBuffer.hasRemaining())
        {
            System.out.println(charBuffer.get());
        }
        curPosition(charBuffer);

    }

    //打印当前位置的具体信息
    public static void curPosition(CharBuffer charBuffer)
    {
        System.out.println("当前缓冲区容量capacity: "+charBuffer.capacity()
                +" 当前缓冲区limit位置: "+charBuffer.limit()+
                " 当前缓冲区position指针位置: "+charBuffer.position());
    }
}

输出结果:

当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 0
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 3
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 3 当前缓冲区position指针位置: 0
大忽悠当前缓冲区容量capacity: 12 当前缓冲区limit位置: 3 当前缓冲区position指针位置: 3
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 0
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 3
小
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 3 当前缓冲区position指针位置: 1
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 3 当前缓冲区position指针位置: 0
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 3
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 0
小朋友         
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 0
小
朋
友
 
 
 
 
 
 
 
 
 
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 12

缓冲区批量数据传输

使用缓冲区就是为了提高数据传输效率,一次读写一个字符或者一个字节效率并不高,可以进行批量的操作,可以借助于数组,把缓冲区中的一块数据读到数组中,也可以把数组中的部分内容保存到缓冲区中

public class BufferAPI
{
    public static void main(String[] args) {
        //使用字符缓冲区--一个中文字符和英文字符都是一个字符大小,只是所占字节大小不一样
        CharBuffer charBuffer=CharBuffer.allocate(12);
        //将字符串保存到buffer缓冲区
        charBuffer.put("123456654321");
        //反转读写模式
        charBuffer.flip();//position=0,limit=12
        System.out.println(charBuffer);//只会输出position到limit的内容

        //通过字符数组,来接收缓冲区里面的数据
        char[] dst=new char[8];
        //调用get()方法把缓冲区中的数据读到字符数组中
        //注意批量传输时大小总是固定的,如果没有指定传输的大小,意味着把数组填满
        CharBuffer charBuffer1 = charBuffer.get(dst);
        System.out.println(new String(dst));
        //get方法返回的缓冲区和最开始定义的是同一个
        System.out.println(charBuffer);
        System.out.println(charBuffer1);//缓冲区剩余的数据

        //继续把buf缓冲区的内容读到字符数组中
        //当缓冲区的数据量不足以填满整个数组时,会抛出异常
         //charBuffer.get(dst);//BufferUnderflowException

        //在批量读取缓冲区数据时,记得查询缓冲区的剩余量
        //把小缓冲区的数据填充到大的数组时,要指定缓冲区剩余量的长度
        //把buf缓冲区中剩余的数据传输到dst数组的0开始的位置
      charBuffer.get(dst,0,charBuffer.remaining());
        System.out.println(dst);

        //循环读取缓冲区中的数据
        charBuffer.clear();
        dst=new char[8];
        while(charBuffer.hasRemaining())
        {
            int min = Math.min(dst.length, charBuffer.remaining());
            charBuffer.get(dst,0,min);
            System.out.println(dst);
        }

        curPosition(charBuffer);
        //向缓冲区存入数据的时候,需要注意缓冲区空间不足的情况,否则会抛出异常
        //调整position指针的位置
        charBuffer.position(10);
        //此时还可以存放两个字符
        char[] test={'a','b','c','d'};
        charBuffer.put(test,0,charBuffer.remaining());
        charBuffer.clear();
        System.out.println(charBuffer);
    }

    //打印当前位置的具体信息
    public static void curPosition(CharBuffer charBuffer)
    {
        System.out.println("capacity: "+charBuffer.capacity()
                +" limit: "+charBuffer.limit()+
                " position: "+charBuffer.position());
    }
}

缓冲区创建的两种方式

1.分配创建缓冲区,allocate()方法分配一个私有的,指定容量大小的数组来存储元素

2.包装操作创建缓冲区,它使用提供的数组作为存储空间来存储缓冲区中的数据,不再分配其他空间

public class BufferAPI
{
    public static void main(String[] args) {
        //缓冲区创建的两种方式
        //1.allocate()
        CharBuffer charBuffer1=CharBuffer.allocate(12);
        //2.wrap()
        CharBuffer charBuffer3=CharBuffer.wrap("大忽悠");
        curPosition(charBuffer3);

        char[] array=new char[12];
        CharBuffer charBuffer2=CharBuffer.wrap(array);
        curPosition(charBuffer2);
        
    }

    //打印当前位置的具体信息
    public static void curPosition(CharBuffer charBuffer)
    {
        System.out.println("capacity: "+charBuffer.capacity()
                +" limit: "+charBuffer.limit()+
                " position: "+charBuffer.position());
    }
}

调用put方法对向缓冲区中放入数据会直接影响到数组,同样对数组做任何修改也会直接影响到缓冲区对象

char[] array=new char[12];
        CharBuffer charBuffer2=CharBuffer.wrap(array);
        curPosition(charBuffer2);

        charBuffer2.put("嘿嘿嘿");
        System.out.println(array);
       curPosition(charBuffer2);

        array[4]='小';
        array[5]='朋';
        array[6]='友';
        System.out.println(charBuffer2);
        curPosition(charBuffer2);

hasArray()方法判断是否有一个可存取的备份数组

如果hasArray()返回true,可以通过array()返回缓冲区对象使用的备份数组的引用

char[] array=new char[12];
        CharBuffer charBuffer2=CharBuffer.wrap(array);
        charBuffer2.put("嘿嘿嘿");

        if(charBuffer2.hasArray())
        {
            char[] array1 = charBuffer2.array();
            System.out.println(array1);
        }

缓冲区的复制与分隔

复制操作

public class BufferAPI
{
    public static void main(String[] args)
    {
       CharBuffer charBuffer=CharBuffer.wrap("123456");
       //缓冲区的复制
        CharBuffer duplicate = charBuffer.duplicate();
        System.out.println(duplicate);
        //下面可以看出两个缓冲区只用数组是同一个,两个缓冲区对象是不同的对象
        //两个缓冲区实际上引用的是同一个数组
        duplicate.position(4);
        curPosition(duplicate);
        curPosition(charBuffer);
    }

    //打印当前位置的具体信息
    public static void curPosition(CharBuffer charBuffer)
    {
        System.out.println("capacity: "+charBuffer.capacity()
                +" limit: "+charBuffer.limit()+
                " position: "+charBuffer.position());
    }
}

缓冲区的复制,本质是两个不同的缓冲区对象共用一个缓冲数组

分隔操作

分隔缓冲区,slice()方法根据[position.limit)区间创建一个新的缓冲区

public class BufferAPI
{
    public static void main(String[] args)
    {
       CharBuffer charBuffer=CharBuffer.wrap("123456");
       charBuffer.position(5);
        CharBuffer slice = charBuffer.slice();
        curPosition(charBuffer);
        curPosition(slice);
    }

    //打印当前位置的具体信息
    public static void curPosition(CharBuffer charBuffer)
    {
        System.out.println("capacity: "+charBuffer.capacity()
                +" limit: "+charBuffer.limit()+
                " position: "+charBuffer.position());
    }
}

直接字节缓冲区

在硬盘中和操作系统处理的数据都是01二进制,缓冲区中只有ByteBuffer字节缓冲区由资格参与I/O操作。

Channel通道只能使用ByteBuffer作为它的参数。

直接字节缓冲区通常是I/O操作最好的选择,如果使用非直接字节缓冲区可能会导致性能损耗,如果向通道传递一个非直接字节缓冲区,通道可能会先创建一个临时的直接字节缓冲区,将非直接缓冲区的内容复制到这个临时的直接字节缓冲区执行底层的I/O操作。

直接缓冲区时I/O的最佳选择,可能创建直接缓冲区比创建非直接缓冲区的成本要高。直接缓冲区使用的内存是通过调用本地操作系统代码分配的,绕过了JVM的堆栈,现在JVM可能会执行缓冲区的缓存的优化.

ByteBuffer.allocateDirect()方法创建直接字节缓冲区

Channel

Channel是一种新的I/O访问方式,用于在字节缓冲区与通道另一侧的实体(可以是文件也可以是Socket)之间进行传输数据。

Channel可以双向读写数据,也开业实现异步读写。

程序不能直接访问Channel,Channel只能与Buffer缓冲区进行交互,即把通道中的数据读到Buffer缓冲区中,程序从缓冲区中读取数据;

在写操作时,程序把数据写入Buffer缓冲区中,再把缓冲区的数据写入到Channel中.

常用的Channel:

  • FileChannel:读写文件的通道
  • SocketChannel/ServerSocketChannel:读写Socket套接字中的数据
  • DatagramChannel:通过UDP读写网络数据。

注意:

1.通道不能通过构造方法创建

2.通道不能重复使用,一旦创建了一个通道,就表示一个特定的I/O服务建立了一个连接,一旦通道关闭,连接就没了

3.通道可以以默认阻塞的方式运行,也可设置为非阻塞方式运行

Scatter与Gather

Scatte(发散),Gather(聚焦)是通道提供的一个重要功能(有时也成为矢量I/O)

Scatte,Gather是指在多个缓冲区中实现一个简单的I/O操作。

Scatter是指从channel 通道中读取数据,把这些数据按顺序分散写入到多个Buffer缓冲区中

Gather是指在写操作时,将多个Buffer缓冲区的数据写入到同一个Channel中

Scatter,Gather经常用于需要将传输的数据分开处理的场景

如Scatter从一个Channel中读取数据存储到多个Buffer:

//示例伪代码
ByteBuffer c1=ByteBuffer.allocate(3);
ByteBuffer c2=ByteBuffer.allocate(3);
ByteBuffer[] bufArray={buf1,buf2}
Channel.read(bufArray);

Read()方法从Channel中读取数据,按照在数组中的顺序依次存储到缓冲区中。

注意必须在buf1缓冲区满后才能写入buf2缓冲区中。

使用Scatter/Gather处理的数据大小都是固定的

对于不确定数据大小的情况下,不适合用

FileChannel

FileChannel通过RandomAccessFile,FileInputStream,FileOutputStream对象调用getChannel()方法调用。

FileChannel虽然是双向的,既可以读也可以写,但是从FileInputStream流中获得的通道只能读不能写,如果进行写操作会抛出异常;从FileOutputStream流中获得的通道只能写不能读;如果访问的文件时只读的也不能执行写操作。

FileChannel是线程安全的,但是并不是所有的操作都是多线程的,如影响通道位置或者影响文件大小的操作都是单线程的。

1.内存映射文件

FileChannel的map()方法可以把磁盘上的文件整个的映射到计算机的虚拟内存,把这块虚拟内存包装为一个MappedByteBuffer对象,注意: 当前把一个文件映射到虚拟内存中,文件的内容通常不会从硬盘读取到内存。

通过内存映射完成文件的拷贝操作:

public class BufferAPI
{
  //当前项目的路径
  private static final String project_path=System.getProperty("user.dir")+System.getProperty("file.separator");
  //源文件
  private final static File src=new File(project_path+"src.txt");
  //目的文件
  private final static File dst=new File(project_path+"tar.txt");

    public static void main(String[] args)
    {
       //把src.txt以内存映射的方式读取到tar.txt

      //使用try-resources写法
      try(
              FileChannel fin=new FileInputStream(src).getChannel();
              FileChannel fout=new FileOutputStream(dst).getChannel();
              ) {
          //将fin通道里面的数据映射到虚拟内存中
          //参数一:映射方式,可读还是可写
          //参数二,参数三:从某个位置开始映射,映射多少个字节
          MappedByteBuffer mapBuffer = fin.map(FileChannel.MapMode.READ_ONLY, 0, src.length());
          //将缓冲区中的数据输出到fout通道中
          fout.write(mapBuffer);

          //也可以把buffer里面的内容打印出来
          mapBuffer.flip();//转换为读模式
          //创建字符集
          Charset charset = Charset.defaultCharset();
          //使用默认字符集将buf里面的字节转换为字符
          CharBuffer decode = charset.decode(mapBuffer);
          System.out.println("解码后的buffer: "+decode);
      } catch (FileNotFoundException e) {
          e.printStackTrace();
      } catch (IOException e) {
          e.printStackTrace();
      }
    }
}

2.FileChannel的双向传输

使用RandomAccessFile获得的通道是双向传输的

将tar.txt文件的内容全部读取出来,然后再次追加到该文件的尾部

public class BufferAPI
{
  //当前项目的路径
  private static final String project_path=System.getProperty("user.dir")+System.getProperty("file.separator");

    public static void main(String[] args)
    {
        //使用try-resources写法
      try(
              RandomAccessFile dst=new RandomAccessFile(project_path+"tar.txt","rw");
              FileChannel fout=dst.getChannel();
              )
      {
          //把channel中的数据映射到虚拟内存中
          MappedByteBuffer mappedByteBuffer = fout.map(FileChannel.MapMode.READ_ONLY, 0, dst.length());
          //设置channel的position操作
          fout.position(dst.length());
          //把缓冲区数据写入到channel中
          fout.write(mappedByteBuffer);
      } catch (FileNotFoundException e) {
          e.printStackTrace();
      } catch (IOException e) {
          e.printStackTrace();
      }
    }
}

channel只有position,没有limit和capacity,因为这两个属性是针对缓冲区而言的

3.Filechannel读写文件时,缓冲区固定大小
public class BufferAPI
{
  //当前项目的路径
  private static final String project_path=System.getProperty("user.dir")+System.getProperty("file.separator");
    //源文件
    private final static File src=new File(project_path+"src.txt");
    //目的文件
    private final static File dst=new File(project_path+"dst.txt");

    public static void main(String[] args)
    {
        //使用try-resources写法
      try(
         FileChannel fin=new FileInputStream(src).getChannel();
         FileChannel fout=new FileOutputStream(dst).getChannel();
         )
      {
        //定义固定大小的缓冲区
          ByteBuffer byteBuffer=ByteBuffer.allocate(48);
          //不断的从通道中读取数据放入缓冲区中
          while(fin.read(byteBuffer)!=-1)
          {
              byteBuffer.flip();//缓冲区从写模式切换为读模式
              while(byteBuffer.hasRemaining())
              {
                  //从缓冲中读取数据放入通道中
                  fout.write(byteBuffer);
              }
          }
          byteBuffer.clear();
      } catch (FileNotFoundException e) {
          e.printStackTrace();
      } catch (IOException e) {
          e.printStackTrace();
      }
    }
}

文件不存在,会创建文件

4.通道与通道之间的传输

经常需要把文件从一个位置批量传输到另一个位置,可以直接使用通道到通道之间的传输,不需要中间缓冲区传递数据.

注意,只有FileChannel支持通道到通道之间的传输.

通道到通道的传输非常快速,有的操作系统可以不使用用户空间直接传输数据。

public class BufferAPI
{
  //当前项目的路径
  private static final String project_path=System.getProperty("user.dir")+System.getProperty("file.separator");
    public static void main(String[] args)
    {
        //使用try-resources写法
      try(
              RandomAccessFile src= new RandomAccessFile(project_path+"src.txt", "rw");
              FileChannel fin=src.getChannel();
            FileChannel fout=new FileOutputStream(project_path+"dst.txt").getChannel();
              )
      {
          //transferTo--将当前通道内的数据传输到另一个通道中去
          //参数一:从当前通道的哪一个位置进行数据传输
          //参数二:传输多少个字节数据
          //参数三:目的地的通道
          //fin.transferTo(0,src.length(),fout);

          //transform
          //参数一:源通道
          //参数二和三对应上面:从原通道的某个位置,传输多少个字节到现在的通道
        fout.transferFrom(fin,0,src.length());

      } catch (FileNotFoundException e) {
          e.printStackTrace();
      } catch (IOException e) {
          e.printStackTrace();
      }
    }
}
5.Gather实现

把文件的属性和文件的内容分别存储到不同的缓冲区中,再写入到另外一个文件中

public class BufferAPI
{
  //当前项目的路径
  private static final String project_path=System.getProperty("user.dir")+System.getProperty("file.separator");
  //换行符
  private static final String line_separator=System.getProperty("line.separator");
  //当前类的.java文件
  private static final String curJavaFilePath=project_path+"src\\com\\NIO\\BufferAPI.java";

    public static void main(String[] args)
    {
        File file=new File(curJavaFilePath);

        //获取文件绝对路径
        String absolutePath = file.getAbsolutePath();
        //文件最后一次修改的时间
        long lastModified = file.lastModified();
        String formatDate = formatDate(lastModified);
        //把文件属性存储在缓冲区中
        String headerText="file path: "+absolutePath+" "+line_separator
                +"file modified: "+formatDate+line_separator;
        ByteBuffer headerBuffer=ByteBuffer.wrap(headerText.getBytes());

        //存放文件内容类型
        ByteBuffer contentBuffer=ByteBuffer.allocate(1024);

        //创建一个缓冲区数组
        ByteBuffer[] gather={headerBuffer,contentBuffer,null};

        String contentType="unkonwn";
        Long length=-1l;

        //把文件映射到虚拟内存中
        try (FileChannel channel = new FileInputStream(file).getChannel();)
        {
            //把文件映射到虚拟内存中
            MappedByteBuffer fileData = channel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
            //把映射到内存上的文件内容保存到gather数组中
            gather[2]=fileData;

            //文件长度
            length=file.length();
            //返回当前文件的类型
            String name = URLConnection.guessContentTypeFromName(file.getAbsolutePath());
            StringBuilder stringBuilder=new StringBuilder();
            String string = stringBuilder.append("file Len: " + length+line_separator)
                    .append("contentType: " + name+line_separator).toString();
            gather[1]=contentBuffer.put(string.getBytes());
            //反转读写模式
            contentBuffer.flip();
            //让gather数组里面的内容写入到文件中
            FileChannel fileChannel = new FileOutputStream(project_path + "dst.txt").getChannel();
            //write返回值如果是0表示写完了,如果不是0则还有数据没写完
            while(fileChannel.write(gather)>0)
            {}
            //写完了,关闭通道
            fileChannel.close();
        } catch (FileNotFoundException e)
        {
            addErrorMessageToFile(gather,e.getMessage());
        } catch (IOException e) {
            addErrorMessageToFile(gather,e.getMessage());
        }
    }

    //如果文件操作出现异常,将出现的异常保存到错误日志中去
    public static void addErrorMessageToFile(ByteBuffer[] gather,String error)
    {
        ByteBuffer wrap = ByteBuffer.wrap(error.getBytes());
        gather[2]=wrap;
    }

   //时间格式化
   public static String formatDate(Long epochSecond)
   {
         Date date=new Date(epochSecond);
         DateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
       String format = dateFormat.format(date);
       return format;
   }
}

效果:

socketChannel和serverSocketChannel

ServerSocketChannel可以监听新进来的TCP连接通道

SocketChannel是一个连接到TCP网络套接字的通道

服务器端代码—serverSocketChannel实现

public class DHYServerSocketChannel
{
    //服务器端的端口号
    private static Integer Default_Port=80;

    public static void main(String[] args) throws IOException, InterruptedException {
        //建立一个未绑定的ServerScoket服务器的通道
        java.nio.channels.ServerSocketChannel ssc = java.nio.channels.ServerSocketChannel.open();
        //ServerSocketChannel没有bind绑定方法,需要先通过socket()方法获得ServerSocket对象,然后在进行端口号的绑定操作
        ssc.socket().bind(new InetSocketAddress(Default_Port));
        //设置通道为非阻塞模式,当没有socket没有传入连接时,accept()方法返回null
        ssc.configureBlocking(false);

        while(true)
        {
            java.nio.channels.SocketChannel socketChannel = ssc.accept();
            //如果没有连接,socketChannel=null
            if(socketChannel==null)
            {
                Thread.sleep(2000);
            }
            else
            {
                //有连接传入
                //先给客户端发送一个问候
                ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
                byteBuffer.put("你好,我是服务器".getBytes());
                byteBuffer.flip();
                socketChannel.write(byteBuffer);
                //再读取客户端中发送来的内容
                System.out.println("客户端["+socketChannel.socket().getRemoteSocketAddress()+"]");
                byteBuffer.clear();
                //读取客户端发送的数据,保存到buffer中
                socketChannel.read(byteBuffer);
                byteBuffer.flip();//切换为读模式
                //解码
                Charset charset = Charset.defaultCharset();
                //解码后生产一个字符缓冲区
                CharBuffer decode = charset.decode(byteBuffer);
                System.out.println("客户端消息: "+decode);
                //关闭服务器与客户端之间的连接
                socketChannel.close();
                break;
            }
        }
    }
}

客户端端代码—SocketChannel实现

public class DHYSocketChannel
{
    //serverSocket的IP地址---即服务器的ip地址
    private  static String HOST_IP="localhost";
    //serverSocket注册的端口号
    private static Integer HOST_PORT=80;
    public static void main(String[] args) throws IOException {
        InetSocketAddress address=new InetSocketAddress(HOST_IP, HOST_PORT);
        //创建一个未连接的SocketChannel
        SocketChannel socketChannel = SocketChannel.open();
        //建立与服务器的连接
        socketChannel.connect(address);
        //TCP连接需要一定时间,两个连接的建立需要进行包对话
        //调用finishConnect()方法完成连接过程,如果没有连接成功返回false
        while(!socketChannel.finishConnect())
        {
            System.out.println("等待连接中...");
        }
        System.out.println("连接成功");
         //向服务器发送消息
        ByteBuffer buffer=ByteBuffer.wrap("你好,我是客户端".getBytes());
        while(buffer.hasRemaining())
        {
            socketChannel.write(buffer);
        }
        //获得服务器发送给客户端的消息
        InputStream inputStream = socketChannel.socket().getInputStream();
        ReadableByteChannel readableByteChannel = Channels.newChannel(inputStream);//channels工具类获得通道
        buffer.clear();
        readableByteChannel.read(buffer);
        buffer.flip();//切换读模式
        //解码
        Charset charset = Charset.defaultCharset();
        CharBuffer decode = charset.decode(buffer);
        System.out.println(decode);
    }
}

DatagramChannel

DatagramChannel是收发UDP包的通道,与TCP协议不同,UDP发送不进行连接,也不对确认数据是否收到。

ByteBuffer receiveBuffer = ByteBuffer.allocate(64);
receiveBuffer.clear();
SocketAddress receiveAddr = server.receive(receiveBuffer);

SocketAddress可以获得发包的ip、端口等信息,用toString查看,格式如下

/127.0.0.1:57126

连接

udp不存在真正意义上的连接,这里的连接是向特定服务地址用read和write接收发送数据包。

client.connect(new InetSocketAddress("127.0.0.1",10086));
int readSize= client.read(sendBuffer);
server.write(sendBuffer);

read()和write()只有在connect()后才能使用,不然会抛NotYetConnectedException异常。用read()接收时,如果没有接收到包,会抛PortUnreachableException异常。

数据接收端–receiver

public class DHYDatagramReceiver
{
    public static void main(String[] args) throws IOException, InterruptedException {
        //创建一个未绑定的通道
        DatagramChannel datagramChannel=DatagramChannel.open();
        //绑定一个端口号
        datagramChannel.bind(new InetSocketAddress(80));
        //设置为非阻塞
        datagramChannel.configureBlocking(false);

        //接收键盘数据
        Scanner sc=new Scanner(System.in);
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        //判断是否接受到数据
         while(true)
         {
             //先接收数据
             buffer.clear();
             //通过receive()接收udp包---从通道中接收数据,放入缓冲区中
             InetSocketAddress receive = (InetSocketAddress) datagramChannel.receive(buffer);
             //判断是否有接收到数据
             if(receive==null)
             {
                 Thread.sleep(2000);
                 continue;
             }
             System.out.print("数据来自: "+
                     receive);
             //切换读模式
             buffer.flip();
             String s = new String(buffer.array(), 0, buffer.limit());
             System.out.println(" :"+receive.getPort()+"---->"+s);

             //发送数据
             String text=sc.nextLine();
             //发送的数据,发送给谁
             datagramChannel.send(ByteBuffer.wrap(text.getBytes()),receive);
         }

    }
}

数据发送端—sender

public class DHYDatagramSender
{
    public static void main(String[] args) throws IOException {
        //创建未绑定的channel
        DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.configureBlocking(false);

        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNext())
        {
            String nextLine = scanner.nextLine();
            byteBuffer.clear();
            byteBuffer.put(nextLine.getBytes());
            byteBuffer.flip();
            //发送的数据,发送给谁
            datagramChannel.send(byteBuffer,new InetSocketAddress("localhost",80));

            //接收数据
            byteBuffer.clear();
            SocketAddress receive = datagramChannel.receive(byteBuffer);
            while (receive==null)
            {
                receive=datagramChannel.receive(byteBuffer);
            }
            byteBuffer.flip();
            System.out.println(new String(byteBuffer.array(),0,byteBuffer.limit()));
        }
    }
}

Pipe

pipe管道用于在两个线程之间进行单向的数据连接.

pipe有一个source通道和一个sink通道

创建管道: Pipe pipe=Pipe.open();

向管道中写数据,首先需要访问sink通道

Pipe.SinkChannel sc=pipe.sink();

sc.write(buffer);

读数据需要通过source通道

Pipe.SourceChannel source=pipe.source();

source.read(buffer);

使用演示:

public static void test() throws IOException {
            // 1. 获取管道
            Pipe pipe = Pipe.open();

            // 2. 将缓冲区数据写入到管道
            // 2.1 获取一个通道
            Pipe.SinkChannel sinkChannel = pipe.sink();
            // 2.2 定义缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(48);
            buffer.put("发送数据".getBytes());
            buffer.flip(); // 切换数据模式
            // 2.3 将数据写入到管道
            sinkChannel.write(buffer);

            // 3. 从管道读取数据
            Pipe.SourceChannel sourceChannel = pipe.source();
            buffer.flip();
            int len = sourceChannel.read(buffer);
            System.out.println(new String(buffer.array(), 0, len));

            // 4. 关闭管道
            sinkChannel.close();
            sourceChannel.close();
        }

演示在两个线程之间使用pipe管道进行数据的传输:

使用PipedOutPutStream和PipedInputStream两个类分别是管道输出流和管道输入流类

在管道通信时,线程A向PipedOutPutStream中写入数据,这些数据会自动发送到对应的PipedInputStream中

线程B可以从PipedInputStream中读取数据

public class DHYPipe
{
    public static void main(String[] args) throws IOException {
        //创建输入流管道
        PipedInputStream in=new PipedInputStream();
        //创建输出流管道
        PipedOutputStream out=new PipedOutputStream();
        //在输入流管道和输出流管道之间建立连接
        in.connect(out);
        //或者
        //out.connect(in);

        //创建线程
        new Thread(new Sender(out)).start();
        new Thread(new Receiver(in)).start();
    }
}

//发送端
class Sender implements Runnable{
 PipedOutputStream out;

    public Sender(PipedOutputStream out) {
        this.out = out;
    }

    @Override
    public void run() {
        //模拟发送数据
            try
            {
                for(int i=0;i<10;i++) {
                    out.write(("你好第" + i + "个大忽悠\n").getBytes(StandardCharsets.UTF_8));
                }

            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    }
}

//接收端
class Receiver implements Runnable
{
 PipedInputStream in;

    public Receiver(PipedInputStream in) {
        this.in = in;
    }

    @Override
    public void run() {
        //接收数据
        byte[] bytes=new byte[1024];

        try {
            int len;
            while((len=in.read(bytes))!=-1)
            {
                System.out.println(new String(bytes,0,len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

使用sinkChannel和sourceChannel完成演示

public class DHYPipe
{
    public static void main(String[] args) throws IOException {
       //获取管道
        Pipe pipe=Pipe.open();
        //创建线程
        new Thread(new Sender(pipe.sink())).start();
        new Thread(new Receiver(pipe.source())).start();
    }

}

//发送端
class Sender implements Runnable{
    Pipe.SinkChannel sinkChannel;

    public Sender(Pipe.SinkChannel sinkChannel) throws IOException {
        this.sinkChannel = sinkChannel;
        //设置为非阻塞
        sinkChannel.configureBlocking(false);
    }

    @Override
    public void run() {
        //模拟发送数据
            try
            {
                sinkChannel.write(ByteBuffer.wrap("我是大忽悠".getBytes()));
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    sinkChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    }
}

//接收端
class Receiver implements Runnable
{
 Pipe.SourceChannel sourceChannel;

    public Receiver(Pipe.SourceChannel sourceChannel) {
        this.sourceChannel = sourceChannel;
    }

    @Override
    public void run() {

        try {
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            sourceChannel.read(byteBuffer);
            byteBuffer.flip();
            Charset charset = Charset.defaultCharset();
            CharBuffer decode = charset.decode(byteBuffer);
            System.out.println(decode);
        } catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            try {
                sourceChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

补充知识点整理

HTTP,状态码,TCP、UDP等网络协议

图解HTTP,状态码,TCP、UDP等网络协议相关总结(持续更新)

UrlConnection

URLConnection

Channels

Channels

相关文章