Java7-NIO2-高级教程-三-

102 阅读1小时+

Java7 NIO2 高级教程(三)

协议:CC BY-NC-SA 4.0

七、随机访问文件

在前面的章节中,我们已经按顺序探索了文件。可以顺序浏览的文件称为顺序文件。在本章中,你将看到使用非顺序(随机)访问文件内容的优点。允许随机访问其内容的文件被称为随机访问文件(RAFs )。顺序文件更常用,因为它们易于创建,但是 RAF 更灵活,并且可以更快地找到它们的数据。

使用 RAF,您可以打开文件,查找特定位置,并读取或写入该文件。在你打开 RAF 之后,你可以通过使用一个记录号以随机的方式读取或写入它,或者你可以添加到文件的开头或结尾,因为你知道文件中有多少记录。RAF 允许您读取单个字符、读取一个字节块或一行、替换文件的一部分、添加行、删除行等等,并允许您以随机的方式执行所有这些操作。

Java 7 (NIO.2)引入了一个全新的接口来使用 RAFs。它的名字叫SeekableByteChannel,在java.nio.channels包中有售。它扩展了旧的ByteChannel接口,并代表一个字节通道,该通道保持当前位置并允许修改该位置。此外,Java 7 通过实现这个接口并一次性提供 RAF 和FileChannel功能,改进了众所周知的FileChannel类。通过简单的造型,我们可以将一个SeekableByteChannel转换成一个FileChannel

本章广泛使用了java.nio.ByteBuffer类,所以我们将从它的一个简短概述开始。我们将继续详细介绍SeekableByteChannel与应用的接口,这些应用将随机读写文件以完成不同类型的常见任务。然后,您将看到如何获得具有 RAF 功能的FileChannel,并探索FileChannel提供的主要功能,例如将文件的一个区域直接映射到内存中以实现更快的访问,锁定文件的一个区域,以及在不影响通道当前位置的情况下从绝对位置读取和写入字节。本章结尾是一个基准测试应用,它将帮助您确定使用FileChannel功能复制文件的最快方法,而不是其他常见的方法,如Files.copy(),缓冲流等等。

byte buffer 概述

缓冲区本质上是一个数组(通常是字节数组,但也可以使用其他类型的数组——Buffer接口提供了ByteBufferCharBufferIntBufferShortBufferLongBufferFloatBufferDoubleBuffer,用于保存一些要写入或刚刚读取的数据。

NIO 中缓冲区的两个最重要的组件是属性和祖先方法,下面将依次讨论。

ByteBuffer 属性

以下是缓冲区的基本属性:

  • Limit :当从一个缓冲区写入时,Limit 指定还有多少数据要获取。当你读入一个缓冲区时,这个限制指定了还有多少空间可以存放数据。
  • Position:Position 记录你读了或写了多少数据。它指定下一个字节将进入或来自哪个数组元素。缓冲区的位置永远不会是负的,也永远不会大于它的限制。
  • 容量:容量指定一个缓冲区可以存储的最大数据量。限制永远不能大于容量。

Image 注意作为不变量,这三个性质尊重以下关系:0 ≤位置≤极限≤容量。

例如,假设一个缓冲器有 6 字节的容量,如图 7-1 所示。

Image

图 7-1。 Java 缓冲区表示(一)

在起点,极限和容量相等(极限不能大于容量,但反过来是完全正常的),并被设置为一个虚拟槽(在我们的例子中,槽号为 7),如图图 7-2 所示。

Image

图 7-2 Java 缓冲区表示(b)

Image 注意在某些情况下,初始限制可能是 0,也可能是其他值,这取决于缓冲区的类型及其构造方式。

同样,在起始点,位置被设置为 0(槽 1,如图 7-3 中的所示)——一个读或写字节将访问位置 0。

Image

图 7-3 Java 缓冲区表示(c)

接下来,假设我们将 2 个字节的数据读入缓冲区。这 2 个字节的数据从位置 0 开始进入缓冲区。因此,前两个字节被填充,位置转到第三个字节,如图图 7-4 所示。

Image

图 7-4 Java 缓冲区表示(d)

继续第二次读取,另外 3 个字节进入缓冲区。位置增加到 5(槽 6),如图图 7-5 所示。

Image

图 7-5 Java 缓冲区表示(e)

此时,假设我们不再读入缓冲区,而是想从缓冲区写入。为此,我们首先需要在写入任何字节之前调用flip()方法。这会将限制设置到当前位置,并将位置设置为 0。翻转后,缓冲器出现如图图 7-6 所示。

Image

图 7-6 Java 缓冲区表示(f)

假设我们从缓冲区写入 3 个字节。由于位置为 0,前 3 个字节被写入,位置移动到 3(槽 4),如图图 7-7 所示。限制和容量保持不变。

Image

图 7-7 Java 缓冲区表示(g)

接下来我们再写 2 个字节,位置前移至槽 6,如图图 7-8;限制和容量保持不变。

Image

图 7-8 Java 缓冲区表示(h)

我们可能还想完成另外两个操作。继续参照图 7-8 ,我们可能想要倒带缓冲区或清除缓冲区。倒带缓冲区(调用rewind()方法)将为重新读取缓冲区已经包含的数据做准备——限制保持不变,位置设置为 0。清空缓冲区(调用clear()方法)将重置缓冲区以接收更多字节(数据不会被删除)—限制设置为容量,位置设置为 0。图 7-9 显示了clear()方法的效果,图 7-10 显示了rewind()方法的效果。

Image

图 7-9 Java 缓冲区表示(一)

Image

图 7-10 Java 缓冲区表示(j)

此外,一个缓冲区保存一个标记。这是调用reset()方法时其位置将被重置的索引。标记并不总是确定的,但它永远不会是负数,也永远不会大于位置。如果标记被定义,那么当位置或限制被调整到小于标记的值时,它被丢弃。如果标记没有定义,那么调用reset()方法会导致抛出一个InvalidMarkException

Image 将标记插入关系中得到以下结果:0 ≤标记≤位置≤极限≤容量。

ByteBuffer 祖先方法

ByteBuffer提供了一套访问数据的get()put()方法。由于它们非常直观,我将在此简单列出。更多详情,请查阅[download.oracle.com/javase/7/docs/api/index.html](http://download.oracle.com/javase/7/docs/api/index.html)[download.oracle.com/javase/7/docs/index.html](http://download.oracle.com/javase/7/docs/index.html)的官方文件。

public abstract byte get()
public ByteBuffer get(byte[] dst)
public ByteBuffer get(byte[] dst, int offset, int length)
public abstract byte get(int index)

public abstract ByteBuffer put(byte b)
public final ByteBuffer put(byte[] src)
public ByteBuffer put(byte[] src, int offset, int length)
public ByteBuffer put(ByteBuffer src)
public abstract ByteBuffer put(int index, byte b)

除了get()put()方法,ByteBuffer还有额外的方法来读取和写入不同类型的值,如下所示:

public abstract char getChar()
public abstract char getChar(int index)
public abstract double getDouble()
public abstract double getDouble(int index)
public abstract float getFloat()
public abstract float getFloat(int index)
public abstract int getInt()
public abstract int getInt(int index)
public abstract long getLong()
public abstract long getLong(int index)
public abstract short getShort()
public abstract short getShort(int index)

public abstract ByteBuffer putChar(char value)
public abstract ByteBuffer putChar(int index, char value)
public abstract ByteBuffer putDouble(double value)
public abstract ByteBuffer putDouble(int index, double value)
public abstract ByteBuffer putFloat(float value)
public abstract ByteBuffer putFloat(int index, float value)
public abstract ByteBuffer putInt(int value)
public abstract ByteBuffer putInt(int index, int value)
public abstract ByteBuffer putLong(int index, long value)
public abstract ByteBuffer putLong(long value)
public abstract ByteBuffer putShort(int index, short value)
public abstract ByteBuffer putShort(short value)

一个字节缓冲区可以是直接的,也可以是非直接的。JVM 将在直接缓冲区上执行本机 I/O 操作。直接缓冲区通过使用allocateDirect()方法创建,而非直接缓冲区通过使用allocate()方法创建。

此时你已经有了足够的关于ByteBuffer的信息来理解下面的应用。(要深入了解ByteBuffer,请访问网上的专门教程。)因此,我们暂时抛开ByteBuffer,进入本章的主题SeekableByteChannel界面。下一节将向您介绍通道,并将它们与缓冲区联系起来。

渠道概述

面向流的 I/O 系统中,输入流产生 1 字节的数据,输出流消耗 1 字节的数据——这样的系统通常相当慢。相比之下,在一个面向块的 I/O 系统中,输入/输出流一步就产生或消耗一个数据块。

通道类似于流,但有一些不同:

  • 虽然流通常是单向的(读或写),但通道支持读和写。
  • 通道可以异步读写。
  • 通道总是读取或写入缓冲区。发送到通道的所有数据必须首先放在缓冲区中。从通道读取的任何数据都被读入缓冲区。

使用 SeekableByteChannel 接口对文件进行随机访问

新的SeekableByteChannel接口通过实现通道上位置的概念来提供对 RAF 的支持。我们可以从通道中读取或向通道中写入一个ByteBuffer,获取或设置当前位置,并将连接到通道的实体截断到指定的维度。以下方法与这些功能相关(更多详情可在[download.oracle.com/javase/7/docs/api/index.html](http://download.oracle.com/javase/7/docs/api/index.html)的官方文件中获得):

  • position():返回通道的当前位置(非负)。
  • position(long):将通道的位置设置为指定的long(非负)。将位置设置为大于当前大小的值是合法的,但不会改变实体的大小。
  • truncate(long):将连接到通道的实体截断到指定的长度。
  • read(ByteBuffer):从通道读取字节到缓冲区。
  • write(ByteBuffer):将字节从缓冲区写入通道。
  • size():返回该通道连接的实体的当前大小。

获取SeekableByteChannel的实例可以通过Files类的两个方法完成,名为newByteChannel()。第一个(最简单的)newByteChannel()方法接收要打开或创建的文件的路径和一组指定如何打开文件的选项。StandardOpenOption枚举常量在第四章章节“使用标准打开选项”中有所描述,但为了便于参考,在此重复这些常量:

Image

第二个newByteChannel()方法接收要打开或创建的文件的路径,一组指定如何打开文件的选项,以及可选的,创建文件时自动设置的文件属性列表。

这两种方法都打开或创建一个文件,返回SeekableByteChannel来访问该文件。

使用 SeekableByteChannel 读取文件

关注第一个newByteChannel()方法,我们得到一个用于读取路径C:\rafaelnadal\grandslam\RolandGarros\story.txtSeekableByteChannel(文件必须存在):

… Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt"); … try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path,                                                          EnumSet.of(StandardOpenOption.READ))) { … } catch (IOException ex) {    System.err.println(ex); }

例如,下面的应用将使用一个ByteBuffer读取并显示story.txt的内容(该文件必须存在)。我选择了 12 字节的缓冲区,但也可以随意使用其他大小的缓冲区。

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
import java.nio.file.StandardOpenOption;

public class Main {

 public static void main(String[] args) {

  Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");

  //read a file using SeekableByteChannel
  try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path,
                                                       EnumSet.of(StandardOpenOption.READ))) {

   ByteBuffer buffer = ByteBuffer.allocate(12);            
   String encoding = System.getProperty("file.encoding");
   buffer.clear();

   while (seekableByteChannel.read(buffer) > 0) {
         buffer.flip();
         System.out.print(Charset.forName(encoding).decode(buffer));
         buffer.clear();                
   }
  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}

输出应该类似于以下内容:


Rafa Nadal produced another masterclass of clay-court tennis to win his fifth French Open
title ...

用 SeekableByteChannel 写文件

SeekableByteChannel写文件需要使用WRITE选项。此外,如果我们想在写作前清理现有的内容,我们可以添加如下的TRUNCATE_EXISTING选项。在这里,我们截断了story.txt,并准备写它(?? 文件必须存在)。

Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");
…
try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path,
                EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING))) {
…
} catch (IOException ex) {
   System.err.println(ex);
}

例如,下面的应用将使用一个ByteBuffer截断并在story.txt中写入一些文本(在这种情况下,文件已经存在;如果它不存在,那么我们将添加CREATECREATE_NEWWRITE选项,并去掉TRUNCATE_EXISTING选项,因为文件反正是空的)。

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;

public class Main {

 public static void main(String[] args) {

   Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");

   //write a file using SeekableByteChannel
   try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path,
                EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING))) {

    ByteBuffer buffer = ByteBuffer.wrap("Rafa Nadal produced another masterclass of clay-court
                                   tennis to win his fifth French Open title ...".getBytes());

    int write = seekableByteChannel.write(buffer);
    System.out.println("Number of written bytes: " + write);

    buffer.clear();
   } catch (IOException ex) {
     System.err.println(ex);
   }
 }
}

当你写一个文件时,有一些常见的情况涉及到组合打开选项:

  • 要写入一个存在的文件,在开始时,使用WRITE
  • 要写入一个存在的文件,在最后,使用WRITEAPPEND
  • 要写入一个存在的文件并在写入前清理其内容,使用WRITETRUNCATE_EXISTING
  • 要写入一个不存在的文件,使用CREATE(或CREATE_NEW)和WRITE
可查找的字节通道和文件属性

以下代码片段(为 Unix 和其他 POSIX 文件系统编写)创建了一个具有一组特定文件权限的文件。这段代码在home\rafaelnadal\email目录中创建文件email.txt,如果它已经存在,则追加到该文件中。创建的email.txt文件对所有者具有读写权限,对组具有只读权限。

`import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.EnumSet; import java.util.Set;

public class Main {

 public static void main(String[] args) {

 Path path = Paths.get("home/rafaelnadal/email", "email.txt");  ByteBuffer buffer = ByteBuffer.wrap("Hi Rafa, I want to congratulate you for the amazing                                                       match that you played ... ".getBytes());

 //create the custom permissions attribute for the email.txt file  Set perms = PosixFilePermissions.fromString("rw-r------");  FileAttribute<Set> attr = PosixFilePermissions.asFileAttribute(perms);

 //write a file using SeekableByteChannel  try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path,                     EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.APPEND), attr)) {

  int write = seekableByteChannel.write(buffer);   System.out.println("Number of written bytes: " + write);

 } catch (IOException ex) {    System.err.println(ex);  }

 buffer.clear();  } }`

使用旧的 ReadableByteChannel 接口读取文件

新的SeekableByteChannel接口基于旧的接口ReadableByteChannel(代表读取字节的通道;一次只能有一个线程读取)和WritableByteChannel(代表一个写字节的通道;一次只能有一个线程可以写),这是从 JDK 1.4 开始在 NIO 中提供的。这两个接口是SeekableByteChannel的超级接口。由于它们之间的这种关系,我们可以使用旧的ReadableByteChannel接口和新的Files.newByteChannel()方法,如下所示,其中我们读取现有的story.txt文件的内容:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Main {

 public static void main(String[] args) {

  Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");

  //read a file using ReadableByteChannel
  try (ReadableByteChannel readableByteChannel = Files.newByteChannel(path)) {

   ByteBuffer buffer = ByteBuffer.allocate(12);
   buffer.clear();

   String encoding = System.getProperty("file.encoding");

   while (readableByteChannel.read(buffer) > 0) {
          buffer.flip();                
          System.out.print(Charset.forName(encoding).decode(buffer));
          buffer.clear();                
   }
  } catch (IOException ex) {
     System.err.println(ex);
  }
 }
}

