NIO

NIO

Java在1.4版本开始,引入了NIO,称为Java New IO。又称老的阻塞IO为OIO(Old IO)。

NIO与OIO对比:

  1. OIO是面向流,操作的是字节。NIO引入了Buffer和Channel,操作的是缓冲区。

  2. OIO是阻塞的,NIO是非阻塞的

  3. OIO没有Selector的概念

NIO的三大组件如下所示。

Buffer

数据存储的媒介。

Buffer其实是对数组的包装,定义了一些属性来保证同时读写。有ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer一些属性fer,ShortBuffer,MappedByteBuffer。使用最多的是ByteBuffer。Buffer的缓冲区没有定义在Buffer类中,而是定义在子类中。如ByteBuffer中定义了byte[]作为缓冲区。

为了记录读写的位置,Buffer类提供了一些重要属性。

  1. capacity
    缓冲区的容量,表示Buffer类能存储的数据大小,一旦初始化就不能改变。不是表示字节数,而是表示能存储的数据对象数。比如CharBuffer的capacity是100,能100个char。

  2. position
    Buffer类有两种模式:

  • 读模式:表示读取数据的位置。创建Buffer时为0,读取数据后移动到下一个位置,limit为读取的上限,当position的值为limit时,表示没有数据可读。

  • 写模式:表示下一个可写的位置。创建Buffer时为0,写入数据后移动到下一个位置,capacity为写入的上限,当position的值为capacity时,表示Buffer没有空间可写入。

读写模式怎么切换呢?刚创建Buffer时为写模式,写入数据后要想读取,必须切换为读模式,可以调用flip方法:此时会将limit设置为写模式的position值,表示可以读取的最大位置。position设为0,表示从头开始读取。

写模式:

读模式:

3.limit

  • 读模式:读取的最大位置

  • 写模式:可写入的最大上限。

4.mark
调用mark()方法将position设置到mark中,再调用reset()方法将mark的值恢复到position属性中。重新从position读取。

重要方法

allocate

allocate创建一个Buffer并分配空间:

@Test
    public void testAllocate() {
        IntBuffer intBuffer = IntBuffer.allocate(20);

        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());
    }

可以看到创建缓冲区后处于写模式,初始position为0,limit和capacity为allocate传入的参数。

put

创建Buffer后,可以调用put往Buffer中填充数据,只要保证数据的类型和Buffer的类型保持一致。

    @Test
    public void testPut() {
        IntBuffer intBuffer = IntBuffer.allocate(20);

        for (int i = 0; i < 5; i++) {
            intBuffer.put(i);
        }

        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());
    }

写入5个元素后,position变为5,指向第6个位置,而limit,capacity没有变化。

flip

写入数据后不能直接读取,而是需要调用flip转换为读模式。

    @Test
    public void testFlip() {
        IntBuffer intBuffer = IntBuffer.allocate(20);

        for (int i = 0; i < 5; i++) {
            intBuffer.put(i);
        }

        intBuffer.flip();

        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());
    }

可以看到,flip切换为读模式后,position设为0,limit的值是之前写入的position的值。同时清除mark标记。查看flip源码:

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

不然,切换为读取后,position的值已修改,不清除mark标记会导致position混乱。

那么,读取完成后怎么切换为写模式,继续写入呢?可以调用clear清空Buffer或compact压缩Buffer。

clear
    @Test
    public void testClear() {
        IntBuffer intBuffer = IntBuffer.allocate(20);

        for (int i = 0; i < 5; i++) {
            intBuffer.put(i);
        }

        intBuffer.flip();

        System.out.println("读取数据并处理");
        while (intBuffer.hasRemaining()) {
            int i = intBuffer.get();
            System.out.println(i);
        }
        System.out.println("读取数据完成");

        System.out.println();
        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());
        System.out.println();


        System.out.println("清空Buffer");
        intBuffer.clear();
        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());
    }

可以看到调用clear后,Buffer的状态和allocate时的状态一致。

