NIO简介
nio的包主要有四个,分别是
1. 缓冲区包:java.nio 出现版本:Java SE1.4
2. 通道包:java.nio.channels 出现版本:Java SE1.4
3. 字符集包:java.nio.charset 出现版本:Java SE1.4
4. 文件处理包:java nio.file 出现版本:Java SE1.7
a)通道数据的进出,都要经过缓冲区
b)通道是一种新的原生I/O抽象概念,好比连接两端数据的管道,用于数据的交互
c)字符集包:大多数字符集的集合,处理字节字符之间相互转换
d)文件包:处理目录和文件。包含io中File类功能,比之更加强大,更具有名字等价的意义
1.缓冲Buffer
所有的基本数据类型都有相应的缓冲器(布尔型除外),但字节是操作系统及其 I/O设备使用的基本数据类型,所以唯一与通道交互的缓冲器是ByteBuffer。后面再讲字符集转换时会用到CharBuffer
Buffer基础
有几个标志位,用于操作缓冲区
容量(capacity):缓冲区大小(byte),读出、写入值都不会变
位置(position):下一个字节被读出写入的位置,注意:position永远小于limit
界限(limit) :写入时(界限=容量),读出时(上次写入时position的位置)
标记(mark) :初始化为-1,调用reset()可回到标记位置
public static void main(String[] args) { // 初始方法化一:直接用包装的数组作为缓冲区,不再分配空间 ByteBuffer temp = ByteBuffer.wrap("字节数组".getBytes()); // 初始方法化二,初始化一个容量大小为10的 缓冲区。注意: 初始化时,为写入状态 ByteBuffer bb = ByteBuffer.allocate(10); bb.capacity();// 容量:10 bb.position();// 位置:0 bb.limit();// 界限:10 // 写入字节 bb.put("345".getBytes()); // position = 3 // 倒回 执行position = 0 ; mark = -1 bb.rewind(); // 写入字节 bb.put("012".getBytes()); // position = 3 // 做个标记(mark) bb.mark(); // mark = position = 3; // 继续写入 bb.put("678".getBytes()); // position = 6 // 重置:回到记号处 bb.reset(); // position = mark = 3 // 继续写入 bb.put("345".getBytes()); // position = 6 // 切换到写入状态,调用下面方法 bb.flip(); // 执行:limit = position = 6 ; position = 0 ; mark = -1; // position=0 可以从头开始读出数据。因为bb中只有6个字节,所以被设置读出的界限为6 //剩下可读取的字节数 bb.remaining(); while (bb.hasRemaining()) {// 是否还有没有读取的字节 // 读出1个字节 bb.get(); } // position = 6 // 继续从头读 bb.rewind(); // position = 0 // 设置position = 3 bb.position(3); System.out.println((char) bb.get()); // 输出3 // 包含索引的get(),不会改变position的值 System.out.println((char) bb.get(1)); // 输出1 // position = 4 // 创建一个只读的缓冲区,两个缓冲区共享数据元素 ByteBuffer readOnly = bb.asReadOnlyBuffer(); // 复制一个缓冲区,两个缓冲共享数据元素,有各自的位置、标记等。 // 如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性 ByteBuffer duplicate = bb.duplicate(); // 创建一个从原始缓冲区的当前位置开始的新缓冲区 // 其容量是原始缓冲区的剩余元素数量(limit-position) ByteBuffer slice = bb.slice(); // 判断缓冲区是否是只读 readOnly.isReadOnly(); // true // 切换到写入状态 bb.clear(); // position = 0 ; limit=10 }
- 当一个管理其他缓冲器所包含的数据元素的缓冲器被创建时,这个缓冲器被称为视图缓冲器
- 我们可以通过as的工厂方法来创建ByteBuffer的视图缓冲器,视图缓冲器的任何修改操作都会映射成对ByteBuffer中数据的修改。
public static void main(String[] args) { ByteBuffer bb = ByteBuffer.allocate(20); // 以下都是ByteBuffer的视图缓冲器 CharBuffer cb = bb.asCharBuffer(); DoubleBuffer db = bb.asDoubleBuffer(); FloatBuffer fb = bb.asFloatBuffer(); IntBuffer ib = bb.asIntBuffer(); LongBuffer lb = bb.asLongBuffer(); ShortBuffer sb = bb.asShortBuffer(); // ByteBuffer 大小20个字节,刚好容下5个int类型 int[] is = new int[] { 1, 2, 3, 4, 5 }; bb.asIntBuffer().put(is); // 不同的机器可能使用不同的字节排序方法来存储数据,ByteBuffer以高位优先的形式存储数据 // “big endian” 高位优先 // “little endian”低位优先 System.out.println(Arrays.toString(bb.array())); // 输出[0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5] //查看缓冲区的当前字节顺序设定 bb.order(); //设置bb以地位优先存储数据 bb.order(ByteOrder.LITTLE_ENDIAN); //不调用clear()是因为asIntBuffer()会生成一个新的int缓冲,与bb共享数据元素 bb.asIntBuffer().put(is); System.out.println(Arrays.toString(bb.array())); //输出:[1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0] }
间接缓冲区:
- 通过allocate()或者wrap()函数创建的缓冲区通常都是间接的。间接的缓冲区跟上一节介绍的IO缓冲区性质差不多(磁盘数据—>内核空间—>用户空间),但是NIO的缓冲区设计更加灵活,每个缓冲区都可以通过各自的工厂方法创建
- 间接的缓冲区使用备份数组,可以通过函数获得对这些数组的存取权。boolean型函数hasArray()告诉您这个缓冲区是否有一个可存取的备份数组。如果这个函数的返回true,array()函数会返回这个缓冲区对象所使用的数组存储空间的引用
ByteBuffer bb = ByteBuffer.allocate(1024); CharBuffer cb = CharBuffer.allocate(1024); // 对缓冲区的修改会影响到wrap的数组 ByteBuffer bbw = ByteBuffer.wrap("wrap".getBytes()); CharBuffer cbw = CharBuffer.wrap("aaa"); //还有其他非boolean的基本类型可以这样创建
直接缓冲区:
- 本地内存:一个4G的内存条,如果JVM分配2G,另外2G的一部分就可以作为本地内存。
- 直接缓冲区:JVM可以通过JNI(Native)方法,分配指定大小的本地内存作为自己的缓冲区(直接缓冲区)。
- 通过JVM堆中的DirectByteBuffer对象来对这部分内存进行包装、操作。
- 通道可以用直接缓冲区的本地内存空间来操作系统级I/O,将数据直接读入直接缓冲区中。
- 直接缓冲区属于本地内存,不归JVM管理,所以GC无法回收。但是其包装类(DirectByteBuffer)存在于JVM堆中,当包装类被GC回收时,JVM会调用一个JNI法来释放直接缓冲区的本地内存(详细过程)。所以本地内存的释放依赖于包装类的回收,而GC的运行一般在堆内存即将不够用时才启动,这点要注意。
- 直接缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区,建议将直接缓冲区主要分配给那些持久的 I/O 操作。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。例如:网络数据传输, 可考虑合理应用DirectBuffer
- 因为直缓冲区是通过Native方法创建的,所以可以通过一个JVM参数MaxDirectMemorySize设置能创建的最大值。
// 创建一个直接缓冲区,只有字节缓冲区有这个工厂方法 ByteBuffer bb = ByteBuffer.allocateDirect(1024); // 构建一个通道 FileChannel fc = new FileInputStream(new File("文件路径")).getChannel(); // 将数据从磁盘读到直接缓冲区 fc.read(bb);
内存映射文件:
- 现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java 内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。
- 内存映射文件不需要把内容拷贝到内存,它可以直接把文件当作内存来处理。
- 大型文件使用映射,无需耗费大量内存,即可进行数据拷贝
- 在FileChannel上调用map()方法会创建一个由磁盘文件支持的虚拟内存映射
//通过通道创建映射缓冲区 public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { /** * 创建一个映射的缓冲区方法 * @param mode 缓冲区的模式(只读、读写、私有的) * @param position 映射文件的起始位置 * @param size 映射缓冲区大小 * @return */ public abstract MappedByteBuffer map(MapMode mode, long position, long size) //创建映射缓冲区的第一个参数 public static class MapMode { public static final MapMode READ_ONLY public static final MapMode READ_WRITE public static final MapMode PRIVATE } }
// 构建一个通道 FileChannel fc = new FileInputStream(new File("文件路径")).getChannel(); //在虚拟内存空间外部封装一个MappedByteBuffer对象 MappedByteBuffer bb = fc.map(MapMode.READ_ONLY, 0, 1024); //从内存区域获取数据实际上就是从相应文件的偏移位置处返回数据 bb.get(); //修改则会将文件从内存空间写入磁盘 bb.put((byte)1);
- 第三种模式MapMode.PRIVATE表示创建一个写时拷贝(copy-on-write)的映射。任何修改都会导致产生一个私有的数据拷贝并且该拷贝中的数据只有MappedByteBuffer实例可以看到。该过程不会对底层文件做任何修改,而且一旦缓冲区被施以垃圾收集动作(garbage collected),那些修改都会丢失。
- 所有的MappedByteBuffer对象都是直接的,它们占用的内存空间位于Java虚拟机内存堆之外(并且可能不会算作 Java 虚拟机的内存占用,不过这取决于操作系统的虚拟内存模型)
- load()方法会加载整个文件以使它常驻内存,此方法实现最小的缓冲区访问延迟,但是代价是高昂的,轻易不要使用。通过isLoaded() 可以判断映射区域是否已经常驻内存了。
- force()方法,强制将缓冲区内容的更改同步到磁盘上。
- 所有的缓冲区都提供了一个叫做isDirect()的boolean函数,来测试特定缓冲区是否为直接缓冲区
测试一下3中缓冲内存使用情况
- 非直接缓冲区,读取500M内容
public static void main(String[] args) throws IOException, InterruptedException { // 创建通道 FileChannel fc = new FileInputStream(new File("E:\\电影\\temp.rmvb")).getChannel(); // 在JVM堆中创建一个500M非直接缓冲区 ByteBuffer bb = ByteBuffer.allocate(1024 * 1024 * 500); // 将文件内容写入缓冲区 fc.read(bb); // 切换到读状态 bb.flip(); while (bb.hasRemaining()) { bb.get(); } // 1000M内存被占用 Thread.sleep(1000000); }
- 直接缓冲区,读取500M内容
public static void main(String[] args) throws IOException, InterruptedException { // 创建通道 FileChannel fc = new FileInputStream(new File("E:\\电影\\temp.rmvb")).getChannel(); // 在本地内存中创建一个500M直接缓冲区 ByteBuffer bb = ByteBuffer.allocateDirect(1024 * 1024 * 500); // 将文件内容写入缓冲区 fc.read(bb); // 切换到读状态 bb.flip(); while (bb.hasRemaining()) { bb.get(); } // 500M内存被占用 Thread.sleep(1000000); }
内存映射500M区域
public static void main(String[] args) throws IOException, InterruptedException { // 创建通道 FileChannel fc = new FileInputStream(new File("E:\\电影\\temp.rmvb")).getChannel(); // 映射500M文件区域 MappedByteBuffer bb = fc.map(MapMode.READ_ONLY, 0, 1024 * 1024 * 500); while (bb.hasRemaining()) { bb.get(); } // 没有内存被占用 Thread.sleep(1000000); }
通道channel
FileChannel
- FileChannel是一个连接到文件的通道,可以对文件进行读写
- FileChannel是阻塞的,不能运行在非阻塞模式下
- FileChannel对象是线程安全的,多个线程可以在同一个实例上并发调用方法而不会引起任何问题
- FileChannel的初始化
public static void main(String[] args) throws IOException { File file = new File("D:\\TEXT.txt"); // 只能用于读的通道 FileChannel readCh = new FileInputStream(file).getChannel(); // 只能用于写的通道 FileChannel writCh = new FileOutputStream(file).getChannel(); // 可以读也可以写的通道 FileChannel rwCh = new RandomAccessFile(file, "rw").getChannel(); // 在Java SE1.7中提供了新的初始化方法 // 这个Path后面会介绍 Path path = Paths.get("D:", "TEXT.txt"); // 只能用于读的通道 FileChannel nReadCh = FileChannel.open(path, StandardOpenOption.READ); // 只能用于写的通道 FileChannel nWriteCh = FileChannel.open(path, StandardOpenOption.WRITE); // 可以读也可以写的通道,第二个参数可以是个数组 FileChannel nReadWrite = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.READ); }
5. 基本方法
public static void main(String[] args) throws IOException { File file = new File("D:\\t2.txt"); // 如果文件不存,就会直接创建一个空文件 FileChannel rw = new RandomAccessFile(file, "rw").getChannel(); // 初始化一个缓冲区 ByteBuffer bb = ByteBuffer.wrap("temp".getBytes()); // 注意 通道也有一个读写的位置,而且是从底层的文件描述符获得的 // 这也就意味着一个对象对该position的更新可以被另一个对象看到 rw.position(); // 向通道道写入数据 while (bb.hasRemaining()) { // position放在末尾,write会自动对文件进行扩容 rw.write(bb); } // 先清空缓冲区 bb.clear(); // 通道的读写文件位置在末尾,设置到0位置 rw.position(0); // 读数据到缓冲区 rw.read(bb); // 通道关联文件的大小(字节) rw.size(); // 截断文件,只保留前三个字节,其他删掉 rw.truncate(3); // 所有的现代文件系统都会缓存数据和延迟磁盘文件更新以提高性能。调用force()方法要求文件的所有待定修改立即同步到磁盘 // boolean参数设置 元数据 是否要写到磁盘 // 元数据:指文件所有者、访问权限、最后一次修改时间等信息 // 同步元数据要求操作系统至少一次的I/O操作,为了提高性能,可以不同步元数据,同时也不会牺牲数据完整性 rw.force(false); // 关闭通道 if (rw.isOpen()) rw.close(); }
6. Channel-to-Channel传输
public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // 这里仅列出部分API public abstract long transferTo (long position, long count, WritableByteChannel target) public abstract long transferFrom (ReadableByteChannel src, long position, long count) }
transferTo()和transferFrom()方法允许将一个通道交叉连接到另一个通道,而不需要通过一个中间缓冲区来传递数据。只有FileChannel类有这两个方法,因此Channel-to-Channel传输中通道之一必须是FileChannel。不能在socket通道之间直接传输数据,不过socket通道实现WritableByteChannel和ReadableByteChannel接口,因此文件的内容可以用transferTo()方法传输给一个socket通道,或者也可以用transferFrom()方法将数据从一个socket通道直接读取到一个文件中。
public static void main(String[] args) throws IOException { // 将两个通道相连传输数据 File inFile = new File("D:" + File.separator + "temp.png"); File outFile = new File("E:" + File.separator + "temp.png"); FileChannel in = new FileInputStream(inFile).getChannel(); FileChannel out = new FileOutputStream(outFile).getChannel(); // 将inFile文件数据拷贝到outFile out.transferFrom(in, 0, in.size()); in.transferTo(0, in.size(), out); }
7. 通道可以向缓冲区数组写入数据,并按顺序填充每个缓冲区直到所有缓冲区满或者没有数据可读为止。聚集写也是以类似的方式完成,数据从列表中的每个缓冲区中顺序取出来发送到通道就好像顺序写入一样
文件锁定
- 文件锁定是依赖于操作系统的,而且锁的对象是文件内部区域而不是通道或线程
- 文件锁是进程级的,不是线程级的,一个java线程获得了文件区域a的锁,那这个锁是归JVM进程所持有的,表示这个JVM进程的所有线程对区域a有同等权限。如果这个JVM的另一个线程再次申请区域a或者区域b(包含区域a)的锁,会抛出一个异常OverlappingFileLockException重叠的文件锁。
- 独占锁:锁定的区域用于读写操作。该区域属于进程独享。
- 共享锁:允许多个进程从该区域读取内容,阻止任何进程修改该区域并阻止任何进程获得包含该区域的独占的锁,
- 现在让我们来看下与文件锁定有关的FileChannel API方法
// 如果请求的锁定范围是有效的,阻塞直至获取锁 public final FileLock lock() // 尝试获取锁非阻塞,立刻返回结果 public final FileLock tryLock() // 第一个参数:要锁定区域的起始位置 // 第二个参数:要锁定区域的尺寸, // 第三个参数:true为共享锁,false为独占锁 public abstract FileLock lock (long position, long size, boolean shared) public abstract FileLock tryLock (long position, long size, boolean shared)
- 锁定区域的范围不一定要限制在文件的size值以内,锁可以扩展从而超出文件尾。因此,我们可以提前把待写入数据的区域锁定,我们也可以锁定一个不包含任何文件内容的区域,比如文件最后一个字节以外的区域。如果之后文件增长到达那块区域,那么您的文件锁就可以保护该区域的文件内容了。相反地,如果您锁定了文件的某一块区域,然后文件增长超出了那块区域,那么新增加 的文件内容将不会受到您的文件锁的保护。
- 不带参数的简单形式的lock()方法是一种在整个文件上请求独占锁的便捷方法,锁定区域等于它能达到的最大范围。该方法等价于
fileChannel.lock(0L,Long.MAX_VALUE,false)
- 并非所有的操作系统都支持共享锁,可能在请求共享锁时获得独占锁,可以调用FileLock类的isShared方法查询获得的锁类型。
public static void main(String[] args) throws IOException { File file = new File("D:\\t2.txt"); // 如果文件不存,就会直接创建一个空文件 FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel(); // 写入4个字节 fileChannel.write(ByteBuffer.wrap("abcd".getBytes())); // 将前2个字节区域锁定(共享锁) FileLock lock1 = fileChannel.lock(0, 2, true); // 当前锁持有锁的类型(共享锁/独占锁) lock1.isShared(); // IOException 不能修改只读的共享区域 // fileChannel.write(ByteBuffer.wrap("a".getBytes())); // 可以修改共享锁之外的区域,从第三个字节开始写入 fileChannel.write(ByteBuffer.wrap("ef".getBytes()), 2); // OverlappingFileLockException 重叠的文件锁异常 // FileLock lock2 = fileChannel.lock(0, 3, true); // FileLock lock3 = fileChannel.lock(0, 3, false); //得到创建锁的通道 lock1.channel(); //锁的起始位置 lock1.position(); //锁的范围 lock1.size(); //判断锁是否与指定文件区域有重叠 lock1.overlaps(position, size) // 记得用try/catch/finally{release()}方法释放锁 lock1.release(); }
socket通道
- socket通道可以在非阻塞模式下运行,一个线程可以通过Selector选择器管理多个socket连接,并不需要为每个socket连接分配一个线程,也避免了大量线程上下文切换的开销。具有很好的可伸缩性和灵活性。
- 全部socket的通道类(SocketChannel、DatagramChannel、ServerSocketChannel)都继承了AbstractSelectableChannel这个抽象类,这个抽象类有一个注册方法register()可以将通道类注册到Selector选择器中,一个通道可以注册到多个选择器中。任何一个通道和选择器的关系都被封装在SelectionKey中。
// 通过静态open()工厂方法初始化ServerSocketChannel ServerSocketChannel ssc = ServerSocketChannel.open(); // 监听端口1100 SE1.7中新增的方法,之前需要ssc.socket().bind(endpoint); ssc.bind(new InetSocketAddress(1100)); // 初始化选择器 Selector s = Selector.open(); // 必须设置Socket通道为非阻塞才能注册到Selector中 if (ssc.isBlocking()) ssc.configureBlocking(false); // 非阻塞模式accept会立即返回,如果没有连接传入,直接返回null // SocketChannel sc = ssc.accept(); // 注册到选择器中,并指定该通道对什么事件感兴趣 SelectionKey sk = ssc.register(s, SelectionKey.OP_ACCEPT); sk.attach(new Object());//如果有需要可以添加一个附件
SocketChannel和DatagramChannel都实现了读写功能的接口,而ServerSocketChannel并没有实现。ServerSocketChannel只负责监听传入的连接和创建SocketChannel对象。
真正的就绪选择必须由操作系统来做。操作系统的一项最重要的功能就是处理I/O请求并通知各个线程它们的数据已经准备好了。选择器类提供了这种抽象,使用Java代码能够以可移植的方式,请求底层的操作系统提供就绪选择服务。
可以只用一个线程监控通道的就绪状态并使用一个协调好的工作线程池来处理共接收到的数据。根据部署的条件,线程池的大小是可以调整的(或者它自己进行动态的调整)。
package i.io.socket; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.util.Iterator; public class SelectSockets { /** * 服务器端 */ public static class ServerSocketListen implements Runnable { @Override public void run() { try { server(1234); } catch (Exception e) { e.printStackTrace(); } } public void server(int... port) throws Exception { // 初始化一个选择器 Selector selector = Selector.open(); // 监听多个端口 for (int pt : port) { System.out.println("Listening on port " + pt); // 初始化一个服务器套接字通道 ServerSocketChannel serverChannel = ServerSocketChannel.open(); // 设置端口服务器通道监听的端口 serverChannel.bind(new InetSocketAddress(pt)); // 设置监听套接字的非阻塞模式 serverChannel.configureBlocking(false); // 注册ServerSocketChannel选择器 serverChannel.register(selector, SelectionKey.OP_ACCEPT); } while (true) { // 这个可能阻塞很长时间,返回后选择集包含准备好的通道键 if (selector.select() == 0) continue; // 处理准备好的通道 handleChannel(selector); } } /** * 注册通道和通道感兴趣的业务到选择器 */ protected void handleChannel(Selector selector) throws Exception { // 得到选择键的迭代器 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); // 有新的连接 if (key.isAcceptable()) { // 得到服务器通道 ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel channel = server.accept(); // 设置通道为非阻塞 channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); } // 有可读取数据的通道 if (key.isReadable()) { readDataFromSocket(key); } it.remove(); } } /** * 读取通道的数据 */ protected void readDataFromSocket(SelectionKey key) throws Exception { SocketChannel socketChannel = (SocketChannel) key.channel(); // 初始化缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); // 将数据读到缓冲区 while (socketChannel.read(buffer) > 0) { // 切换到缓冲区到读模式 buffer.flip(); // 以字符视图打开缓冲 CharBuffer cb = buffer.asCharBuffer(); StringBuilder messClient = new StringBuilder(); while (cb.hasRemaining()) { messClient.append(cb.get()); } System.err.println("2-服务器端接收客户端数据:" + messClient.toString()); buffer.clear(); } // 回写 System.err.println("3-反馈数据到客户端:go"); Charset charset = Charset.forName("gbk"); socketChannel.write(charset.encode("还可以吧")); } } /** * 客户端类 */ public static class ClientSocketListen { public void client() throws IOException { Selector selector = Selector.open(); SocketChannel sc = SocketChannel.open(); sc.configureBlocking(false); sc.register(selector, SelectionKey.OP_CONNECT); sc.connect(new InetSocketAddress(1234)); while (true) { if (selector.select() == 0) continue; handleChannel(selector); } } protected void handleChannel(Selector selector) throws ClosedChannelException, IOException { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); int op = key.readyOps(); SocketChannel channel = (SocketChannel) key.channel(); // 监听这个通道,用于接收服务器端的反馈数据 if ((op & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT) { if (channel.finishConnect()) { //一个选择器只能有一个当前通道的实例, // channel.register(selector, SelectionKey.OP_READ); key.interestOps(SelectionKey.OP_READ); System.out.println("1-客户端发数据到服务器:go"); ByteBuffer bb = ByteBuffer.allocate(8); bb.asCharBuffer().put("4个字符"); channel.write(bb); bb.clear(); } } // 接收服务器端反馈数据 if ((op & SelectionKey.OP_READ) == SelectionKey.OP_READ) { ByteBuffer bb = ByteBuffer.allocate(1024); while (channel.read(bb) > 0) { bb.flip(); Charset charset = Charset.forName("gbk"); System.out.println("4-客户端接收服务器反馈数据:" + charset.decode(bb)); } } it.remove(); } } } public static void main(String[] argv) throws Exception { // 先用一个线程启动服务器端 new Thread(new SelectSockets.ServerSocketListen()).start(); // 客户端调用 new SelectSockets.ClientSocketListen().client(); } }
DatagramChannel是面向UDP的,DatagramChannel对象既可以充当服务器(监听者)也可以充当客户端(发送者), 不同于SocketChannel(必须连接了才有用并且只能连接一次),DatagramChannel对象可以任意次数地进行连接或断开连接。每次连接都可以到一个不同的远程地址。调用disconnect()方法可以配置通道,以便它能再次接收来自安全管理器(如果已安装)所允许的任意远程地址的数据或发送数据到这些地址上。
列出几种使用数据包的情况
程序可以承受数据丢失或无序的数据。
希望「发射后不管」(fire and forget)而不需要知道您发送的包是否已接收。
数据吞吐量比可靠性更重要。
您需要同时发送数据给多个接受者(多播或者广播)。
包隐喻比流隐喻更适合手边的任务。