如您所见,不需要指定READ选项。

用旧的 WritableByteChannel 接口写文件

我们也可以将旧的WritableByteChannel接口与新的Files.newByteChannel()方法结合起来,如下所示,其中我们将一些文本添加到story.txt中:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;
public class Main {

public static void main(String[] args) {

 Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");

 //write a file using WritableByteChannel
 try (WritableByteChannel writableByteChannel = Files.newByteChannel(path,
                           EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.APPEND))) {

  ByteBuffer buffer = ByteBuffer.wrap("Vamos Rafa!".getBytes());

  int write = writableByteChannel.write(buffer);
  System.out.println("Number of written bytes: " + write);

  buffer.clear();

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}

即使我们使用了一个WritableByteChannel,我们仍然需要显式地指定WRITE选项。APPEND选项是可选的,特定于前面的例子。

玩弄 SeekableByteChannel 的立场

现在您已经知道如何使用SeekableByteChannel读写整个文件,您已经准备好发现如何在指定的通道(实体)位置执行相同的操作。为此,我们将在一组四个例子中利用position()position(long)方法,意在让你熟悉 RAF 概念。请记住,不带参数的position()方法返回当前的通道(实体)位置,而position(long)方法通过计算从通道(实体)开始的字节数来设置通道(实体)的当前位置。第一个位置是 0,最后一个有效位置是通道(实体)大小。

例 1:从不同位置读一个字符

我们从一个简单的例子开始,它从文本文件的第一、中间和最后位置读取一个字符。文件是MovistarOpen.txt,它位于C:\rafaelnadal\tournaments\2009目录中。

`import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.EnumSet;

public class Main {

 public static void main(String[] args) {

  Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "MovistarOpen.txt");   ByteBuffer buffer = ByteBuffer.allocate(1);   String encoding = System.getProperty("file.encoding");

  try (SeekableByteChannel seekableByteChannel = (Files.newByteChannel(path,                                                       EnumSet.of(StandardOpenOption.READ)))) {

   //the initial position should be 0 anyway    seekableByteChannel.position(0);

   System.out.println("Reading one character from position: " +                                                               seekableByteChannel.position());    seekableByteChannel.read(buffer);    buffer.flip();    System.out.print(Charset.forName(encoding).decode(buffer));    buffer.rewind();

   //get into the middle    seekableByteChannel.position(seekableByteChannel.size()/2);

   System.out.println("\nReading one character from position: " +                                                               seekableByteChannel.position());    seekableByteChannel.read(buffer);    buffer.flip();    System.out.print(Charset.forName(encoding).decode(buffer));    buffer.rewind();

   //get to the end    seekableByteChannel.position(seekableByteChannel.size()-1);

   System.out.println("\nReading one character from position: " +                                                               seekableByteChannel.position());    seekableByteChannel.read(buffer);    buffer.flip();    System.out.print(Charset.forName(encoding).decode(buffer));    buffer.clear();

  } catch (IOException ex) {    System.err.println(ex);   }          } }`

前面的应用将产生以下输出:


Reading one character from position: 0

T

Reading one character from position: 181

n

Reading one character from position: 361

.

例 2:在不同的位置书写字符

接下来,我们将尝试写入特定位置。假设MovistarOpen.txt文件有以下默认内容:

The Movistar Open moved to Santiago from Viña del Mar in 2010\. It is the first clay-court
tournament of the ATP World Tour season and also the opening leg of the four-tournament swing
through Latin America, aptly coined the "Golden Swing" in honour of top Chileans and Olympic
Gold medalists Fernando Gonsales and Nicolas Massu. Gonzalez is a four-time champion.

我们希望完成两项任务:首先,在前面的文本末尾添加一些文本,其次,将“Gonsales”替换为“Gonzalez ”,因为 Fernando 的姓在第一个实例中被拼错了。下面是应用:

`import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.EnumSet;

public class Main { public static void main(String[] args) {

  Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "MovistarOpen.txt");   ByteBuffer buffer_1 = ByteBuffer.wrap("Great players participate in our tournament, like:               Tommy Robredo, Fernando Gonzalez, Jose Acasuso or Thomaz Bellucci.".getBytes());   ByteBuffer buffer_2 = ByteBuffer.wrap("Gonzalez".getBytes());

  try (SeekableByteChannel seekableByteChannel = (Files.newByteChannel(path,                                                      EnumSet.of(StandardOpenOption.WRITE)))) {

   //append some text at the end    seekableByteChannel.position(seekableByteChannel.size());

   while (buffer_1.hasRemaining()) {           seekableByteChannel.write(buffer_1);    }

   //replace "Gonsales" with "Gonzalez"    seekableByteChannel.position(301);

   while (buffer_2.hasRemaining()) {           seekableByteChannel.write(buffer_2);    }

   buffer_1.clear();    buffer_2.clear();

  } catch (IOException ex) {     System.err.println(ex);   }  } }`

如果一切正常,新的MovistarOpen.txt内容应该如下所示:

The Movistar Open moved to Santiago from Viña del Mar in 2010\. It is the first clay-court
tournament of the ATP World Tour season and also the opening leg of the four-tournament swing
through Latin America, aptly coined the "Golden Swing" in honour of top Chileans and Olympic
Gold medalists Fernando Gonzalez and Nicolas Massu. Gonzalez is a four-time champion. Great
players participate in our tournament, like: Tommy Robredo, Fernando Gonzalez, Jose Acasuso or
Thomaz Bellucci.
例 3:从头到尾复制一个文件的一部分

转到一个新的应用,我们下一步想把一部分文本从一个文件的开头复制到同一个文件的结尾。例如,我们将使用HeinekenOpen.txt文件(位于C:\rafaelnadal\tournaments\2009目录中),该文件包含以下内容:

The Pride Of New Zealand
The Heineken Open is the biggest men's professional sporting event in New Zealand, held in...

我们想把“新西兰的骄傲”这句话抄在最后,就像这样:

The Pride Of New Zealand
The Heineken Open is the biggest men's professional sporting event in New Zealand, held in...
The Pride Of New Zealand

以下应用完成了这项任务:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;

public class Main {

 public static void main(String[] args) {

  Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "HeinekenOpen.txt");        

  ByteBuffer copy = ByteBuffer.allocate(25);
  copy.put("\n".getBytes());

  try (SeekableByteChannel seekableByteChannel = (Files.newByteChannel(path,
                            EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE)))) {

   int nbytes;
   do {
      nbytes = seekableByteChannel.read(copy);
   } while (nbytes != -1 && copy.hasRemaining());

   copy.flip();            

   seekableByteChannel.position(seekableByteChannel.size());
   while (copy.hasRemaining()) {
          seekableByteChannel.write(copy);
   }

   copy.clear();

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}
例 4:用截断功能替换文件部分

在本例中,我们将截断一个文件,并在截断的文本中添加新的文本。我们将使用BrasilOpen.txt文件(在C:\rafaelnadal\tournaments\2009目录中找到),该文件包含以下内容:

Brasil Open At Forefront Of Green Movement
The Brasil Open, the second stop of the four-tournament Latin American swing, is held in an
area renowned for its lush natural beauty and stunning beaches. From this point forward ...

我们希望截断文件内容,以删除“从现在开始...”的文本并在其位置追加新文本。以下是解决方案:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;

public class Main {

 public static void main(String[] args) {

  Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BrasilOpen.txt");

  ByteBuffer buffer = ByteBuffer.wrap("The tournament has taken a lead in environmental
conservation efforts, with highlights including the planting of 500 trees to neutralise carbon
emissions and providing recyclable materials to local children for use in craft
work.".getBytes());

  try (SeekableByteChannel seekableByteChannel = (Files.newByteChannel(path,
                            EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE)))) {

   seekableByteChannel.truncate(200);

   seekableByteChannel.position(seekableByteChannel.size()-1);
   while (buffer.hasRemaining()) {
          seekableByteChannel.write(buffer);
   }

   buffer.clear();

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}

该应用的效果是对BrasilOpen.txt文件的如下修改:

Brasil Open At Forefront Of Green Movement
The Brasil Open, the second stop of the four-tournament Latin American swing, is held in an
area renowned for its lush natural beauty and stunning beaches. The tournament has taken a
lead in environmental conservation efforts, with highlights including the planting of 500
trees to neutralise carbon emissions and providing recyclable materials to local children for
use in craft work.

这一组例子应该有助于您理解如何随机访问文件内容。接下来,我们将把SeekableByteChannel接口转换为FileChannel,让我们可以访问更高级的特性。

使用文件通道

FileChannel是在 Java 4 中引入的,但最近它被更新以实现新的SeekableByteChannel接口,结合它们的力量以实现更大的能力。SeekableByteChannel提供了随机访问文件功能,而FileChannel提供了强大的高级功能,例如将文件的一个区域直接映射到内存中,以便更快地访问和锁定文件的一个区域。

为一个Path获取一个FileChannel可以通过两个新的FileChannel.open()方法来完成。这两种方法都能够为给定的Path打开或创建一个文件,并返回一个新的通道。第一种(最简单的)方法接收要打开或创建的文件的路径,以及一组指定如何打开文件的选项。第二种方法接收要打开或创建的文件的路径、一组指定如何打开文件的选项,以及(可选)在创建文件时自动设置的文件属性列表。

例如,以下代码为指定路径获取具有读/写功能的文件通道:

Path path = Paths.get("…");
…
try (FileChannel fileChannel = (FileChannel.open(path, EnumSet.of(
                                       StandardOpenOption.READ, StandardOpenOption.WRITE)))) {
…
} catch (IOException ex) {
  System.err.println(ex);
}

SeekableByteChannel显式转换为FileChannel可以替代前面的代码:

Path path = Paths.get("…");
…
try (FileChannel fileChannel = (FileChannel)(Files.newByteChannel(path,
                              EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE)))) {
…
} catch (IOException ex) {
  System.err.println(ex);
}

现在,fileChannel实例可以访问由SeekableByteChannelFileChannel提供的方法。

将通道的文件区域直接映射到内存中

最棒的FileChannel功能之一是能够将通道文件的一个区域直接映射到内存中。这要归功于FileChannel.map()方法,它获得以下三个参数:

  • mode:将一个区域映射到内存有三种方式可以完成:MapMode.READ_ONLY ( 只读映射;写尝试会抛出ReadOnlyBufferExceptionMapMode.READ_WRITE ( 读/写映射;结果缓冲区中的变化可以传播到文件,并且可以从映射相同文件的其他程序中看到),或者MapMode.PRIVATE ( 写时复制映射;结果缓冲区中的更改不能传播到文件,并且在其他程序中不可见)。
  • position:映射的区域从文件中指定的位置开始(非负)。
  • size:表示映射区域的大小(0sizeInteger.MAX_VALUE)。

Image 注意只有打开读的通道才能映射为只读,只有打开读写的通道才能映射为读/写或私有。

map()方法将返回一个实际代表提取区域的MappedByteBuffer。这用以下三个方法扩展了ByteBuffer,更多细节可以在[download.oracle.com/javase/7/docs/api/index.html](http://download.oracle.com/javase/7/docs/api/index.html)的官方文档中找到:

  • force():强制将转换后的缓冲区传播到原始文件
  • load():将缓冲内容加载到物理内存中
  • isLoaded():验证缓冲区内容是否在物理内存中

下一个应用为文件BrasilOpen.txt(位于C:\rafaelnadal\tournaments\2009中)获得一个新的通道,并在READ_ONLY模式下将其全部内容映射到一个字节缓冲区中。为了测试操作是否成功完成,以下是字节缓冲区内容的打印输出:

`import java.io.IOException; import java.nio.CharBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.EnumSet;

public class Main { public static void main(String[] args) {

   Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BrasilOpen.txt");    MappedByteBuffer buffer = null;

   try (FileChannel fileChannel = (FileChannel.open(path,                                                       EnumSet.of(StandardOpenOption.READ)))) {

     buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

   } catch (IOException ex) {      System.err.println(ex);    }

   if (buffer != null) {        try {            Charset charset = Charset.defaultCharset();            CharsetDecoder decoder = charset.newDecoder();            CharBuffer charBuffer = decoder.decode(buffer);            String content = charBuffer.toString();            System.out.println(content);

           buffer.clear();        } catch (CharacterCodingException ex) {          System.err.println(ex);        }     }   } }`

如果一切正常,您应该看到BrasilOpen.txt内容输出到控制台。

锁定频道的文件

文件锁定是一种限制访问文件或其他数据的机制,以确保两个或更多用户不能同时修改同一个文件。这防止了经典的调解更新场景。通常,当第一个用户访问该文件时,该文件被锁定,并保持锁定(可以读取,但不能修改),直到该用户完成该文件。

文件锁定的确切行为取决于平台。在某些平台上,文件锁定是建议性的(如果应用不检查文件锁定,任何应用都可以访问文件),而在其他平台上,它是强制性的(文件锁定阻止任何应用访问文件)。

我们可以通过 NIO API 利用 Java 应用中的文件锁定。但是,不能保证文件锁定机制总是按您预期的那样工作。底层操作系统支持或有时错误的实现可能会影响预期的行为。请记住以下几点:

  • “文件锁代表整个 Java 虚拟机持有。它们不适合控制同一虚拟机内多个线程对文件的访问。(Java 平台 SE 7 官方文档,http://download . Oracle . com/javase/7/docs/API/Java/nio/channels/file lock . html。)
  • Windows 会为您锁定目录和其他结构,因此如果另一个进程打开了文件,删除、重命名或写操作将会失败。因此,在系统锁之上创建 Java 锁将会失败。
  • Linux 内核管理一组被称为咨询锁定机制的功能。此外,您可以使用强制锁在内核级别强制锁定。因此,在使用 Java 锁时,请记住这一点。

FileChannel类提供了四种文件锁定方法:两种lock()方法和两种tryLock()方法。lock()方法阻塞应用,直到可以检索到所需的锁,而tryLock()方法不阻塞应用,如果文件已经被锁定,则返回null或抛出异常。有一个lock() / tryLock()方法用于检索该通道文件上的排他锁,还有一个方法用于检索该通道文件的某个区域上的锁——该方法还允许共享锁。

为了演示文件锁定,我们来看两个应用。第一个在向一个名为vamos.txt(在C:\rafaelnadal\email下)的文件中写入一些文本时锁定该文件 2 分钟。在此期间,第二个应用将尝试写入同一文件。如果文件成功锁定了 2 分钟,那么第二个应用将抛出一个java.io.IO.Exception,并输出如下消息:


The process cannot access the file because another process has locked a portion of the file.

这是第一个应用:

`import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.EnumSet;

public class Main {

 public static void main(String[] args) {

   Path path = Paths.get("C:/rafaelnadal/email", "vamos.txt");    ByteBuffer buffer = ByteBuffer.wrap("Vamos Rafa!".getBytes());

   try (FileChannel fileChannel = (FileChannel.open(path, EnumSet.of(StandardOpenOption.READ,                                                                 StandardOpenOption.WRITE)))) {

    // Use the file channel to create a lock on the file.     // This method blocks until it can retrieve the lock.     FileLock lock = fileChannel.lock();

     // Try acquiring the lock without blocking. This method returns      // null or throws an exception if the file is already locked.      //try {         //    lock = fileChannel.tryLock();      //} catch (OverlappingFileLockException e) {          // File is already locked in this thread or virtual machine      //}

     if (lock.isValid()) {

         System.out.println("Writing to a locked file ...");          try {              Thread.sleep(60000);              } catch (InterruptedException ex) {                System.err.println(ex);              }          fileChannel.position(0);          fileChannel.write(buffer);          try {              Thread.sleep(60000);          } catch (InterruptedException ex) {            System.err.println(ex);          }       }

      // Release the lock       lock.release();

      System.out.println("\nLock released!");

     } catch (IOException ex) {        System.err.println(ex);      }   } }`

运行前面的应用,并在最多 2 分钟内并行启动下面的应用:

`import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.EnumSet;

public class Main {

 public static void main(String[] args) {

   Path path = Paths.get("C:/rafaelnadal/email", "vamos.txt");    ByteBuffer buffer = ByteBuffer.wrap("Hai Hanescu !".getBytes());

   try (FileChannel fileChannel = (FileChannel.open(path, EnumSet.of(StandardOpenOption.READ,                                                                 StandardOpenOption.WRITE)))) { fileChannel.position(0);      fileChannel.write(buffer);

   } catch (IOException ex) {      System.err.println(ex);    }  } }`

你应该会发现第二个应用只有在锁被释放后,也就是 2 分钟后才能写入vamos.txt

使用 FileChannel 复制文件

FileChannel提供了几种复制文件的方法。您可以使用直接或非直接ByteBufferFileChannel,使用FileChannel.transferTo()FileChannel.transferFrom(),或使用FileChannel.map()

使用 FileChannel 和直接或非直接字节缓冲区复制文件

要复制带有FileChannel和直接或非直接ByteBuffer的文件,我们需要一个通道用于源文件,一个通道用于目标文件,以及一个直接或非直接ByteBuffer。例如,下面的代码片段将使用 4KB 的直接文件ByteBuffer将文件Rafa Best Shots.mp4(位于C:\rafaelnadal\tournaments\2009\videos目录中)复制到C:\根目录:

`… final Path copy_from = Paths.get("C:/rafaelnadal/tournaments/2009/                                                              videos/Rafa Best Shots.mp4"); final Path copy_to = Paths.get("C:/Rafa Best Shots.mp4"); int bufferSizeKB = 4; int bufferSize = bufferSizeKB * 1024; … System.out.println("Using FileChannel and direct buffer ..."); try (FileChannel fileChannel_from = (FileChannel.open(copy_from,                       EnumSet.of(StandardOpenOption.READ)));      FileChannel fileChannel_to = (FileChannel.open(copy_to,                       EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {          

     // Allocate a direct ByteBuffer      ByteBuffer bytebuffer = ByteBuffer.allocateDirect(bufferSize);

     // Read data from file into ByteBuffer      int bytesCount;      while ((bytesCount = fileChannel_from.read(bytebuffer)) > 0) {              //flip the buffer which set the limit to current position, and position to 0                          bytebuffer.flip();              //write data from ByteBuffer to file              fileChannel_to.write(bytebuffer);              //for the next read              bytebuffer.clear();      } } catch (IOException ex) {   System.err.println(ex); } …`

要使用非直接ByteBuffer,只需更换线路即可

     ByteBuffer bytebuffer = ByteBuffer.allocateDirect(bufferSize);

用下面一行:

     ByteBuffer bytebuffer = ByteBuffer.allocate(bufferSize);
使用 FileChannel.transferTo()或 FileChannel.transferFrom()复制文件

FileChannel.transferTo()将字节从一个通道的文件传输到给定的可写字节通道。您选择位置、要传输的最大字节数和目标通道,FileChannel.transferTo()返回传输的字节数。下面的例子转移了Rafa Best Shots.mp4的全部内容:

…
System.out.println("Using FileChannel.transferTo method ...");
try (FileChannel fileChannel_from = (FileChannel.open(copy_from,
                      EnumSet.of(StandardOpenOption.READ)));
     FileChannel fileChannel_to = (FileChannel.open(copy_to,
                      EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

            fileChannel_from.transferTo(0L, fileChannel_from.size(), fileChannel_to);

} catch (IOException ex) {
  System.err.println(ex);
}
…

或者,您可以使用FileChannel.transferFrom()将字节从给定的可读字节通道传输到该通道的文件中。为此,请通过替换以下行来修改前面的代码

          fileChannel_from.transferTo(0L, fileChannel_from.size(), fileChannel_to);

用下面一行:

          fileChannel_to.transferFrom(fileChannel_from, 0L, (int) fileChannel_from.size());
使用 FileChannel.map()复制文件

在本章的前面,您已经看到了如何使用MappedByteBuffer将通道文件的一个区域映射到内存中。在本节中,我们推断这个例子复制了Rafa Best Shots.mp4内容:

… System.out.println("Using FileChannel.map method ..."); try (FileChannel fileChannel_from = (FileChannel.open(copy_from, `                      EnumSet.of(StandardOpenOption.READ)));      FileChannel fileChannel_to = (FileChannel.open(copy_to,                       EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

     MappedByteBuffer buffer = fileChannel_from.map(FileChannel.MapMode.READ_ONLY, 0,                                                                      fileChannel_from.size());

     fileChannel_to.write(buffer);      buffer.clear();

} catch (IOException ex) {   System.err.println(ex); } …`

基准测试文件通道拷贝功能

在前三节中,您看到了使用FileChannel功能复制文件的不同方式。Java 还提供了另一套复制文件的解决方案,包括使用Files.copy()方法或者缓冲/非缓冲流和一个字节数组。你应该选择哪一个?这是一个很难回答的问题,它的答案取决于很多因素。本节重点介绍一个因素,即速度,因为快速完成拷贝任务可以提高工作效率,在某些情况下,这对成功至关重要。因此,本节实现了一个应用,该应用比较以下每种解决方案为每个拷贝花费的时间:

  • FileChannel和非直达ByteBuffer
  • FileChannel和直接ByteBuffer
  • FileChannel.transferTo()
  • FileChannel.transferFrom()
  • FileChannel.map()
  • 使用缓冲流和字节数组
  • 使用无缓冲流和字节数组
  • Files.copy() ( PathPathInputStreamPathPathOutputStream)

测试在以下条件下进行:

  • 复制的文件类型:MP4 视频(文件名为Rafa Best Shots.mp4,初始位置在C:\rafaelnadal\tournaments\2009\videos
  • 复制文件大小:58.3MB
  • 测试的缓冲区大小:4KB、16KB、32KB、64KB、128KB、256KB 和 1024KB
  • 机器:移动 AMD Sempron 处理器 3400 + 1.80 GHz,1.00GB 内存,32 位操作系统,Windows 7 旗舰版
  • 测量类型:使用System.nanoTime()方法
  • 仅在三次被忽略的连续运行后捕获时间;忽略前三次运行以获得趋势。第一次运行总是比后续运行慢。

接下来列出了该应用,可以在本书页面的源代码下载部分获得:

`import java.nio.MappedByteBuffer; import java.io.OutputStream; import java.io.InputStream; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.EnumSet; import static java.nio.file.LinkOption.NOFOLLOW_LINKS;

public class Main {

 public static void deleteCopied(Path path){

  try {       Files.deleteIfExists(path);   } catch (IOException ex) {     System.err.println(ex);   }

 }

 public static void main(String[] args) {

 final Path copy_from = Paths.get("C:/rafaelnadal/tournaments/2009/videos/                                                                         Rafa Best Shots.mp4");  final Path copy_to = Paths.get("C:/Rafa Best Shots.mp4");  long startTime, elapsedTime;  int bufferSizeKB = 4; //also tested for 16, 32, 64, 128, 256 and 1024  int bufferSize = bufferSizeKB * 1024;

 deleteCopied(copy_to);

 //FileChannel and non-direct buffer  System.out.println("Using FileChannel and non-direct buffer ...");  try (FileChannel fileChannel_from = (FileChannel.open(copy_from,                       EnumSet.of(StandardOpenOption.READ))); FileChannel fileChannel_to = (FileChannel.open(copy_to,                       EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

      startTime = System.nanoTime();

      // Allocate a non-direct ByteBuffer       ByteBuffer bytebuffer = ByteBuffer.allocate(bufferSize);

      // Read data from file into ByteBuffer       int bytesCount;       while ((bytesCount = fileChannel_from.read(bytebuffer)) > 0) {        //flip the buffer which set the limit to current position, and position to 0                    bytebuffer.flip();        //write data from ByteBuffer to file        fileChannel_to.write(bytebuffer);        //for the next read        bytebuffer.clear();       }

      elapsedTime = System.nanoTime() - startTime;       System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds");  } catch (IOException ex) {    System.err.println(ex);  }

 deleteCopied(copy_to);

 //FileChannel and direct buffer  System.out.println("Using FileChannel and direct buffer ...");  try (FileChannel fileChannel_from = (FileChannel.open(copy_from,                       EnumSet.of(StandardOpenOption.READ)));       FileChannel fileChannel_to = (FileChannel.open(copy_to,                       EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

      startTime = System.nanoTime();

      // Allocate a direct ByteBuffer       ByteBuffer bytebuffer = ByteBuffer.allocateDirect(bufferSize);

      // Read data from file into ByteBuffer       int bytesCount;       while ((bytesCount = fileChannel_from.read(bytebuffer)) > 0) {        //flip the buffer which set the limit to current position, and position to 0                    bytebuffer.flip();        //write data from ByteBuffer to file        fileChannel_to.write(bytebuffer);        //for the next read        bytebuffer.clear();       }

      elapsedTime = System.nanoTime() - startTime;       System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) {    System.err.println(ex);  }

 deleteCopied(copy_to);

 //FileChannel.transferTo()  System.out.println("Using FileChannel.transferTo method ...");  try (FileChannel fileChannel_from = (FileChannel.open(copy_from,                       EnumSet.of(StandardOpenOption.READ)));       FileChannel fileChannel_to = (FileChannel.open(copy_to,                       EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

      startTime = System.nanoTime();

      fileChannel_from.transferTo(0L, fileChannel_from.size(), fileChannel_to);

      elapsedTime = System.nanoTime() - startTime;       System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds");  } catch (IOException ex) {    System.err.println(ex);  }

 deleteCopied(copy_to);

 //FileChannel.transferFrom()  System.out.println("Using FileChannel.transferFrom method ...");  try (FileChannel fileChannel_from = (FileChannel.open(copy_from,                       EnumSet.of(StandardOpenOption.READ)));       FileChannel fileChannel_to = (FileChannel.open(copy_to,                       EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

      startTime = System.nanoTime();

      fileChannel_to.transferFrom(fileChannel_from, 0L, (int) fileChannel_from.size());

      elapsedTime = System.nanoTime() - startTime;       System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds");  } catch (IOException ex) {    System.err.println(ex);  }

 deleteCopied(copy_to);

 //FileChannel.map  System.out.println("Using FileChannel.map method ...");  try (FileChannel fileChannel_from = (FileChannel.open(copy_from,                       EnumSet.of(StandardOpenOption.READ)));       FileChannel fileChannel_to = (FileChannel.open(copy_to,                       EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

      startTime = System.nanoTime(); MappedByteBuffer buffer = fileChannel_from.map(FileChannel.MapMode.READ_ONLY,                                                                   0, fileChannel_from.size());

      fileChannel_to.write(buffer);       buffer.clear();

      elapsedTime = System.nanoTime() - startTime;       System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) {   System.err.println(ex); }

deleteCopied(copy_to);

//Buffered Stream I/O System.out.println("Using buffered streams and byte array ..."); File inFileStr = copy_from.toFile(); File outFileStr = copy_to.toFile(); try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(inFileStr));      BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(outFileStr))) {

     startTime = System.nanoTime();

     byte[] byteArray = new byte[bufferSize];      int bytesCount;      while ((bytesCount = in.read(byteArray)) != -1) {              out.write(byteArray, 0, bytesCount);      }

     elapsedTime = System.nanoTime() - startTime;      System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) {   System.err.println(ex); }

deleteCopied(copy_to);

System.out.println("Using un-buffered streams and byte array ..."); try (FileInputStream in = new FileInputStream(inFileStr);      FileOutputStream out = new FileOutputStream(outFileStr)) {

     startTime = System.nanoTime();

     byte[] byteArray = new byte[bufferSize];      int bytesCount;      while ((bytesCount = in.read(byteArray)) != -1) {              out.write(byteArray, 0, bytesCount);      }

     elapsedTime = System.nanoTime() - startTime;      System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) {   System.err.println(ex); }

deleteCopied(copy_to);

System.out.println("Using Files.copy (Path to Path) method ..."); try {     startTime = System.nanoTime();

    Files.copy(copy_from, copy_to, NOFOLLOW_LINKS);

    elapsedTime = System.nanoTime() - startTime;     System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException e) {   System.err.println(e); }

deleteCopied(copy_to);

System.out.println("Using Files.copy (InputStream to Path) ..."); try (InputStream is = new FileInputStream(copy_from.toFile())) {

    startTime = System.nanoTime();

    Files.copy(is, copy_to);

    elapsedTime = System.nanoTime() - startTime;     System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException e) {   System.err.println(e); }

deleteCopied(copy_to);

System.out.println("Using Files.copy (Path to OutputStream) ..."); try (OutputStream os = new FileOutputStream(copy_to.toFile())) {

     startTime = System.nanoTime();

     Files.copy(copy_from, os);

     elapsedTime = System.nanoTime() - startTime;      System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds");  } catch (IOException e) {    System.err.println(e);  }  } }`

这个应用的输出很难进行分类,因为涉及的数字太多了,所以我绘制了一些数据,以便让您更清楚地看到几次比较的结果,如下面几节中的图所示。这些图中的 Y 轴是以秒表示的估计时间,X 轴是所用缓冲区的大小(或运行次数,跳过前三次运行后)。

文件通道和非直接缓冲区与文件通道和直接缓冲区

如图 7-11 所示,似乎对于小于 256KB 的缓冲区,非直接缓冲区要快得多,而对于大于 256KB 的缓冲区,直接缓冲区略快(参见图 7-11 )。

Image

图 7-11 文件通道和非直接缓冲区对比文件通道和直接缓冲区

filechannel . transferto()诉 filechannel . transferfrom()诉 FileChannel.map()

如图 7-12 中的所示,看起来transferTo()transferFrom()在连续七次运行后几乎相同,而FileChannel.map()是最慢的解决方案。

Image

图 7-12【filechannel . transfer()诉 filechannel . transferfrom()诉 FileChannel.map()

三种不同的 Files.copy()方法

如图图 7-13 所示,最快的Files.copy()方法是PathPath,其次是PathOutputStream,最后是InputStreamPath

Image

图 7-13 Files.copy()方法

FileChannel 和非直接缓冲区与 FileChannel.transferTo()与路径到路径

作为最终测试,我们从上述三张图中取出最快的结果,并将它们放在图 7-14 中。由于我们没有指定FileChannel.transferTo()PathPath的缓冲区大小,我们将七次运行的平均时间作为参考。如你所见,Files.copy()PathPath似乎是复制文件最快的解决方案。

Image

图 7-14 带非直接缓冲区的文件通道与文件通道. transferTo()与路径到路径的路径

总结

这一章从简单概述ByteBuffer类开始,它通常与SeekableByteChannelFileChannel一起使用。接下来详细介绍了SeekableByteChannel与应用的接口,这些应用将随机读写文件以完成不同类型的常见任务。然后,您看到了如何获得具有 RAF 功能的FileChannel,并发现了FileChannel提供的主要功能,包括将文件的一个区域直接映射到内存中以实现更快的访问,锁定文件的一个区域,以及在不影响通道当前位置的情况下从绝对位置读取和写入字节。本章以一个基准测试应用结束,该应用试图通过将FileChannel的功能与其他常用方法进行比较来确定复制文件的最快方法,例如Files.copy(),使用缓冲流和字节数组,以及使用非缓冲流和字节数组。

八、套接字 API

互联网诞生于 20 世纪 50 年代和 60 年代。几年后,大约在 80 年代,套接字的概念在 BSD(Berkeley Software Distribution——一种 Unix 变体)上被引入,用于使用互联网协议(IP)的进程之间的通信。几年后,在 1996 年,JDK 1.0 将套接字的概念带到了编程世界,作为易于使用和跨平台的网络通信模型。最后,程序员现在可以创建网络应用,而无需对网络通信进行多年的研究。Java 开发人员只需简单了解几个主题,如 IP、IP 地址、端口和 Java 网络,就可以编写一个简单的网络应用。

IP 将所有通信分成从源到目的地分别处理的个数据包(数据块)——没有交付保证。在 IP 之上,我们还有其他常见的协议,如 TCP(传输控制协议)和 UDP(用户数据报协议)(本章的应用利用了这些协议),在这些协议之上,我们还有更多,包括 HTTP、TELNET、DNS 等等。套接字利用 IP 进行机器之间的通信,因此 Java 网络应用可以使用它们预定义的协议与现有的服务器“对话”。

在互联网上,每台机器都可以通过一个数字标签来识别,这个数字标签被称为 IP 地址。每个 Java 开发人员都应该知道我们处理两种类型的 IP 地址:IPv4(用 32 位表示,例如 124.32.45.23)和 IPv6(用 128 位表示,例如 2607:f0d 0:1002:0051:0000:0000:0000:0000:0004)。而且,要知道 IP 地址被组织成 A、B、C、D、e 类是很重要的,由于我们对 D 类 IP 地址特别感兴趣,假设 IPv4s 地址在 224.0.0.1 和 239.255.255.255 之间变化,表示组播组。另外,记住地址 127.0.0.1 是为本地主机地址保留的。

集中在端口上,TCP/UDP 端口的范围在 0 到 65535 之间,它们在 Java 中用整数表示。某些类型的服务器通常位于某些端口上:例如,如果您连接到一台主机的端口 80,您可能会发现一个 HTTP 服务器。在端口 21 上,你可以期待一个 FTP 服务器,在端口 23 上,一个 Telnet 服务器,在端口 119 上,一个 NNTP 服务器,等等。因此,选择端口时要谨慎;确保不要干扰其他进程,并且保持在适当的范围内。

每个概念都有专门的书籍,但是对于创建 Java 客户机/服务器应用来说,这些信息已经足够了。在客户机/服务器模型中,服务器运行在主机上,监听端口,以接收来自网络上的客户机甚至同一台机器的连接请求。客户端使用 IP 地址(主机名)和端口来定位服务器,而服务器根据每个客户端的请求为其提供服务。在连接过程中,客户端通过一个本地端口号向服务器标识自己,该端口号可以由内核显式设置或分配—一个套接字绑定到这个本地端口号,以便在连接过程中使用(我们说客户端绑定到一个本地端口号)。接受之后,服务器获得一个绑定到新的本地端口号的新套接字,并将其远程端点设置为客户端的地址和端口——它需要一个新的端口号,以便可以继续侦听原始端口上的连接请求。一旦通信建立,数据可以在套接字之间来回传递,直到通信被故意关闭或意外中断。

我们可以得出结论,对于 Java 来说,套接字是服务器程序和它的客户端程序之间的双向软件端点,或者更一般地说,是在网络上运行的参与双向通信的两个程序之间的双向软件端点。一个端点是 IP 地址和端口号的组合。

Java 在 JDK 1.0 中引入了对套接字的支持,但是随着时间的推移,版本与版本之间的事情已经发生了变化。跳到 Java 7,NIO.2 通过用新方法更新现有的类和添加新的接口/类来编写基于 TCP/UDP 的应用,改进了这种支持。首先,NIO.2 引入了一个名为NetworkChannel的接口,它为所有网络通道类提供了方法公共——任何实现这个接口的通道都是网络套接字的通道。专用于同步套接字通道的主要类ServerSocketChannelSocketChannelDatagramChannel实现了这个接口,它提供了绑定和返回本地地址的方法,以及通过新的SocketOption<T>接口和StandardSocketOptions类设置和获取套接字选项的方法。该接口的方法和直接添加到类中的方法(用于检查连接状态、获取远程地址和关闭)将使您不必调用socket()方法。

NIO.2 还引入了MulticastChannel接口作为NetworkChannel.的子接口,顾名思义,MulticastChannel接口映射了一个支持 IP 组播的网络通道。请记住,MulticastChannel仅由数据报通道DatagramChannel实现。当加入一个组播组时,你会得到一个成员密钥,这是一个代表组播组成员的令牌。通过成员资格密钥,您可以阻止/解除阻止来自不同地址的数据报、删除成员资格、获取为其创建成员资格密钥的频道和/或多播组,等等。

Image 关于 Java 通道的简要概述,请看看第七章的的“通道简要概述”一节。此外,为了理解 Java 缓冲区是如何工作的,可以考虑“字节缓冲区概述”一节。

网络渠道概述

在本节中,我们将对NetworkChannel方法进行一个简短的概述。这个接口代表了一个网络套接字的通道,并附带了一组用于所有套接字的五个通用方法。我们在这里介绍它们,因为它们在接下来的部分会非常有用。

我们将从bind()方法开始,它将通道的套接字绑定到一个本地地址。更准确地说,该方法将在套接字和本地地址之间建立关联,本地地址通常被显式指定为一个InetSocketAddress实例(该类用 IP(或主机名)和端口表示套接字地址,并扩展了抽象的SocketAddress类)。如果我们将 null 传递给bind()方法,也可以自动分配本地地址。此方法用于将服务器套接字通道、套接字通道和数据报套接字通道与本地机器绑定。它将返回当前频道:

NetworkChannel bind(SocketAddress local) throws IOException

NetworkChannel可以通过调用getLocalAddress()方法提取绑定地址。如果通道的套接字未绑定,则它返回 null:

SocketAddress getLocalAddress() throws IOException
插座选项

NetworkChannel剩下的三个方法处理当前通道支持的套接字选项。与套接字关联的套接字选项由SocketOption<T>接口表示。目前,NIO.2 通过 StandardSocketOptions 类中的一组标准选项来实现这个接口。它们在这里:

  • IP_MULTICAST_IF:该选项用于指定面向数据报套接字发送多播数据报所使用的网络接口(NetworkInterface);如果是null,那么操作系统将选择输出接口(如果有的话)。默认为null,但是可以在 socket 绑定后设置该选项的值。当我们讨论发送数据报时,您将看到如何找出您的机器上有哪些多播接口。
  • IP_MULTICAST_LOOP:这个选项的值是一个布尔值,它控制多播数据报的回送(这取决于操作系统)。作为应用编写人员,您必须决定是否希望您发送的数据返回到您的主机。默认情况下,这是TRUE,但是该选项的值可以在套接字绑定后设置。
  • IP_MULTICAST_TTL:该选项的值是一个 0 到 255 之间的整数,代表面向数据报套接字发出的组播包的生存时间。如果没有另外指定,多播数据报以默认值 1 发送,以防止它们被转发到本地网络之外。通过这个选项,我们可以控制多播数据报的范围。默认情况下,该值设置为 1,但是该选项的值可以在套接字绑定后设置。
  • IP_TOS:该选项的值是一个整数,表示套接字发送的 IP 数据包中服务类型(ToS)八位字节的值——该值的解释因网络而异。目前,这仅适用于 IPv4,默认情况下,其值通常为 0。套接字绑定后,可以随时设置选项的值。
  • SO_BROADCAST:该选项的值为布尔值,表示是否允许发送广播数据报(特定于发送到 IPv4 广播地址的面向数据报的套接字)。默认为FALSE,但该选项的值可以随时设置。
  • SO_KEEPALIVE:该选项的值是一个布尔值,表示连接是否应该保持活动状态。默认设置为FALSE,但该选项的值可以随时设置。
  • SO_LINGER:该选项的值是一个整数,以秒为单位表示超时(逗留时间间隔)。当试图通过close()方法关闭阻塞模式套接字时,它将在传输未发送的数据(没有为非阻塞模式定义)之前等待逗留间隔的持续时间。默认情况下,它是一个负值,这意味着此选项被禁用。该选项的值可以随时设置,最大值取决于操作系统。
  • SO_RCVBUF:该选项的值是一个整数,表示套接字接收缓冲区——网络实现使用的输入缓冲区——的字节大小。默认情况下,该值取决于操作系统,但可以在套接字绑定或连接之前设置。根据操作系统的不同,该值可以在套接字绑定后更改。不允许负值。
  • SO_SNDBUF:该选项的值是一个整数,表示套接字发送缓冲区的字节大小——网络实现使用的输出缓冲区。默认情况下,该值取决于操作系统,但可以在套接字绑定或连接之前设置。根据操作系统的不同,该值可以在套接字绑定后更改。不允许负值。
  • SO_REUSEADDR:该选项的值是一个整数,表示地址是否可以重用。当我们希望多个程序绑定到同一个地址时,这在数据报多播中非常有用。在面向流的套接字的情况下,当先前的连接处于TIME_WAIT状态时,套接字可以被绑定到一个地址—TIME_WAIT意味着操作系统已经接收到关闭套接字的请求,但是等待来自客户端的可能的延迟通信。默认情况下,该选项的值依赖于操作系统,但是可以在套接字绑定或连接之前进行设置。
  • TCP_NODELAY:该选项的值是一个启用/禁用 Nagle 算法的整数(有关 Nagle 算法的更多信息,请参见[en.wikipedia.org/wiki/Nagle%27s_algorithm](http://en.wikipedia.org/wiki/Nagle%27s_algorithm))。默认为FALSE,但可以随时设置。

现在,设置和获取选项可以通过NetworkChannel.getOption()NetworkChannel.setOption()方法来完成:

<T> T getOption(SocketOption<T> name) throws IOException
<T> NetworkChannel setOption(SocketOption<T> name, T value) throws IOException

检索特定通道(网络套接字)的支持选项可以通过调用该通道上的NetworkChannel.supportedOptions()方法来完成:

Set<SocketOption<?>> supportedOptions()

编写 TCP 服务器/客户端应用

编写 TCP 教程远不是我们的目标,因为这是一个文档丰富的大主题,并且涉及许多技术概念和方面,但是我们将给出一个快速的概述。TCP 类似于电话连接,它通过套接字在两个端点之间建立连接,并且套接字在整个通信期间保持打开。TCP 的主要功能是提供点对点通信机制。一台机器上的一个进程与另一台机器上或同一台机器内的另一个进程通信。唯一的 TCP 连接由五个元素标识:服务器的 IP 地址和端口、客户端的 IP 地址和端口以及协议(TCP/IP、UDP 等。).服务器监听一个端口,并且可以同时与许多客户端通话。TCP 提供了许多涉及数据分组的优点(例如,优于 UDP)。TCP 负责许多重要的任务,包括将数据分成数据包、缓冲数据、跟踪丢失的数据包(用于重新发送丢失或乱序的数据包)以及根据应用处理能力控制数据传输的速度。而且,TCP 支持以字节数组或使用流的形式发送数据,这在 Java 中非常流行。

阻塞与非阻塞机制

当您决定编写一个 Java TCP 服务器/客户端应用时,您必须考虑您是需要编写一个阻塞应用还是非阻塞应用。这个决定很重要,因为实现是不同的,而且复杂性可能也很关键。

阻塞机制的主要特征是假定在完全接收到 I/O 之前,给定的线程不能再做任何事情,这在某些情况下可能需要一段时间——应用的流被阻塞,因为方法不会立即返回。另一方面,非阻塞机制会立即对 I/O 请求进行排队,并将控制权返回给应用流(方法会立即返回)。请求将由内核稍后处理。

从 Java 开发人员的角度来看,您还必须考虑这些机制所涉及的复杂程度。非阻塞机制实现起来比阻塞机制复杂得多,但是它们允许您获得更高的性能和可伸缩性。

Image 注意非阻塞机制不同于异步机制(尽管这经常被争论,取决于你问谁)。例如,在非阻塞环境中,如果不能快速返回答案,API 会立即返回一个错误,并且不做任何其他事情,而在异步环境中,API 总是立即返回,并开始在后台努力为您的请求提供服务。换句话说,使用非阻塞机制,函数不会在堆栈上等待,而使用异步机制,在调用离开堆栈后,工作可以代表函数调用继续进行。异步更熟悉并行(如线程),而非阻塞通常指轮询。

自 NIO 以来,阻塞和非阻塞模式都已经实现,但是我们将尝试用新的 NIO.2 特性来增加代码的趣味。

在接下来的部分中,我们将开发这两种类型的应用。让我们从使用阻塞机制的简单方法开始。

编写阻塞的 TCP 服务器

为了更好地理解如何完成这项任务,最简单的方法是遵循一组简单的步骤,并在讨论结束时将代码块粘在一起。我们想开发一个单线程阻塞的 TCP 服务器,它将把从它那里得到的所有信息反馈给客户端。实现这一点的许多步骤也可以转移到其他阻塞 TCP 服务器。

创建新的服务器套接字通道

第一步包括为面向流的监听套接字创建一个可选择的通道,这要感谢java.nio.channels.ServerSocketChannel类,它可以安全地供多个并发线程使用。更准确地说,这个任务是通过ServerSocketChannel.open()方法完成的,如下所示:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

请记住,新创建的服务器套接字通道没有绑定或连接。绑定和连接将在接下来的步骤中完成。

您可以通过调用ServerSocketChannel.isOpen()方法来检查服务器套接字是否已经打开或者已经成功打开,该方法返回相应的Boolean值:

if (serverSocketChannel.isOpen()) {
    ...
}
配置阻塞机制

如果服务器套接字通道已经成功打开,就该指定阻塞机制了。为此,我们调用接收一个boolean值的ServerSocketChannel.configureBlocking()方法。如果我们传递 true,那么将使用阻塞机制;如果我们通过false,那么非阻塞机制将被使用:

serverSocketChannel.configureBlocking(true);

注意,这个方法返回一个SelectableChannel对象,它代表一个可以通过Selector复用的通道。这在我们处于非阻塞模式时很有用;因此我们暂时忽略它。

设置服务器套接字通道选项

这是一个可选步骤。没有必需的选项(您可以使用缺省值),但是我们将显式地设置几个选项来向您展示如何做到这一点。更准确地说,一个服务器套接字通道支持两个选项:SO_RCVBUF and SO_REUSEADDR.我们将对它们都进行设置,如下所示:

serverSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
serverSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);

您可以通过调用继承的方法supportedOptions()找到服务器套接字通道支持的选项:

Set<SocketOption<?>> options = serverSocketChannel.supportedOptions();
for(SocketOption<?> option : options) System.out.println(option);
绑定服务器套接字通道

此时,我们可以将通道的套接字绑定到本地地址,并配置套接字来侦听连接。为此,我们称之为新的ServerSocketChannel.bind()方法(该方法在前面的“网络通道概述”一节中介绍过)。我们的服务器将在本地主机(127.0.0.1),端口 5555(任意选择)上等待传入的连接:

final int DEFAULT_PORT = 5555;
final String IP = "127.0.0.1";
serverSocketChannel.bind(new InetSocketAddress(IP, DEFAULT_PORT));

另一种常见的方法是创建一个InetSocketAddress对象,不指定 IP 地址,只指定端口(有一个构造函数)。在这种情况下,IP 地址是通配符地址,端口号是指定值。通配符地址是一个特殊的本地 IP 地址,只能在绑定操作中使用*,通常表示“任何”:*

*serverSocketChannel.bind(new InetSocketAddress(DEFAULT_PORT));

Image 警告当您使用 IP 通配符地址时,请注意避免任何不必要的复杂情况,如果您有多个具有独立 IP 地址的网络接口,可能会出现这种情况。在这种情况下,如果您不确定如何顺利完成,建议将套接字绑定到特定的网络地址,而不是使用通配符。

此外,还有一个bind()方法接收地址以绑定套接字和最大数量的挂起连接:

public abstract ServerSocketChannel bind(SocketAddress local,int pc) throws IOException

如果我们将 null 传递给bind()方法,也可以自动分配本地地址。还可以通过调用从NetworkChannel接口继承的ServerSocketChannel.getLocalAddress()方法找出绑定的本地地址。如果服务器套接字通道尚未绑定,则返回 null。

System.out.println(serverSocketChannel.getLocalAddress());
接受连接

在打开和绑定之后,我们最终到达验收里程碑。由于我们处于阻塞模式,接受连接将阻塞应用,直到有新的连接可用或发生 I/O 错误。我们通过调用ServerSocketChannel.accept()方法来表示对接受新连接的不耐烦。当新连接可用时,该方法返回新连接的客户端套接字通道(或简称为套接字通道)。这是SocketChannel类的一个实例,代表面向流的连接套接字的可选通道。

SocketChannel socketChannel = serverSocketChannel.accept();

Image 注意试图为一个未绑定的服务器套接字通道调用accept()方法会抛出一个NotYetBoundException异常。

一旦我们接受了一个新的连接,我们就可以通过调用SocketChannel.getRemoteAddress()方法找到远程地址。这个方法是 Java 7 (NIO.2)中的新方法,它返回这个通道的套接字所连接的远程地址:

System.out.println("Incoming connection from: " + socketChannel.getRemoteAddress());
通过连接传输数据

此时,服务器和客户端可以通过连接传输数据。它们可以发送和接收映射为字节数组的不同种类的数据包,或者使用流和标准的 Java 文件 I/O 机制。实现传输(发送/接收)是一个灵活且特定于实现的过程,因为它涉及许多方面。例如,对于我们的服务器,我们选择使用ByteBuffers,并且我们记住这是一个 echo 服务器——它从客户端读取的就是它写回的。下面是传输代码片段:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
...
while (socketChannel.read(buffer) != -1) {

       buffer.flip();

       socketChannel.write(buffer);

       if (buffer.hasRemaining()) {
           buffer.compact();
           } else {
                  buffer.clear();
           }
}

SocketChannel类为ByteBuffers.提供了一组read()/write()方法,因为它们非常直观,我们将只列出它们:

  • 从这个通道读取一个字节序列到给定的缓冲区。这些方法返回读取的字节数(可以是零),或者如果通道已经到达流尾,则返回–1:public abstract int read(ByteBuffer dst) throws IOException public final long read(ByteBuffer[] dsts) throws IOException public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException
  • 从给定的缓冲区向该通道写入一个字节序列。这些方法返回写入的字节数;可以为零:public abstract int write(ByteBuffer src) throws IOException public final long write(ByteBuffer[] srcs) throws IOException public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException
使用流代替缓冲区

如你所知,通道和缓冲区是非常好的朋友,但是如果你决定用 streams 代替(InputStream and OutputStream),那么你需要使用下面的代码;一旦获得了 I/O 流,就可以进一步探索标准的 Java 文件 I/O 机制。

InputStream in = socketChannel.socket().getInputStream();
OutputStream out = socketChannel.socket().getOutputStream();
关闭 I/O 连接

通过调用新的 NIO.2 SocketChannel.shutdownInput()SocketChannel.shutdownOutput()方法,可以在不关闭通道的情况下关闭 I/O 连接。关闭输入(或读取)连接将通过返回流结束指示符–1 来拒绝任何进一步的读取尝试。关闭输出(或写)连接将通过抛出一个ClosedChannelException异常来拒绝任何写尝试。

//shut down connection for reading
socketChannel.shutdownInput();

//shut down connection for writing
socketChannel.shutdownOutput();

如果您想在不关闭通道的情况下拒绝读/写尝试,这些方法非常有用。使用下面的代码可以检查某个连接当前是否因 I/O 而关闭:

boolean inputdown = socketChannel.socket().isInputShutdown();
boolean outputdown = socketChannel.socket().isOutputShutdown();
关闭频道

当一个通道变得无用时,它必须被关闭。为此,您可以调用SocketChannel.close()方法(这不会关闭服务器来监听传入的连接,它只是关闭一个客户端的通道)和/或ServerSocketChannel.close()方法(这将关闭服务器来监听传入的连接;其他客户端将无法再定位该服务器)。

serverSocketChannel.close();
socketChannel.close();

或者,我们可以通过将代码放入 Java 7 的 try-with-resources 特性来关闭这些资源——这是可能的,因为ServerSocketChannelSocketChannel类实现了AutoCloseable接口。使用此功能将确保资源自动关闭。如果您不熟悉 try-with-resources 特性,请查看[download.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html](http://download.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html)

将所有这些放入 Echo 服务器

现在我们已经拥有了创建 echo 服务器所需的一切。将前面的块放在一起,添加必要的导入和意大利面条代码,等等,将为我们提供下面的 echo 服务器:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final String IP = "127.0.0.1";

  ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

  //create a new server socket channel
  try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {

       //continue if it was successfully created
       if (serverSocketChannel.isOpen()) {

           //set the blocking mode
           serverSocketChannel.configureBlocking(true);
           //set some options
           serverSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
           serverSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
           //bind the server socket channel to local address
           serverSocketChannel.bind(new InetSocketAddress(IP, DEFAULT_PORT));

           //display a waiting message while ... waiting clients
           System.out.println("Waiting for connections ...");

           //wait for incoming connections
           while(true){
            try (SocketChannel socketChannel = serverSocketChannel.accept()) {
                System.out.println("Incoming connection from: " +
                                                            socketChannel.getRemoteAddress());

                //transmitting data
                while (socketChannel.read(buffer) != -1) {

                       buffer.flip();

                       socketChannel.write(buffer);

                       if (buffer.hasRemaining()) {
                           buffer.compact();
                       } else {
                           buffer.clear();
                       }
                }
            } catch (IOException ex) {                    
            }
           }

       } else {
         System.out.println("The server socket channel cannot be opened!");
       }
  } catch (IOException ex) {
    System.err.println(ex);
  }
}
}
编写阻塞 TCP 客户端

没有客户端的服务器有什么用?我们不想找出这个问题的答案,所以让我们为我们的 echo 服务器开发一个客户机。假设以下场景:客户机连接到我们的服务器,发送一个“Hello!”消息,然后继续发送 0 到 100 之间的随机数,直到生成数字 50。当生成数字 50 时,客户端停止发送并关闭通道。服务器将回显(写回)它从客户端读取的所有内容。现在我们有了一个场景,让我们看看实现它的步骤。

创建新的套接字通道

第一步是为面向流的连接套接字创建一个可选通道。这是通过java.nio.channels.SocketChannel类完成的,它对于多个并发线程来说是安全的。更准确地说,这个任务是通过SocketChannel.open()方法完成的,如下所示:

SocketChannel socketChannel = SocketChannel.open();

请记住,新创建的套接字通道没有连接。在单个镜头中创建和连接一个套接字通道需要调用SocketChannel.open(SocketAddress)方法。正如我们将要讨论的,也可以分两步完成。

您可以通过调用SocketChannel.isOpen()方法来检查服务器套接字是否已经打开或者已经成功打开,该方法返回相应的Boolean值:

if (socketChannel.isOpen()) {
    ...
}
配置阻塞机制

如果套接字通道已经成功打开,就该指定阻塞机制了。我们将传递真值,因为我们想要激活阻塞机制:

socketChannel.configureBlocking(true);
设置套接字通道选项

一个插座通道支持以下选项:SO_RCVBUFSO_LINGERIP_TOSSO_OOBINLINESO_REUSEADDRTCP_NODELAYSO_KEEPALIVESO_SNDBUF。其中一些如下所示:

socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
socketChannel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
socketChannel.setOption(StandardSocketOptions.SO_LINGER, 5);

您可以通过调用继承的方法supportedOptions()找到服务器套接字通道支持的选项:

Set<SocketOption<?>> options = socketChannel.supportedOptions();
for(SocketOption<?> option : options) System.out.println(option);
连接通道的插座

打开套接字通道(并可选地绑定它)后,您应该连接到远程地址(服务器端地址)。由于我们处于阻塞模式,连接到远程地址将阻塞应用,直到新的连接可用或发生 I/O 错误。通过调用SocketChannel.connect()方法并向其传递远程地址作为InetSocketAddress的一个实例来表明连接的意图,如下所示(记住我们的 echo 服务器运行在 127.0.0.1,端口 5555 上):

final int DEFAULT_PORT = 5555;
final String IP = "127.0.0.1";
socketChannel.connect(new InetSocketAddress(IP, DEFAULT_PORT));

该方法返回一个代表成功连接尝试的boolean值。您可以使用这个布尔值来检查连接的可用性,直到通过这个连接发送/接收数据包。此外,同样的检查可以通过调用SocketChannel.isConnected()方法来完成,如下所示:

if (socketChannel.isConnected()) {
    ...
}

Image 注意显然,在现实世界中,在应用中硬编码 IP 地址被认为是一种不好的做法。在这种情况下,客户端将只能与服务器在同一台机器上运行,这在某种程度上违背了远程通信的目的。在您的情况下,客户端可能使用服务器的主机名,而不是 IP 地址(可能通过 DNS 配置)。IP 地址经常变化,有时甚至通过 DHCP 动态分配。

通过连接传输数据

连接已经建立,所以我们可以开始传输数据包。下面的代码发送“Hello!”消息,然后发送随机数,直到生成数字 50。我们使用了ByteBufferCharBufferSocketChannel类的read()/write()方法(我们之前在开发服务器端代码时已经列出了这些方法,所以您应该已经熟悉了):

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
ByteBuffer helloBuffer = ByteBuffer.wrap("Hello !".getBytes());
ByteBuffer randomBuffer;
CharBuffer charBuffer;
Charset charset = Charset.defaultCharset();
CharsetDecoder decoder = charset.newDecoder();
...
socketChannel.write(helloBuffer);

while (socketChannel.read(buffer) != -1) {

       buffer.flip();

       charBuffer = decoder.decode(buffer);
       System.out.println(charBuffer.toString());

       if (buffer.hasRemaining()) {
           buffer.compact();
       } else {
           buffer.clear();
       }

       int r = new Random().nextInt(100);
       if (r == 50) {
           System.out.println("50 was generated! Close the socket channel!");
           break;
       } else {
           randomBuffer = ByteBuffer.wrap("Random number:"
                                           .concat(String.valueOf(r)).getBytes());
           socketChannel.write(randomBuffer);
       }
}
关闭频道

当一个通道变得无用时,它必须被关闭。为此,您可以调用SocketChannel.close(),客户端将与服务器断开连接:

socketChannel.close();

同样,Java 7 try-with-resources 特性可用于自动关闭。

将所有这些放入客户端

现在我们拥有了创建客户所需的一切。将所有需要的元素放在一起将为我们提供以下客户:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Random;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final String IP = "127.0.0.1";

  ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
  ByteBuffer helloBuffer = ByteBuffer.wrap("Hello !".getBytes());
  ByteBuffer randomBuffer;
  CharBuffer charBuffer;
  Charset charset = Charset.defaultCharset();
  CharsetDecoder decoder = charset.newDecoder();

  //create a new socket channel
  try (SocketChannel socketChannel = SocketChannel.open()) {

       //continue if it was successfully created
       if (socketChannel.isOpen()) {                

           //set the blocking mode
           socketChannel.configureBlocking(true);
           //set some options
           socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
           socketChannel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
           socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
           socketChannel.setOption(StandardSocketOptions.SO_LINGER, 5);
           //connect this channel's socket
           socketChannel.connect(new InetSocketAddress(IP, DEFAULT_PORT));

           //check if the connection was successfully accomplished
           if (socketChannel.isConnected()) {

               //transmitting data
               socketChannel.write(helloBuffer);

               while (socketChannel.read(buffer) != -1) {

                      buffer.flip();

                      charBuffer = decoder.decode(buffer);
                      System.out.println(charBuffer.toString());

                      if (buffer.hasRemaining()) {
                          buffer.compact();
                      } else {
                          buffer.clear();
                      }

                      int r = new Random().nextInt(100);
                      if (r == 50) {
                          System.out.println("50 was generated! Close the socket channel!");
                          break;
                      } else {
                          randomBuffer = ByteBuffer.wrap("Random number:".
                                                        concat(String.valueOf(r)).getBytes());
                          socketChannel.write(randomBuffer);
                      }
                }

           } else {
             System.out.println("The connection cannot be established!");
           }
       } else {
         System.out.println("The socket channel cannot be opened!");
       }
  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}
测试阻塞回显应用

测试应用是一项简单的任务。首先,启动服务器,等待直到您看到消息“正在等待连接...”。继续启动客户端并检查输出。以下是一些可能的服务器输出:


Waiting for connections ...

Incoming connection from: /127.0.0.1:49911

下面是一些可能的客户端输出:


Hello !

Random number:71

Random number:60

Random number:22

Random number:4

Random number:60

Random number:13

...

50 was generated! Close the socket channel!

编写一个非阻塞的 TCP 客户机/服务器应用

在我们开始开发之前,让我们对非阻塞 API 做一个简短的概述,它从 NIO 开始就可用了,所以它对您来说不应该是全新的。记住这一点,我们不会对你可能已经知道的事情进行过多的描述。

非阻塞套接字模式就是允许在通道上进行 I/O 操作,而不阻塞使用它的进程。故事从阻塞应用开始:服务器端是开放的,绑定到一个本地地址,并从客户端接收请求,客户端显然是开放的,连接到远程地址,并向服务器发送请求。

当所有非阻塞技术的主要实体——java.nio.channels.Selector类出现时,事情开始变得疯狂。一个Selector是通过一个无参数的open()方法创建的(Selector在 Java 7 中没有被修改)。基本上,这个类能够识别一个或多个通道何时可用于数据传输,并序列化请求以帮助服务器满足其客户端(它监视每个记录的套接字通道)。

此外,Selector在一个线程中处理多个套接字的 I/O 读/写操作,这要归功于一个被称为多路复用的概念——这解决了为每个套接字连接分配一个线程的问题。在 API 术语中,Selectorjava.nio.channels.SelectableChannels多路复用器,它可以通过register()方法注册(在ServerSocketChannelSocketChannel类中可用,它们是SelectableChannel)的间接子类,通过解除Selector分配给通道的资源来取消注册)。

使用 SelectionKey 类

如果你还在正轨上,那我们再深入一点!每次用Selector注册通道时,它都通过java.nio.channels.SelectionKey类的一个实例来表示,这些实例被称为选择键— Java 7 不修改这个类。可以把键想象成选择器用来对客户机请求进行排序的助手——每个助手(键)代表一个客户机子请求,包含识别客户机和请求类型(连接、读、写等)的信息。).注册时,我们指出了选择器,通常还指出了结果键的兴趣集(兴趣集标识了由Selector监控的键通道的操作)。密钥有四种可能的类型:

  • SelectionKey.OP_ACCEPT ( 可接受):关联的客户端请求连接(通常在服务器端创建,用于指示客户端需要连接)。
  • SelectionKey.OP_CONNECT ( 可连接):服务器接受连接(通常在客户端创建)。
  • SelectionKey.OP_READ ( 可读):表示读操作。
  • SelectionKey.OP_WRITE ( 可写):表示写操作。

选择器负责维护三组选择键:

  • key-set:包含代表该选择器当前频道注册的按键
  • selected-key:包含一组键,使得每个键的通道被检测到准备好用于在先前的选择操作期间在该键的兴趣组中识别的至少一个操作
  • cancelled-key:包含已取消但频道尚未注销的按键集合

Image 注意在新创建的选择器中,三个集合都是空的。选择器本身对于多个并发线程来说是安全的,但是它们的键集却不安全。

当战场上发生一些事情时,选择器醒来并创建相应的键(SelectionKey类的实例)。每个键保存关于发出请求的应用和请求类型(尝试/接受连接和读/写操作)的信息。

选择器等待进入无限循环的连接(等待选择器上记录的事件)。通常Selector.select()方法是循环中的第一行,它阻塞应用,直到至少选择了一个通道,调用了选择器的Selector.wakeup()方法,或者中断了当前线程——无论哪一个先发生。(此外,还有一个“select()超时”方法,以及一个名为selectNow().的非阻塞方法)

Selector等待客户机尝试连接,当这种情况发生时,服务器应用获得选择器创建的密钥。对于每个键,它检查类型(通过显式调用键上迭代器的remove()方法,从集合中删除每个处理过的键——这将防止相同的键再次出现)。这里搜索可接受的键,当SelectionKey.isAcceptable()方法返回 true 时,服务器通过调用accept()方法定位客户端套接字通道,将其设置为非阻塞,并使用OP_READ and/or OP_WRITE选项将其注册到选择器。

此时,客户端套接字通道注册到选择器以进行读/写操作。为了保持这种趋势,当客户端在套接字通道上写入数据时,选择器将告诉服务器有一些数据要读取——为此,SelectionKey.isReadable()方法返回 true。如果客户端试图从服务器读取数据,过程是类似的,但是服务器改为写入数据,并且SelectionKey.isWritable()方法返回true

图 8-1 显示了一个无阻塞流程图。

Image

***图 8-1。*选择器基础无阻塞流动。

所以,服务器已经准备好了!

Image 注意在非阻塞模式下,I/O 操作传输的字节可能比请求的少(部分读或写),或者可能根本没有字节。

使用选择器的方法

接下来,我们将回顾本节中调用的方法,以及接下来概述的其他一些方法(以下大部分描述摘自官方的 Java 7 Javadoc)。

  • Selector.open():创建一个新的选择器。
  • Selector.select():通过执行阻止选择操作来选择一组按键。
  • Selector.select(t):与选择相同,但仅在指定的毫秒内执行阻塞。如果时间到了,但没有可供选择的内容,则返回 0。
  • Selector.selectNow():与 select 相同,但具有非阻塞选择操作。如果没有可供选择的内容,它将返回 0。
  • Selector.selectedKeys():返回该选择器选择的按键集合为 Set < SelectionKey >。
  • Selector.keys():返回该选择器的按键设置为 Set < SelectionKey >。
  • Selector.wakeup():使尚未返回的第一次选择操作立即返回。
  • SelectionKey.isValid():检查密钥是否有效。如果键被取消、其通道被关闭或其选择器被关闭,则该键无效。
  • SelectionKey.isReadable():测试该键的通道是否可以读取。
  • SelectionKey.isWritable():测试该键的通道是否准备好写入。
  • SelectionKey.isAcceptable():测试这个键的通道是否准备好接受新的套接字连接。
  • SelectionKey.isConnectable():测试该按键的通道是否已经完成或者未能完成其套接字连接操作。
  • SelectionKey.cancel():请求取消该键的通道与其选择器的注册。
  • SelectionKey.interestOps():检索该键的兴趣集。
  • SelectionKey.interestOps(t):将该键的兴趣集设置为给定值。
  • SelectionKey.readyOps():检索该键的就绪操作设置。

此外,ServerSocketChannelSocketChannel包含register()方法,用于向给定的选择器注册当前频道并返回选择键。它获得选择器、结果键的兴趣集和结果键的附件(可能为空)。

public final SelectionKey register(Selector s,int p,Object a) throws ClosedChannelException
编写服务器

基于这些方法和前面的讨论,我们编写了下面的非阻塞 echo 服务器(对每个步骤都进行了注释,以帮助您更好地理解):

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class Main {

 private Map<SocketChannel, List<byte[]>> keepDataTrack = new HashMap<>();
 private ByteBuffer buffer = ByteBuffer.allocate(2 * 1024);

 private void startEchoServer() {

  final int DEFAULT_PORT = 5555;

  //open Selector and ServerSocketChannel by calling the open() method
  try (Selector selector = Selector.open();
       ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {

       //check that both of them were successfully opened
       if ((serverSocketChannel.isOpen()) && (selector.isOpen())) {

            //configure non-blocking mode
            serverSocketChannel.configureBlocking(false);

            //set some options
            serverSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 256 * 1024);
            serverSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);

            //bind the server socket channel to port
            serverSocketChannel.bind(new InetSocketAddress(DEFAULT_PORT));

            //register the current channel with the given selector  
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            //display a waiting message while ... waiting!
            System.out.println("Waiting for connections ...");

            while (true) {
                   //wait for incomming events
                   selector.select();

                   //there is something to process on selected keys
                   Iterator keys = selector.selectedKeys().iterator();

                   while (keys.hasNext()) {
                          SelectionKey key = (SelectionKey) keys.next();

                          //prevent the same key from coming up again
                          keys.remove();

                          if (!key.isValid()) {
                              continue;
                          }

                          if (key.isAcceptable()) {
                              acceptOP(key, selector);
                          } else if (key.isReadable()) {
                              this.readOP(key);
                          } else if (key.isWritable()) {
                              this.writeOP(key);
                          }
                   }
            }
       } else {
         System.out.println("The server socket channel or selector cannot be opened!");
       }

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }

 //isAcceptable returned true
 private void acceptOP(SelectionKey key, Selector selector) throws IOException {

  ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
  SocketChannel socketChannel = serverChannel.accept();
  socketChannel.configureBlocking(false);

  System.out.println("Incoming connection from: " + socketChannel.getRemoteAddress());

  //write a welcome message
  socketChannel.write(ByteBuffer.wrap("Hello!\n".getBytes("UTF-8")));

  //register channel with selector for further I/O
  keepDataTrack.put(socketChannel, new ArrayList<byte[]>());
  socketChannel.register(selector, SelectionKey.OP_READ);
  }

  //isReadable returned true
  private void readOP(SelectionKey key) {

   try {
       SocketChannel socketChannel = (SocketChannel) key.channel();

       buffer.clear();

       int numRead = -1;
       try {
           numRead = socketChannel.read(buffer);
       } catch (IOException e) {
         System.err.println("Cannot read error!");
       }

       if (numRead == -1) {
           this.keepDataTrack.remove(socketChannel);
           System.out.println("Connection closed by: " + socketChannel.getRemoteAddress());
           socketChannel.close();
           key.cancel();
           return;
       }

       byte[] data = new byte[numRead];
       System.arraycopy(buffer.array(), 0, data, 0, numRead);
       System.out.println(new String(data, "UTF-8") + " from " +      
                                                        socketChannel.getRemoteAddress());

       // write back to client
       doEchoJob(key, data);

   } catch (IOException ex) {
     System.err.println(ex);
   }
 }

 //isWritable returned true
 private void writeOP(SelectionKey key) throws IOException {

  SocketChannel socketChannel = (SocketChannel) key.channel();

  List<byte[]> channelData = keepDataTrack.get(socketChannel);
  Iterator<byte[]> its = channelData.iterator();

  while (its.hasNext()) {
         byte[] it = its.next();
         its.remove();
         socketChannel.write(ByteBuffer.wrap(it));
  }

  key.interestOps(SelectionKey.OP_READ);
 }

 private void doEchoJob(SelectionKey key, byte[] data) {

  SocketChannel socketChannel = (SocketChannel) key.channel();
  List<byte[]> channelData = keepDataTrack.get(socketChannel);
  channelData.add(data);

  key.interestOps(SelectionKey.OP_WRITE);
 }

 public static void main(String[] args) {
  Main main = new Main();
  main.startEchoServer();
 }
}
编写客户端

关注客户端,结构几乎是相同的,只有一些不同:

  • 首先,用SelectionKey.OP_CONNECT选项注册客户机套接字通道,因为客户机希望在服务器接受连接时得到选择器的通知。
  • 第二,客户端不会无限尝试连接,因为服务器可能不是活动的;因此,带有超时的Selector.select()方法对它来说是合适的(500 到 1000 毫秒的超时将完成这项工作)。
  • 第三,客户端必须检查密钥是否可连接(即SelectionKey.isConnectable()方法是否返回true)。如果这个键是可连接的,它会在一个条件语句中混合使用套接字通道isConnectionPending()finishConnect()方法来关闭挂起的连接。当您需要判断这个通道上是否正在进行连接操作时,调用SocketChannel.isConnectionPending()方法,该方法返回一个Boolean值。此外,通过SocketChannel.finishConnect()方法可以完成连接插座通道的过程。

最后,客户机为 I/O 操作做好了准备。我们重现了阻塞客户机/服务器应用中的相同场景:客户机连接到我们的服务器并发送一个“Hello!”消息,然后继续发送 0 到 100 之间的随机数,直到生成数字 50。生成 50 时,客户端停止发送并关闭通道。服务器将回显(写回)它从客户端读取的所有内容。

import java.io.IOException;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final String IP = "127.0.0.1";

  ByteBuffer buffer = ByteBuffer.allocateDirect(2 * 1024);
  ByteBuffer randomBuffer;
  CharBuffer charBuffer;

  Charset charset = Charset.defaultCharset();
  CharsetDecoder decoder = charset.newDecoder();

  //open Selector and ServerSocketChannel by calling the open() method
  try (Selector selector = Selector.open();
       SocketChannel socketChannel = SocketChannel.open()) {

       //check that both of them were successfully opened
       if ((socketChannel.isOpen()) && (selector.isOpen())) {                

            //configure non-blocking mode
            socketChannel.configureBlocking(false);
            //set some options

            socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
            socketChannel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
            socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);

            //register the current channel with the given selector
            socketChannel.register(selector, SelectionKey.OP_CONNECT);

            //connect to remote host
            socketChannel.connect(new java.net.InetSocketAddress(IP, DEFAULT_PORT));

            System.out.println("Localhost: " + socketChannel.getLocalAddress());

            //waiting for the connection
            while (selector.select(1000) > 0) {

                   //get keys
                   Set keys = selector.selectedKeys();
                   Iterator its = keys.iterator();

                   //process each key
                   while (its.hasNext()) {
                          SelectionKey key = (SelectionKey) its.next();

                          //remove the current key
                          its.remove();

                          //get the socket channel for this key
                          try (SocketChannel keySocketChannel=(SocketChannel) key.channel()) {

                               //attempt a connection
                               if (key.isConnectable()) {

                                   //signal connection success
                                   System.out.println("I am connected!");

                                   //close pending connections
                                   if (keySocketChannel.isConnectionPending()) {
                                       keySocketChannel.finishConnect();
                                   }

                                   //read/write from/to server
                                   while (keySocketChannel.read(buffer) != -1) {

                                          buffer.flip();

                                          charBuffer = decoder.decode(buffer);
                                          System.out.println(charBuffer.toString());

                                          if (buffer.hasRemaining()) {
                                              buffer.compact();
                                          } else {
                                              buffer.clear();

                                          }

                                          int r = new Random().nextInt(100);
                                          if (r == 50) {
                                              System.out.println("50 was generated! Close
                                                                    the socket channel!");
                                              break;
                                          } else {
                                            randomBuffer = ByteBuffer.wrap("Random number:"  
                                             .concat(String.valueOf(r)).getBytes("UTF-8"));
                                            keySocketChannel.write(randomBuffer);
                                            try {
                                                Thread.sleep(1500);
                                            } catch (InterruptedException ex) {                                            
                                            }
                                          }
                                   }
                               }
                          } catch (IOException ex) {
                            System.err.println(ex);
                          }
                   }
            }
       } else {
         System.out.println("The socket channel or selector cannot be opened!");
       }
  } catch (IOException ex) {
    System.err.println(ex);
  }

  }
}
测试无阻塞回显应用

测试应用是一项简单的任务。首先,启动服务器,等待直到您看到消息“正在等待连接…”继续启动一组客户机并检查输出。图 8-2 显示了一个运行服务器和三个客户端实例的例子。

Image

图 8-2。非阻塞服务器回显应用输出。

图 8-3 显示了客户端 2 的输出。

Image

图 8-3。非阻塞客户端回显应用输出

请记住,即使它看起来像一个多线程应用,这也是一个基于多路复用技术的单线程应用。

编写 UDP 服务器/客户端应用

既然 TCP 已经有了它的辉煌时刻,现在是 UDP 引起我们注意的时候了。UDP 建立在 IP 之上,有几个重要的特征。首先,包的大小被限制在单个 IP 包所能容纳的数量内——最多 65507 字节;这是 65535 字节的 IP 数据包大小减去 20 字节的最小 IP 报头,再减去 8 字节的 UDP 报头。此外,每个数据包都是独立的,被单独处理(没有数据包知道其他数据包)。此外,数据包可以以任何顺序到达,其中一些可能会在发送者不知情的情况下丢失,或者它们到达的速度比处理速度快或慢——不能保证以特定的顺序发送/接收数据,也不能保证发送的数据会被接收到。

由于发送方无法跟踪数据包的路由,每个数据包都封装了远程 IP 地址和端口。如果说 TCP 像一部电话,那么 UDP 就像一封信。发送方将接收方地址(远程 IP 和端口)和发送方地址(本地 IP 和端口)写在信封(UDP 包)上,将信件(要发送的数据)放入信封,然后发送信件。他不知道这封信是否会到达收信人那里。此外,较新的信可能比旧的信到达得快,而信可能根本就不会到达——这些信彼此并不知道。请记住,TCP 用于高可靠性的数据传输,而 UDP 用于低开销的传输。通常,在可靠性不重要但速度重要的应用中使用 UDP。当顺序不重要并且您不需要将所有消息发送到另一台机器时,UDP 非常适合从一个系统向另一个系统发送消息。

在下一节中,我们将编写一个基于 UDP 的单线程阻塞客户机/服务器应用。我们将从服务器端开始。

编写 UDP 服务器

为了帮助您理解,我们将把开发过程分成几个独立的步骤,并将 NIO.2 旨在提高性能和简化开发的特性放在前面。同样,我们将编写一个 echo 服务器和一个客户机,向它发送一些文本并接收它的返回。

创建面向服务器数据报的套接字通道

编写客户机/服务器 UDP 应用的整个过程涉及到java.nio.channels.DatagramChannel类,它代表面向数据报套接字的线程安全可选通道。因此,我们将通过创建一个新的DatagramChannel来启动我们的服务器,这可以通过调用 NIO.2 DatagramChannel.open()方法来完成。这个方法得到一个称为协议 参数的参数,它实际上是一个java.net.ProtocolFamily对象。这个接口是 NIO.2 中的新特性,它代表了一系列通信协议——目前它有一个名为java.net.StandardProtocolFamily的实现,并定义了两个枚举常量:

  • StandardProtocolFamily.INETIP 版本 4 (IPv4)
  • StandardProtocolFamily.INET6IP 版本 6 (IPv6)

因此,我们可以为 IPv4 创建一个面向服务器数据报的套接字,如下所示:

DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET);

旧的 NIO 无参数DatagramChannel.open()方法仍然可用,并且可以使用,因为它没有被废弃。但是在这种情况下,通道套接字的ProtocolFamily依赖于平台(配置),因此是未指定的。

您可以通过调用DatagramChannel.isOpen()方法来检查面向数据报的套接字通道是否已经打开或者已经成功打开,该方法返回相应的Boolean值:

if (datagramChannel.isOpen()) {
    ...
}

可以用同样的方式创建和检查面向客户端数据报的套接字通道。

设置面向数据报的套接字通道选项

面向数据报的套接字通道支持以下选项(尽管大多数情况下可以使用默认值):SO_REUSEADDRSO_BROADCASTIP_MULTICAST_LOOPSO_SNDBUFIP_MULTICAST_TTLIP_TOSIP_MULTICAST_IFSO_RCVBUF。例如,我们可以将网络实施使用的输入和输出缓冲区设置如下:

datagramChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
datagramChannel.setOption(StandardSocketOptions.SO_SNDBUF, 4 * 1024);

注意,您可以通过调用继承的方法supportedOptions()找到面向数据报的套接字通道的支持选项:

Set<SocketOption<?>> options = datagramChannel.supportedOptions();
for(SocketOption<?> option : options) System.out.println(option);
绑定面向数据报的套接字通道

此时,我们可以将通道的套接字绑定到本地地址,并配置套接字来侦听连接。为此,我们称之为新的DatagramChannel.bind()方法(该方法在前面的“网络通道概述”一节中介绍过)。我们的服务器将在本地主机(127.0.0.1),端口 5555(任意选择)上等待传入的连接:

final int LOCAL_PORT = 5555;
final String LOCAL_IP = "127.0.0.1";
datagramChannel.bind(new InetSocketAddress(LOCAL_IP, LOCAL_PORT));

也可以使用通配符地址:

datagramChannel.bind(new InetSocketAddress(LOCAL_PORT));

如果我们将 null 传递给bind()方法,也可以自动分配本地地址。还可以通过调用从NetworkChannel接口继承的ServerSocketChannel.getLocalAddress()方法找出绑定的本地地址。如果服务器套接字通道尚未绑定,则返回 null。

System.out.println(serverSocketChannel.getLocalAddress());
传输数据包

此时,我们的服务器已经准备好接收和发送数据包。由于 UDP 是一种无连接的网络协议,您不能像从其他通道那样默认读写一个DatagramChannel——稍后,您将看到如何通过 UDP 建立连接。相反,你使用DatagramChannel.send()DatagramChannel.receive()方法发送和接收数据包。

当您发送一个包时,您向send()方法传递一个ByteBuffer,它包含珍贵的数据和远程地址(服务器或客户机的,取决于谁在发送)。根据官方文档,这是如何工作的(参见[download.oracle.com/javase/7/docs/api/)](http://download.oracle.com/javase/7/docs/api/)):

如果该通道处于非阻塞模式,并且底层输出缓冲区中有足够的空间,或者如果该通道处于阻塞模式,并且有足够的空间可用,那么给定缓冲区中的剩余字节将作为单个数据报传输到给定的目标地址。这个方法可以在任何时候调用。但是,如果另一个线程已经在这个通道上启动了写操作,那么这个方法的调用将被阻塞,直到第一个操作完成。如果这个通道的套接字没有绑定,那么这个方法将首先使套接字绑定到一个自动分配的地址,就像通过调用带有参数nullbind()方法一样。

该方法将返回发送的字节数。

当您接收到一个包时,您向receive()方法传递数据报将被传输到的缓冲区(ByteBuffer)。同样,根据文档,它是这样工作的(参见[download.oracle.com/javase/7/docs/api/](http://download.oracle.com/javase/7/docs/api/)):

如果一个数据报立即可用,或者如果该通道处于阻塞模式并且最终有一个数据报可用,那么该数据报被复制到给定的字节缓冲区,并且其源地址被返回。如果该通道处于非阻塞模式,并且数据报不立即可用,则该方法立即返回 null。这个方法可以在任何时候调用。但是,如果另一个线程已经在这个通道上启动了一个读操作,那么这个方法的调用将被阻塞,直到第一个操作完成。如果这个通道的套接字没有绑定,那么这个方法将首先使套接字绑定到一个自动分配的地址,就像通过调用带有参数nullbind()方法一样。

该方法将返回数据报的源地址,或者如果该通道处于非阻塞模式并且没有数据报立即可用,则返回null。远程地址可用于找出向何处发送应答包。

另外,可以通过调用DatagramChannel.getRemoteAddress()方法找出远程地址。这个方法是 Java 7 (NIO.2)中的新方法,它返回这个通道的套接字所连接的远程地址——记住,对于 UDP 无连接的情况,这个方法返回null:

System.out.println("Connected to: " + datagramChannel.getRemoteAddress());

我们的数据报回显服务器将以阻塞模式(默认)在无限循环中监听传入的数据包,当数据包到达时,它将从中提取远程地址和数据。数据根据远程地址发送回来:

final int MAX_PACKET_SIZE = 65507;
ByteBuffer echoText = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);
...
while (true) {

       SocketAddress clientAddress = datagramChannel.receive(echoText);

       echoText.flip();
       System.out.println("I have received " + echoText.limit() + " bytes from " +
                                        clientAddress.toString() + "! Sending them back ...");
       datagramChannel.send(echoText, clientAddress);
       echoText.clear();
}
关闭数据报通道

当数据报通道变得无用时,必须将其关闭。为此,您可以调用DatagramChannel.close()方法:

datagramChannel.close();

同样,Java 7 的 try-with-resources 特性可以用于自动关闭。

将所有内容放入服务器

现在我们已经拥有了创建服务器所需的一切。将前面的所有信息放在一起,我们将得到以下服务器:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.StandardProtocolFamily;
import java.nio.channels.DatagramChannel;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;

public class Main {

public static void main(String[] args) {
  final int LOCAL_PORT = 5555;
  final String LOCAL_IP = "127.0.0.1";  //modify this to your local IP      
  final int MAX_PACKET_SIZE = 65507;

  ByteBuffer echoText = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);

  //create a new datagram channel
  try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {

       //check if the channel was successfully opened
       if (datagramChannel.isOpen()) {

           System.out.println("Echo server was successfully opened!");
           //set some options
           datagramChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
           datagramChannel.setOption(StandardSocketOptions.SO_SNDBUF, 4 * 1024);
           //bind the channel to local address
           datagramChannel.bind(new InetSocketAddress(LOCAL_IP, LOCAL_PORT));
           System.out.println("Echo server was binded on:"+datagramChannel.getLocalAddress());
           System.out.println("Echo server is ready to echo ...");

           //transmitting data packets
           while (true) {

                  SocketAddress clientAddress = datagramChannel.receive(echoText);

                  echoText.flip();
                  System.out.println("I have received " + echoText.limit() + " bytes from " +
                                        clientAddress.toString() + "! Sending them back ...");
                  datagramChannel.send(echoText, clientAddress);
                  echoText.clear();
           }
       } else {
         System.out.println("The channel cannot be opened!");
       }
  } catch (Exception ex) {
           if (ex instanceof ClosedChannelException) {
               System.err.println("The channel was unexpected closed ...");
           }
           if (ex instanceof SecurityException) {
               System.err.println("A security exception occured ...");
           }
           if (ex instanceof IOException) {
               System.err.println("An I/O error occured ...");
           }

           System.err.println("\n" + ex);
  }
 }
}
编写无连接 UDP 客户端

编写无连接的 UDP 客户端类似于编写 UDP 服务器。在以与前面相同的方式创建了一个新的DatagramChannel并设置了您需要的任何选项之后,您就可以开始发送和接收数据包了。面向客户端数据报的套接字通道不必绑定到本地地址,因为服务器将从每个收到的数据包中提取 IP 地址和端口,换句话说,它知道客户端住在哪里。此外,如果这个通道的套接字没有绑定,那么send()receive()方法将首先使套接字(客户端或服务器)绑定到一个自动分配的地址,就像通过调用带有参数nullbind()方法一样。但是请记住,如果服务器端是自动绑定的(不是显式绑定的),那么客户端应该知道所选择的地址(或者更准确地说,知道所选择的 IP 地址和端口)。如果服务器发送第一个数据分组,反之亦然。

我们的客户知道服务器的地址是 127.0.0.1,端口是 5555;因此,它发送第一个数据包,并从中接收答案。这是代码:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardProtocolFamily;
import java.nio.channels.DatagramChannel;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

public class Main {

public static void main(String[] args) throws IOException {

  final int REMOTE_PORT = 5555;
  final String REMOTE_IP = "127.0.0.1"; //modify this accordingly if you want to test remote
  final int MAX_PACKET_SIZE = 65507;

  CharBuffer charBuffer = null;
  Charset charset = Charset.defaultCharset();
  CharsetDecoder decoder = charset.newDecoder();
  ByteBuffer textToEcho = ByteBuffer.wrap("Echo this: I'm a big and ugly server!".getBytes());
  ByteBuffer echoedText = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);

  //create a new datagram channel
  try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {

       //check if the channel was successfully opened
       if (datagramChannel.isOpen()) {

           //set some options
           datagramChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
           datagramChannel.setOption(StandardSocketOptions.SO_SNDBUF, 4 * 1024);

           //transmitting data packets
           int sent = datagramChannel.send(textToEcho,
                                           new InetSocketAddress(REMOTE_IP, REMOTE_PORT));
           System.out.println("I have successfully sent "+sent+ " bytes to the Echo Server!");

           datagramChannel.receive(echoedText);

           echoedText.flip();
           charBuffer = decoder.decode(echoedText);
           System.out.println(charBuffer.toString());
           echoedText.clear();

       } else {
         System.out.println("The channel cannot be opened!");

       }
  } catch (Exception ex) {
    if (ex instanceof ClosedChannelException) {
        System.err.println("The channel was unexpected closed ...");
    }
    if (ex instanceof SecurityException) {
        System.err.println("A security exception occured ...");
    }
    if (ex instanceof IOException) {
        System.err.println("An I/O error occured ...");
    }

    System.err.println("\n" + ex);
  }
 }
}
测试 UDP 无连接回显应用

测试应用是一项简单的任务。首先,启动服务器并等待,直到您看到以下消息:

Echo server was successfully opened!
Echo server was binded on: /127.0.0.1:5555
Echo server is ready to echo ...

然后启动客户机并检查输出。以下是 UDP 服务器的一些可能输出:


Echo server was successfully opened!

Echo server was binded on: /127.0.0.1:5555

Echo server is ready to echo ...

I have received 37 bytes from /127.0.0.1:49155! Sending them back ...

下面是一些可能的 UDP 客户端输出:


I have successfully sent 37 bytes to the Echo Server!

Echo this: I'm a big and ugly server!

Image 注意完成测试后不要忘记手动停止 UDP 服务器!

编写连接的 UDP 客户端

如果你想使用DatagramChannel.read()DatagramChannel.write()方法(基于ByteBuffer s),而不是send()receive(),你需要写一个连接的 UDP 客户端。在连接客户端的场景中,通道的套接字被配置为只从/向给定的远程对等地址接收/发送数据报。连接建立后,数据包可能无法从任何其他地址接收/发送到任何其他地址。面向数据报的套接字保持连接,直到它被显式断开或关闭。

这种类型的客户端必须显式调用DatagramChannel.connect()方法,并向其传递服务器端远程地址,如下所示:

final int REMOTE_PORT = 5555;
final String REMOTE_IP = "127.0.0.1";
datagramChannel.connect(new InetSocketAddress(REMOTE_IP, REMOTE_PORT));

注意,与SocketChannel.connect()方法不同,这种方法实际上并不通过网络发送/接收任何数据包,因为 UDP 是一种无连接协议——这种方法返回非常快,并且不会阻塞应用。这里不需要一个finishConnect()或者isConnectionPending()方法。此方法可以在任何时候调用,因为它不会影响调用时已经在进行的读/写操作。如果这个通道的套接字没有被绑定,那么这个方法将首先导致套接字被绑定到一个自动分配的地址,就像调用带有参数nullbind()方法一样。

您可以通过调用DatagramChannel.isConnected()方法来检查连接状态。将返回一个相应的boolean值(如果该通道的套接字打开并连接,则返回true):

if (datagramChannel.isConnected()) {
    ...
}

以下应用是我们的 UDP echo 服务器的 UDP 连接客户端。它连接到远程地址,并使用read() / write()方法传输数据:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardProtocolFamily;
import java.nio.channels.DatagramChannel;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

public class Main {

public static void main(String[] args) throws IOException {

  final int REMOTE_PORT = 5555;
  final String REMOTE_IP = "127.0.0.1"; //modify this accordingly if you want to test remote
  final int MAX_PACKET_SIZE = 65507;

  CharBuffer charBuffer = null;
  Charset charset = Charset.defaultCharset();

  CharsetDecoder decoder = charset.newDecoder();
  ByteBuffer textToEcho = ByteBuffer.wrap("Echo this: I'm a big and ugly server!".getBytes());
  ByteBuffer echoedText = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);

  //create a new datagram channel
  try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {

       //set some options
       datagramChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
       datagramChannel.setOption(StandardSocketOptions.SO_SNDBUF, 4 * 1024);

       //check if the channel was successfully opened
       if (datagramChannel.isOpen()) {

           //connect to remote address
           datagramChannel.connect(new InetSocketAddress(REMOTE_IP, REMOTE_PORT));

           //check if the channel was successfully connected
           if (datagramChannel.isConnected()) {

               //transmitting data packets
               int sent = datagramChannel.write(textToEcho);
               System.out.println("I have successfully sent "+sent
                                                             +" bytes to the Echo Server!");

               datagramChannel.read(echoedText);

               echoedText.flip();
               charBuffer = decoder.decode(echoedText);
               System.out.println(charBuffer.toString());
               echoedText.clear();

           } else {
             System.out.println("The channel cannot be connected!");
           }
       } else {
         System.out.println("The channel cannot be opened!");
       }
  } catch (Exception ex) {
    if (ex instanceof ClosedChannelException) {
        System.err.println("The channel was unexpected closed ...");
    }
    if (ex instanceof SecurityException) {
        System.err.println("A security exception occured ...");
    }
    if (ex instanceof IOException) {
        System.err.println("An I/O error occured ...");
    }

    System.err.println("\n" + ex);
  }
 }
}

The well-known read()/write() methods are available in DatagramChannel:
  • 将字节序列从该通道读入给定的缓冲区。这些方法返回读取的字节数(可以是零),如果通道已经到达流的末尾,则返回–1:public abstract int read(ByteBuffer dst) throws IOException public final long read(ByteBuffer[] dsts) throws IOException public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException
  • 从给定的缓冲区向该通道写入一个字节序列。这些方法返回写入的字节数;可以为零:public abstract int write(ByteBuffer src) throws IOException public final long write(ByteBuffer[] srcs) throws IOException public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException
测试 UDP 连接回显应用

测试应用是一项简单的任务。首先,启动服务器,等待看到以下消息:


Echo server was successfully opened!

Echo server was binded on: /127.0.0.1:5555

Echo server is ready to echo ...

然后启动客户机并检查输出。UDP 服务器输出如下所示:


Echo server was successfully opened!

Echo server was binded on: /127.0.0.1:5555

Echo server is ready to echo ...

I have received 37 bytes from /127.0.0.1:57374! Sending them back ...

下面是 UDP 客户端的输出:


I have successfully sent 37 bytes to the Echo Server!

Echo this: I'm a big and ugly server!

组播

你可能已经熟悉了术语多播。但是,如果你不是,让我们对这个概念有一个简短的概述。没有学术上的描述和定义,可以把多播看作是广播的互联网版本。例如,电视台从一个信号源广播信号,但信号可以到达信号区域内的每个人,只有那些没有合适设备或拒绝接收信号的人才能接收不到信号。

在计算机世界中,电视台可以被翻译成一个主节点或机器,它将数据报传播到一组目的主机。这要归功于多播传输服务,它在一次呼叫中将数据报从一个源发送到多个接收者——这与单播传输服务相反,后者专用于基于点对点连接的高级网络协议,需要复制单播来将相同的数据发送到多个点(实际上,它将数据的副本发送到每个点)。

多播引入了代表数据报接收者的组的概念。组由 D 类 IP 地址标识(多播组 IPv4 地址在 224.0.0.1 和 239.255.255.255 之间)。当一个新的接收者(客户端)想要加入一个多播组时,它需要通过相应的 IP 地址连接到该组,并监听传入的数据报。

许多现实生活中的案例可以基于多播进行编程,例如在线会议、新闻发布、广告、电子邮件组和数据共享管理。

接下来,我们将讨论 NIO.2 对多播的贡献。

组播频道概述

NIO.2 附带了一个新的接口,用于映射支持 IP 多播的网络通道。这是java.nio.channels.MulticastChannel界面。在 API 层,这是本章前面介绍的NetworkChannel接口的子接口,它由一个类实现:?? 类。

基本上,它定义了两个join()方法和一个close()方法。聚焦于join()方法,这里有一个简短的概述:

  • 第一个join()方法由想要加入多播组以接收传入数据报的客户端调用。我们需要传递该组的 IP 地址和加入该组的网络接口(您将很快看到如何检查您的机器是否有支持多播的网络接口)。如果指示的组被成功加入,该方法返回一个MembershipKey实例。这是 NIO.2 中的新特性,它是一个代表 ip 多播组成员资格的令牌(见下一节)。MembershipKey join(InetAddress g, NetworkInterface i) throws IOException
  • 第二种join()方法也用于加入多播组。然而,在这种情况下,我们指出了一个源地址,组成员可以从该源地址开始接收数据报。成员资格是累积的,这意味着该方法可以用同一个组和接口再次调用,以接收由其他源地址发送到该组的数据报。MembershipKey join(InetAddress g, NetworkInterface i, InetAddress s) throws IOException

Image 注意一个组播通道可以加入几个组播组,包括多个接口上的同一个组。

close()方法用于删除成员资格(如果加入了任何组)并关闭通道。

会员密钥概述

当您加入一个多播组时,您会得到一个成员密钥,该密钥可用于在该组内执行不同种类的操作。最常见的如下所示:

  • 阻塞/解除阻塞:您可以通过调用block()方法并传递源地址来阻塞从特定来源发送的数据报。而且,您可以通过使用相同的地址调用unblock()方法来解锁被阻塞的源。
  • Get group :通过调用无参数的group()方法,可以获得为其创建成员密钥的组播组的源地址。这个方法返回一个InetAddress对象。
  • 获取通道:您可以通过调用无参数方法channel()来获取为其创建这个成员键的通道。这个方法返回一个MulticastChannel对象。
  • 获取源地址:如果成员键是特定于源的(只接收来自特定源地址的数据报),您可以通过调用无参数sourceAddress()方法来获取源地址。这个方法返回一个InetAddress对象。
  • 获取网络接口:您可以通过调用无参数networkInterface()方法来获取为其创建该成员密钥的网络接口。这个方法返回一个NetworkInterface对象。
  • 检查有效性:您可以通过调用isValid()方法来检查成员资格是否有效。这个方法返回一个boolean值。
  • Drop :您可以通过调用无参数drop()方法来删除成员资格(通道将不再接收发送到该组的任何数据报)。

成员资格密钥在创建时有效,并且在使用drop()方法删除成员资格或关闭通道之前一直有效。

网络接口概述

NetworkInterface类代表一个网络接口,它由一个名称和分配给该接口的 IP 地址列表组成。它用于标识多播组加入的本地接口。例如,以下代码将返回在您的计算机上找到的所有网络接口的信息:

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;

public class Main {

public static void main(String argv[]) throws Exception {

  Enumeration enumInterfaces = NetworkInterface.getNetworkInterfaces();
  while (enumInterfaces.hasMoreElements()) {
     NetworkInterface net = (NetworkInterface) enumInterfaces.nextElement();
     System.out.println("Network Interface Display Name: " + net.getDisplayName());
     System.out.println(net.getDisplayName() + " is up and running ?" + net.isUp());
     System.out.println(net.getDisplayName()+" Supports Multicast: "+net.supportsMulticast());
     System.out.println(net.getDisplayName() + " Name: " + net.getName());
     System.out.println(net.getDisplayName() + " Is Virtual:  " + net.isVirtual());
     System.out.println("IP addresses:");
     Enumeration enumIP = net.getInetAddresses();
     while (enumIP.hasMoreElements()) {
        InetAddress ip = (InetAddress) enumIP.nextElement();
        System.out.println("IP address:" + ip);
     }
  }
 }
}

该应用将返回在您的机器上找到的所有网络接口,并为每个接口呈现其显示名称(描述网络设备的人类可读的String)和名称(用于标识网络接口的真实名称)。此外,还会检查每个网络接口,看它是否支持多播,是否是虚拟的(子接口),以及是否已启动并正在运行。

图 8-4 显示了我的机器上的输出片段。帧接口是用于测试多播应用的接口,其名称为 eth3,稍后将在客户端/服务器多播应用中用于指示该接口。

Image

***图 8-4。*找出本地接口。

编写 UDP 组播服务器

在本节中,我们将编写一个 UDP 多播服务器,它向组发送包含服务器上当前日期和时间的数据报。这将每 10 秒钟重复一次。既然我们已经有了一些编写 UDP 客户端/服务器应用的经验,就没有必要一步一步地重复整个过程。我们将只指出将普通的 UDP 客户机/服务器应用转换成 UDP 多播客户机/服务器应用的主要区别。

我们通过调用open()方法创建一个新的DatagramChannel对象来开始开发过程。接下来,我们设置两个重要的选项,IP_MULTICAST_IFSO_REUSEADDR。第一个将指示本例中使用的 IP 多播数据报的网络接口,第二个应在绑定套接字之前启用,这是允许组的多个成员绑定到同一地址所必需的:

NetworkInterface networkInterface = NetworkInterface.getByName("eth3");
...
datagramChannel.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface);
datagramChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);

接下来,我们通过调用bind()方法将通道的套接字绑定到本地地址:

final int DEFAULT_PORT = 5555;
datagramChannel.bind(new InetSocketAddress(DEFAULT_PORT));

最后,我们准备数据报传输代码。因为我们每 10 秒钟向组发送一次服务器日期和时间,所以我们需要一个无限循环,其中包含 10 秒钟的睡眠时间和对send()方法的调用。多播组 IP 地址被任意选择为 225.4.5.6,它由一个InetAddress对象映射:

final int DEFAULT_PORT = 5555;
final String GROUP = "225.4.5.6";
ByteBuffer datetime;
...
while (true) {

       //sleep for 10 seconds
       try {
           Thread.sleep(10000);
       } catch (InterruptedException ex) {}

       System.out.println("Sending data ...");
       datetime = ByteBuffer.wrap(new Date().toString().getBytes());
       datagramChannel.send(datetime, new  
                            InetSocketAddress(InetAddress.getByName(GROUP), DEFAULT_PORT));
       datetime.flip();
}

将所有内容放在一起将产生以下应用:

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.StandardProtocolFamily;
import java.nio.channels.DatagramChannel;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.util.Date;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final String GROUP = "225.4.5.6";
  ByteBuffer datetime;

  //create a new channel
  try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {

       //check if the channel was successfully created
       if (datagramChannel.isOpen()) {

           //get the network interface used for multicast
           NetworkInterface networkInterface = NetworkInterface.getByName("eth3");

           //set some options

           datagramChannel.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface);        
           datagramChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);                

           //bind the channel to the local address
           datagramChannel.bind(new InetSocketAddress(DEFAULT_PORT));
           System.out.println("Date-time server is ready ... shortly I'll start sending ...");

           //transmitting datagrams
           while (true) {

                  //sleep for 10 seconds
                  try {
                      Thread.sleep(10000);
                  } catch (InterruptedException ex) {}

                  System.out.println("Sending data ...");
                  datetime = ByteBuffer.wrap(new Date().toString().getBytes());
                  datagramChannel.send(datetime, new
                               InetSocketAddress(InetAddress.getByName(GROUP), DEFAULT_PORT));
                  datetime.flip();
           }
       } else {
         System.out.println("The channel cannot be opened!");
       }
  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}
编写 UDP 组播客户端

UDP 组播客户端的代码与服务器几乎相同,只是有一些不同。首先,您可能想要检查远程地址是否实际上是一个多播地址——这可以通过调用返回一个booleanInetAddress.isMulticastAddress()方法来实现。其次,因为这是一个客户端,它必须通过调用两个join()方法之一来加入这个组。数据报传输代码仅适用于从 UDP 多播服务器接收数据报。以下应用是一种可能的客户端实现:

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.StandardProtocolFamily;
import java.nio.channels.DatagramChannel;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.MembershipKey;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final int MAX_PACKET_SIZE = 65507;
  final String GROUP = "225.4.5.6";