compact
	    @Test
    public void testCompact() {
        IntBuffer intBuffer = IntBuffer.allocate(20);

        for (int i = 0; i < 5; i++) {
            intBuffer.put(i);
        }

        intBuffer.flip();

        System.out.println("读取数据并处理");
        for (int i = 0; i < 2; i++) {
            System.out.println(intBuffer.get());
        }
        System.out.println("读取数据完成");

        System.out.println();
        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());
        System.out.println();


        System.out.println("压缩Buffer");
        intBuffer.compact();
        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());
    }

看到调用compact后会将position到limit范围内(不包含limit位置)的元素往缓冲区的头部移动。上面的例子中读取后剩余3个元素,compact后position的值为3.

get

当调用flip切换为读模式后,就可以调用get获取数据了。

@Test
    public void testGet() {
        IntBuffer intBuffer = IntBuffer.allocate(20);

        for (int i = 0; i < 5; i++) {
            intBuffer.put(i);
        }

        intBuffer.flip();

        System.out.println("切换flip");
        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());


        System.out.println(intBuffer.get());

        System.out.println("调用get()后");
        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());

        System.out.println(intBuffer.get(1));

        System.out.println("调用get(i)后");
        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());
    }

看出调用get()获取数据后会改变position的值,而调用get(int i)则不会改变position的值

rewind

读取完所有数据后,可以重新读取吗?可以调用rewind。

@Test
public void testRewind() {
    IntBuffer intBuffer = IntBuffer.allocate(20);

    for (int i = 0; i < 5; i++) {
        intBuffer.put(i);
    }

    intBuffer.flip();

    System.out.println("切换flip");
    System.out.println("position:"+intBuffer.position());
    System.out.println("limit:"+intBuffer.limit());
    System.out.println("capacity:"+intBuffer.capacity());

    System.out.println("读取数据并处理");
    while (intBuffer.hasRemaining()) {
        System.out.println(intBuffer.get());
    }
    System.out.println("读取数据完成");
    System.out.println("position:"+intBuffer.position());
    System.out.println("limit:"+intBuffer.limit());
    System.out.println("capacity:"+intBuffer.capacity());

    System.out.println();

    intBuffer.rewind();
    System.out.println("调用rewind后");
    System.out.println("position:"+intBuffer.position());
    System.out.println("limit:"+intBuffer.limit());
    System.out.println("capacity:"+intBuffer.capacity());
}

可以看到调用rewind后会将position设置为0,limit不变。查看JDK源码:

    public Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }

发现会清除mark标记。

mark()和reset()

mark()将当前的position保存到mark属性中,reset()将mark属性设置到position,可以从position开始读取了。

@Test
    public void testMarkReset() {
        IntBuffer intBuffer = IntBuffer.allocate(20);

        for (int i = 0; i < 5; i++) {
            intBuffer.put(i);
        }

        intBuffer.flip();

        System.out.println("切换flip");
        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());

        System.out.println("读取数据并处理");
        int j = 0;
        while (intBuffer.hasRemaining()) {
            if (j == 3) {
                intBuffer.mark();
            }
            System.out.println(intBuffer.get());
            j++;
        }
        System.out.println("读取数据完成");
        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());
        System.out.println();

        intBuffer.reset();
        System.out.println("调用reset后");
        System.out.println("position:"+intBuffer.position());
        System.out.println("limit:"+intBuffer.limit());
        System.out.println("capacity:"+intBuffer.capacity());
    }

调用reset后将position重置为mark属性了。

Channel

数据传输的媒介。一个Channel表示一个Socket连接。更通用的来说,一个Channel表示一个文件描述符,可以表示文件,网络连接,硬件设备等。最重要的Channel有四种:FileChannel,SocketChannel,ServerSocketChannel,DatagramChannel。

FileChannel

文件通道,用于文件的读写。

读取

首先通过FileInputStream.getChannel()获取FileChannel,然后调用int read(ByteBuffer dst)方法读取数据到ByteBuffer中并返回读到的字节数。

@Test
public void testGetFileChannel() {
    try(FileInputStream fileInputStream = new FileInputStream(filePath)) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        FileChannel channel = fileInputStream.getChannel();
        int read = channel.read(byteBuffer);
        if (read == -1) {
            return;
        }
        System.out.println("读到"+read+"字节");
        byteBuffer.flip();
        byte[] bytes = new byte[read];
        byteBuffer.get(bytes);
        System.out.println("数据是:" + new String(bytes));
        channel.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

}

写入

首先通过FileInputStream.getChannel()获取FileChannel,然后调用int read(ByteBuffer dst)方法读取数据到ByteBuffer中并返回读到的字节数。

  @Test
    public void testWrite() {
        try(FileOutputStream fileOutputStream = new FileOutputStream(filePath)) {
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            byteBuffer.put("你好啊".getBytes(StandardCharsets.UTF_8));
            FileChannel channel = fileOutputStream.getChannel();
            byteBuffer.flip();
            int len;
            while ((len = channel.write(byteBuffer)) >0) {
                System.out.println("写入"+len+"字节");
            }

            channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

写入成功后,查看文件内容:

RandomAccessFile

首先通过RandomAccessFile.getChannel()获取FileChannel,然后调用int read(ByteBuffer dst)方法读取数据到ByteBuffer中并返回读到的字节数。

@Test
public void testRandomAccessFile() {
    try(RandomAccessFile randomAccessFile = new RandomAccessFile(filePath,"r")) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        FileChannel channel = randomAccessFile.getChannel();
        int read = channel.read(byteBuffer);
        if (read == -1) {
            return;
        }
        System.out.println("读到"+read+"字节");
        byteBuffer.flip();
        byte[] bytes = new byte[read];
        byteBuffer.get(bytes);
        System.out.println("数据是:" + new String(bytes));
        channel.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

关闭

使用完Channel后调用close方法关闭通道。

强制刷新到磁盘

通过write写入数据后,数据先保存到缓冲区中,要保证数据写入磁盘,必须调用channel.force将数据刷新到磁盘。

例子

看个简单例子,通过Channel复制文件。

@Test
public void testCopyFile() {
    String srcFilePath = "/home/shigp/图片/1.jpg";
    String destFilePath = "/home/shigp/图片/2.jpg";
    try (FileInputStream fileInputStream = new FileInputStream(srcFilePath);
        FileOutputStream fileOutputStream = new FileOutputStream(destFilePath)) {

        Path path = Path.of(destFilePath);
        if (!Files.exists(path)) {
            Files.createFile(path);
        }

        FileChannel srcChannel = fileInputStream.getChannel();
        FileChannel destChannel = fileOutputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);

        int len;

        while((len = srcChannel.read(byteBuffer)) > 0) {
            byteBuffer.flip();

            int size;

            while((size = destChannel.write(byteBuffer)) > 0) {

            }

            byteBuffer.clear();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

除了Channel的操作外,还需注意Buffer的读写状态切换。以上的效率不是很高,可以使用FileChannel.transferTo方法。

@Test
public void testTransferTo() {

    String srcFilePath = "/home/shigp/图片/1.jpg";
    String destFilePath = "/home/shigp/图片/2.jpg";
    try (FileInputStream fileInputStream = new FileInputStream(srcFilePath);
         FileOutputStream fileOutputStream = new FileOutputStream(destFilePath)) {

        Path path = Path.of(destFilePath);

        if (!Files.exists(path)) {
            Files.createFile(path);
        }

        File file = new File(srcFilePath);
        FileChannel srcChannel = fileInputStream.getChannel();
        FileChannel destChannel = fileOutputStream.getChannel();

        for (long count = srcChannel.size(); count > 0; ) {
            long transfer = srcChannel.transferTo(srcChannel.position(), count, destChannel);
            count  -= transfer;
        }

       destChannel.force(true);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
SocketChannel,ServerSocketChannel

涉及网络连接的通道有两个,一个是SocketChannel,负责数据的传输,对应OIO中的Socket; 一个是ServerSocketChannel,负责监听连接,对应OIO中的ServerSocket。

SocketChannel和ServerSocketChannel都支持阻塞和非阻塞两种模式。都是通过调用configureBlocking方法实现的,参数false表示设置为非阻塞模式,参数true表示设置为阻塞模式。

SocketChannel通过read从Channel中读取数据到Buffer。通过write将Buffer中的数据写入到Channel中。在关闭Channel前,可以通过调用shutdownOutput终止输出,发送一个输出结束标志。在调用close关闭通道。

DatagramChannel

DatagramChannel也可以通过调用configureBlocking(false)将Channel设置为非阻塞模式。

DatagramChannel调用receive从Channel中读取数据到Buffer中。通过send将Buffer中的数据发送到Channel中。但是要指定接收数据的IP和端口,因为UDP是面向无连接的。调用close关闭Channel。

Selector

一个线程管理多个Socket连接。Selector的作用是完成IO多路复用,通道的注册,监听,事件查询。通道通过register的方式注册到Selector中。register方法有两个参数:第一个参数是要注册到的Selector,第二个参数是Channel感兴趣的IO事件类型:

  • SelectionKey.OP_READ:可读
  • OP_WRITE:可写
  • OP_CONNECT:建立连接
  • OP_ACCEPT:接收

如果要注册多个事件类型,每个事件类型用或运算符(|)连接。

那是不是什么类型的Channel都可以注册到Selector中?并不是。只有继承了SelectableChannel的通道才能被注册到Selector中,且Channel不能被设置为阻塞。

Channel注册到Selector后,可以调用Selector的select方法获取已被Selector监控且IO已就绪的IO事件及相应的通道,也就是SelectionKey。通过SelectionKey可以获取到已就绪的IO事件类型,发生的Channel。然后根据IO事件类型做不同的处理。处理完后将SelectionKey移除,避免下次重复调用。

SElector的select方法有多个重载版本:

  • select():阻塞调用,直到有一个Channel发生了感兴趣的IO事件
  • select(long timeout):和select() 一样,但是最长阻塞时间为timeout毫秒
  • selectNow():非阻塞,不管是否发生感兴趣的IO事件都立刻返回。

例子,实现一个Discard服务器和客户端

Discard服务器仅接收客户端通道的数据并直接丢弃,然后关闭客户端。

public class DiscardServer {
    public static void startServer() throws Exception{
        // TCP服务端
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress("127.0.0.1",10880));


        DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.configureBlocking(false);
        datagramChannel.bind(new InetSocketAddress("127.0.0.1", 10890));


        Selector selector = Selector.open();

        // 将通道注册到Selector中,并设置感兴趣的IO事件为accept,也就是接收客户端连接
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        datagramChannel.register(selector, SelectionKey.OP_READ);


        // 发生了注册的感兴趣的IO事件
        while (selector.select() > 0) {
            // 获取感兴趣的IO事件集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();

            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();

                if (selectionKey.isAcceptable()) {
                   //连接客户端
                    ServerSocketChannel sChannel = (ServerSocketChannel) selectionKey.channel();

                    SocketChannel channel = sChannel.accept();
                    // 设置为非阻塞
                    channel.configureBlocking(false);
                    // 将客户端通道注册到Selector中,设置感兴趣的IO事件为read
                    channel.register(selector, SelectionKey.OP_READ);

                } else if (selectionKey.isReadable()) {
                    // 从客户端通道中读取
                    SelectableChannel selectableChannel = selectionKey.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

                    if (selectableChannel instanceof SocketChannel) {
                        SocketChannel channel = (SocketChannel) selectableChannel;

                        int len;
                        while ((len = channel.read(byteBuffer)) > 0) {
                            byteBuffer.flip();
                            System.out.println("读取的数据是:" + new String(byteBuffer.array(), 0 ,byteBuffer.limit()));
                            byteBuffer.clear();
                        }
                        // 关闭客户端
                        channel.close();
                    } else if (selectableChannel instanceof DatagramChannel) {
                        DatagramChannel channel = (DatagramChannel) selectableChannel;

                        SocketAddress receive = channel.receive(byteBuffer);
                        byteBuffer.flip();
                        System.out.println("从"+ receive +"读取的数据是:" + new String(byteBuffer.array(), 0 ,byteBuffer.limit()));

                        byteBuffer.clear();
                        // 关闭客户端
                        channel.close();

                    }

                }

                // 移除selectionKey
                iterator.remove();
            }
        }

        selector.close();
        serverSocketChannel.close();

    }

    public static void main(String[] args) {
        try {
            startServer();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
}

TCP客户端:

 public class DiscardTCPClient {
    public static void startClient() throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);

        // 连接Discard服务器
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 10880));

        while(!socketChannel.finishConnect()) {

        }

        System.out.println("已经与服务器建立连接");

        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put("你好啊".getBytes(StandardCharsets.UTF_8));

        byteBuffer.flip();

        socketChannel.write(byteBuffer);

        socketChannel.shutdownOutput();

        socketChannel.close();
    }

    public static void main(String[] args) {
        try {
            startClient();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
}

UDP客户端:

  public class DiscardUDPClient {
    public static void startClient() throws Exception {
        DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.configureBlocking(false);


        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put("发送UDP".getBytes(StandardCharsets.UTF_8));

        byteBuffer.flip();

        datagramChannel.send(byteBuffer, new InetSocketAddress("127.0.0.1", 10890));

        datagramChannel.close();
    }

    public static void main(String[] args) {
        try {
            startClient();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
}

例子,实现传输文件的服务器和客户端

服务器

public class SendFileServer {

    public final static String QUERY_FILE_PATH = "/home/shigp/图片";

    public static void main(String[] args) {
        try {
            startServer();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void startServer() throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress("localhost",10900));


        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (selector.select() > 0) {
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();

                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel channel = serverSocketChannel1.accept();
                    channel.configureBlocking(false);

                    System.out.println("接受连接");

                    channel.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    System.out.println("可读");
                    SocketChannel channel = (SocketChannel) selectionKey.channel();

                    ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();

                    if (byteBuffer == null) {
                        byteBuffer = ByteBuffer.allocate(2048);
                    } else {
                        System.out.println("start,position="+byteBuffer.position());
                    }

                    int len;
                    int tmpPosition = 0;
                    int tmpLimit = 0;

                    int sum = 0;

                    while ((len = channel.read(byteBuffer)) > 0) {

                        sum += len;

                        System.out.println("1.读取字节数:"+len+",position="+byteBuffer.position());
                        if (byteBuffer.position() < 4) {
                            continue;
                        }

                        byteBuffer.flip();
                        tmpPosition = byteBuffer.position();
                        tmpLimit = byteBuffer.limit();

                        System.out.println("while,limit="+byteBuffer.limit()+",position="+byteBuffer.position());
                        int tmpFileNameSize = byteBuffer.getInt();
                        System.out.println("while,limit="+byteBuffer.limit()+",position="+byteBuffer.position());

                        byteBuffer.position(tmpPosition);
                        byteBuffer.limit(tmpLimit);
                        byteBuffer.compact();
                        if (tmpLimit >= tmpFileNameSize + 4){
                            System.out.println("2.读取字节数:"+len+",position="+byteBuffer.position());
                            break;
                        }

                        System.out.println("3.读取字节数:"+len+",position="+byteBuffer.position());
                    }

                    byteBuffer.flip();
                    System.out.println("1234,limit="+byteBuffer.limit()+",position="+byteBuffer.position());

                    tmpPosition = byteBuffer.position();
                    tmpLimit = byteBuffer.limit();
                    int tmpFileNameSize = byteBuffer.getInt();
                    byteBuffer.position(tmpPosition);
                    byteBuffer.limit(tmpLimit);
                    if (tmpLimit >= tmpFileNameSize + 4){
                        System.out.println("1.读取到的文件名是:" + new String(byteBuffer.array(), 4 , tmpFileNameSize));

                        channel.register(selector, SelectionKey.OP_WRITE);
                        selectionKey.attach(new String(byteBuffer.array(), 4 , tmpFileNameSize));
                    } else {
                        System.out.println("position="+byteBuffer.position());
                        byteBuffer.compact();
                        selectionKey.attach(byteBuffer);
                    }
                } else if (selectionKey.isWritable()) {
                    System.out.println("可写");
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    String fileName = (String) selectionKey.attachment();

                    System.out.println(fileName);

                    File file = traverseFolder(new File(QUERY_FILE_PATH), fileName);
                    ByteBuffer allocate = ByteBuffer.allocate(8);
                    if (file == null) {
                        System.out.println("没有找到文件:" + fileName);

                        allocate.putLong(-1L);
                        allocate.flip();

                        while (channel.write(allocate) > 0) {

                        }
                    } else {

                        allocate.putLong(file.length());
                        allocate.flip();

                        while (channel.write(allocate) > 0) {

                        }

                        System.out.println("写入文件大小成功,"+file.length());

                        FileChannel channel1 = new FileInputStream(file).getChannel();

                        for (long count = file.length(); count > 0;) {
                            long transfer = channel1.transferTo(channel1.position(), count, channel);
                            count -= transfer;
                        }
                        System.out.println("写入文件内容成功");
                        channel1.close();
                    }
                    channel.close();
                }

                iterator.remove();
            }
        }
    }

    public static File traverseFolder(File folder,String fileName) {
        if (!folder.exists()) {
            return null;
        }
        if (folder.isDirectory()) {
            for (File file : folder.listFiles()) {
                if (file.isDirectory()) {
                    traverseFolder(file,fileName);
                } else if (fileName.equals(file.getName())){
                    return file;
                }
            }
        }

        return null;
    }
}

客户端

public class SendFileClient {

public static void main(String[] args) {
    try {
        receive();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static void receive() throws Exception {
    String fileName = "Spark教程.docx";
    String destFileName = "/home/shigp/文档/client";

    File file1 = new File(destFileName);
    if (!file1.exists()) {
        file1.mkdirs();
    }

    File file = new File(destFileName + File.separator + fileName);

//        if (file.exists()) {
//            file.createNewFile();
//        }

    FileChannel fileChannel = new FileOutputStream(destFileName + File.separator + fileName).getChannel();

    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.configureBlocking(false);

    socketChannel.connect(new InetSocketAddress("localhost",10900));

    while (!socketChannel.finishConnect()) {

    }

    byte[] bytes = fileName.getBytes(StandardCharsets.UTF_8);
    ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
    byteBuffer.putInt(bytes.length);
    byteBuffer.flip();
    while (socketChannel.write(byteBuffer) >0) {

    }

    byteBuffer.clear();
    byteBuffer.put(bytes);
    byteBuffer.flip();
    while (socketChannel.write(byteBuffer) >0) {

    }

    System.out.println("写入成功");

    System.out.println("开始接收文件");

    byteBuffer.clear();

    int len;
    long fileSize = 0L;
    int tpmPosition = 0;
    int tmpLimit = 0;
    long readFileSize = 0L;

    boolean isReadFileSize = true;

    int sum  =0;
    int writeBytes = 0;

    while ((len = socketChannel.read(byteBuffer)) >= 0) {
        if (len == 0){
            continue;
        }

        sum += len;

        byteBuffer.flip();
        tmpLimit=byteBuffer.limit();
        tpmPosition = byteBuffer.position();
        if (isReadFileSize) {
            if (tmpLimit >= 8) {
                fileSize = byteBuffer.getLong();
                byteBuffer.position(8);
                isReadFileSize=false;
                readFileSize += (tmpLimit-8);
            } else {
                byteBuffer.position(tpmPosition);
            }
        } else {
            readFileSize += len;

            if (readFileSize>=fileSize) {
                byteBuffer.limit(tmpLimit);
                break;
            }
        }


        byteBuffer.limit(tmpLimit);
        boolean hasRemaining = byteBuffer.hasRemaining();
        int size = 0;
        if (!isReadFileSize) {
            while (hasRemaining && (size = fileChannel.write(byteBuffer)) > 0) {
                hasRemaining = byteBuffer.hasRemaining();
                writeBytes += size;
            }

            byteBuffer.clear();
        }
    }

    if (byteBuffer.hasRemaining()) {
        int size = 0;
        while ((size=fileChannel.write(byteBuffer)) > 0) {
            writeBytes += size;
        }
    }

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

    System.out.println("readFileSize="+readFileSize+",fileSize="+fileSize);
    System.out.println("写入字节="+writeBytes);

    if (fileSize >=0) {
        if (readFileSize>=fileSize) {
            System.out.println("接收文件成功");
        } else {
            System.out.println("接收文件失败");
        }
    } else {
        System.out.println("文件不存在");
    }

    fileChannel.force(true);
    fileChannel.close();
    socketChannel.close();
}
}
posted @ 2024-09-28 11:17  shigp1  阅读(24)  评论(0编辑  收藏  举报