  CharBuffer charBuffer = null;
  Charset charset = Charset.defaultCharset();
  CharsetDecoder decoder = charset.newDecoder();
  ByteBuffer datetime = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);

  //create a new channel
  try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {

       InetAddress group = InetAddress.getByName(GROUP);
       //check if the group address is multicast
       if (group.isMulticastAddress()) {
           //check if the channel was successfully created
           if (datagramChannel.isOpen()) {
               //get the network interface used for multicast
               NetworkInterface networkInterface = NetworkInterface.getByName("eth3");

               //set some options
               datagramChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
               //bind the channel to the local address
               datagramChannel.bind(new InetSocketAddress(DEFAULT_PORT));
               //join the multicast group and get ready to receive datagrams
               MembershipKey key = datagramChannel.join(group, networkInterface);

               //wait for datagrams
               while (true) {

                      if (key.isValid()) {

                          datagramChannel.receive(datetime);
                          datetime.flip();
                          charBuffer = decoder.decode(datetime);
                          System.out.println(charBuffer.toString());
                          datetime.clear();
                      } else {
                        break;
                      }
               }

           } else {
             System.out.println("The channel cannot be opened!");
           }
       } else {
         System.out.println("This is not  multicast address!");

       }

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}
阻塞和解除阻塞数据报

有时加入多播组会给你带来不想要的数据报(原因与此无关)。您可以通过调用MembershipKey.block()方法并向其传递发送方的InetAddress来阻止从发送方接收数据报。此外,通过调用MembershipKey.unblock()方法并向其传递同一个InetAddress,您可以解除对同一个发送方的阻止,并再次开始从其接收数据报。通常,您会处于以下两种情况之一:

  • 您有一个想要加入的发件人地址列表。假设地址存储在一个List中,你可以循环它并分别连接每个地址,如下所示:`List like = ...; DatagramChannel datagramChannel =...;

    if(!like.isEmpty()){     for(InetAddress source: like){         datagramChannel.join(group, network_interface, source);     } }`

  • 您有一个不想加入的发件人地址列表。假设地址存储在一个List中,那么你可以循环它,分别阻塞每个地址,如下所示:`List dislike = ...; DatagramChannel datagramChannel =...;

    MembershipKey key = datagramChannel.join(group, network_interface);

    if(!dislike.isEmpty()){    for(InetAddress source: dislike){        key.block(source);    } }`

测试 UDP 组播应用

测试应用是一项简单的任务。首先,启动多播服务器,等待看到以下消息:


Date-time server is ready ... shortly I'll start sending ..

然后启动客户机并检查输出。以下是 UDP 多播服务器的一些输出示例:


Date-time server is ready ... shortly I'll start sending ...

Sending data ...

Sending data ...

Sending data ...

Sending data ...

Sending data ...

以下是 UDP 客户端输出(客户端在几分钟后启动):


Sat Oct 08 09:40:09 GMT+02:00 2011

Sat Oct 08 09:40:19 GMT+02:00 2011

对这个例子进行一些测试将会揭示一些问题。当服务器启动时,它发送数据报,而不知道是否有任何客户端正在侦听这些数据报。此外,它不知道客户端何时加入或离开组。另一方面,客户端在加入组时开始接收数据报,但不知道服务器是否因为任何原因而停止发送。如果服务器脱机,客户端仍在等待,当服务器再次联机并开始发送时,它将再次接收。如果您的情况需要更多的控制,尝试解决这些问题可能是一个有趣的练习。此外,您可能希望试验线程、阻塞/非阻塞模式和无连接/连接特性,以便为您的多播应用增加更多的灵活性和性能。

总结

本章讲述了用于创建 TCP/UDP 客户端/服务器应用的 NIO.2 特性。如前所述,NIO.2 通过用新方法更新现有的类,并为编写这样的应用添加新的接口/类,改进了这种支持。

这一章从NetworkChannel接口开始,它为所有网络通道类提供了公共方法。它还涵盖了专用于同步套接字通道的主要类:ServerSocketChannelSocketChannelDatagramChannel。它还讨论了MulticastChannel接口——映射支持 IP 多播的网络通道的NetworkChannel子接口。最后,您看到了如何编写单线程阻塞/非阻塞 TCP 客户机/服务器应用、单线程阻塞 UDP 客户机/服务器应用和单线程多播 UDP 客户机/服务器应用。*