Java NIO系列三 之 Channel(通道)

本文详细介绍Java NIO中的各种通道,包括FileChannel、SocketChannel、ServerSocketChannel和DatagramChannel的使用方法,并介绍了Scatter/Gather操作及通道间的数据传输。

一 Channel(通道)介绍

通常来说NIO中的所有IO都是从 Channel(通道) 开始的。

  • 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。

  • 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。

数据读取和写入操作图示:

Java NIO Channel通道和流非常相似,主要有以下几点区别:

  • 通道可以读也可以写,流一般来说是单向的(只能读或者写,所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流)。

  • 通道可以异步读写。

  • 通道总是基于缓冲区Buffer来读写。

Java NIO中最重要的几个Channel的实现:

  • FileChannel: 用于文件的数据读写

  • DatagramChannel: 用于UDP的数据读写

  • SocketChannel: 用于TCP的数据读写,一般是客户端实现

  • ServerSocketChannel: 允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel,一般是服务器实现

类层次结构:

下面的UML图使用Idea生成的。

二 FileChannel的使用

使用FileChannel读取数据到Buffer(缓冲区)以及利用Buffer(缓冲区)写入数据到FileChannel:


 
  1. package filechannel;

  2.  

  3. import java.io.IOException;

  4. import java.io.RandomAccessFile;

  5. import java.nio.ByteBuffer;

  6. import java.nio.channels.FileChannel;

  7.  

  8. public class FileChannelTxt {

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

  10.        //1.创建一个RandomAccessFile(随机访问文件)对象,

  11.        RandomAccessFile raf=new RandomAccessFile("D:\\niodata.txt", "rw");

  12.        //通过RandomAccessFile对象的getChannel()方法。FileChannel是抽象类。

  13.        FileChannel inChannel=raf.getChannel();

  14.        //2.创建一个读数据缓冲区对象

  15.        ByteBuffer buf=ByteBuffer.allocate(48);

  16.        //3.从通道中读取数据

  17.        int bytesRead = inChannel.read(buf);

  18.        //创建一个写数据缓冲区对象

  19.        ByteBuffer buf2=ByteBuffer.allocate(48);

  20.        //写入数据

  21.        buf2.put("filechannel test".getBytes());

  22.        buf2.flip();

  23.        inChannel.write(buf);

  24.        while (bytesRead != -1) {

  25.  

  26.            System.out.println("Read " + bytesRead);

  27.            //Buffer有两种模式,写模式和读模式。在写模式下调用flip()之后,Buffer从写模式变成读模式。

  28.            buf.flip();

  29.           //如果还有未读内容

  30.            while (buf.hasRemaining()) {

  31.                System.out.print((char) buf.get());

  32.            }

  33.            //清空缓存区

  34.            buf.clear();

  35.            bytesRead = inChannel.read(buf);

  36.        }

  37.        //关闭RandomAccessFile(随机访问文件)对象

  38.        raf.close();

  39.    }

  40. }

运行效果:

通过上述实例代码,我们可以大概总结出FileChannel的一般使用规则:

1. 开启FileChannel

使用之前,FileChannel必须被打开 ,但是你无法直接打开FileChannel(FileChannel是抽象类)。需要通过 InputStreamOutputStreamRandomAccessFile 获取FileChannel。

我们上面的例子是通过RandomAccessFile打开FileChannel的:


 
  1.        //1.创建一个RandomAccessFile(随机访问文件)对象,

  2.        RandomAccessFile raf=new RandomAccessFile("D:\\niodata.txt", "rw");

  3.        //通过RandomAccessFile对象的getChannel()方法。FileChannel是抽象类。

  4.        FileChannel inChannel=raf.getChannel();

2. 从FileChannel读取数据/写入数据

从FileChannel中读取数据/写入数据之前首先要创建一个Buffer(缓冲区)对象,Buffer(缓冲区)对象的使用我们在上一篇文章中已经详细说明了,如果不了解的话可以看我的上一篇关于Buffer的文章。

使用FileChannel的read()方法读取数据:


 
  1.        //2.创建一个读数据缓冲区对象

  2.        ByteBuffer buf=ByteBuffer.allocate(48);

  3.        //3.从通道中读取数据

  4.        int bytesRead = inChannel.read(buf);

使用FileChannel的write()方法写入数据:


 
  1.        //创建一个写数据缓冲区对象

  2.        ByteBuffer buf2=ByteBuffer.allocate(48);

  3.        //写入数据

  4.        buf2.put("filechannel test".getBytes());

  5.        buf2.flip();

  6.        inChannel.write(buf);

3. 关闭FileChannel

完成使用后,FileChannel您必须关闭它。


 
  1. channel.close();    

三 SocketChannel和ServerSocketChannel的使用

利用SocketChannel和ServerSocketChannel实现客户端与服务器端简单通信:

SocketChannel 用于创建基于tcp协议的客户端对象,因为SocketChannel中不存在accept()方法,所以,它不能成为一个服务端程序。通过 connect()方法 ,SocketChannel对象可以连接到其他tcp服务器程序。

客户端:


 
  1. package socketchannel;

  2.  

  3. import java.io.IOException;

  4. import java.net.InetSocketAddress;

  5. import java.nio.ByteBuffer;

  6. import java.nio.channels.SocketChannel;

  7.  

  8. public class WebClient {

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

  10.        //1.通过SocketChannel的open()方法创建一个SocketChannel对象

  11.        SocketChannel socketChannel = SocketChannel.open();

  12.        //2.连接到远程服务器(连接此通道的socket)

  13.        socketChannel.connect(new InetSocketAddress("127.0.0.1", 3333));

  14.        // 3.创建写数据缓存区对象

  15.        ByteBuffer writeBuffer = ByteBuffer.allocate(128);

  16.        writeBuffer.put("hello WebServer this is from WebClient".getBytes());

  17.        writeBuffer.flip();

  18.        socketChannel.write(writeBuffer);

  19.        //创建读数据缓存区对象

  20.        ByteBuffer readBuffer = ByteBuffer.allocate(128);

  21.        socketChannel.read(readBuffer);

  22.        //String 字符串常量,不可变;StringBuffer 字符串变量(线程安全),可变;StringBuilder 字符串变量(非线程安全),可变

  23.        StringBuilder stringBuffer=new StringBuilder();

  24.        //4.将Buffer从写模式变为可读模式

  25.        readBuffer.flip();

  26.        while (readBuffer.hasRemaining()) {

  27.            stringBuffer.append((char) readBuffer.get());

  28.        }

  29.        System.out.println("从服务端接收到的数据:"+stringBuffer);

  30.  

  31.        socketChannel.close();

  32.    }

  33.  

  34. }

ServerSocketChannel 允许我们监听TCP链接请求,通过ServerSocketChannelImpl的 accept()方法 可以创建一个SocketChannel对象用户从客户端读/写数据。

服务端:


 
  1. package socketchannel;

  2.  

  3. import java.io.IOException;

  4. import java.net.InetSocketAddress;

  5. import java.nio.ByteBuffer;

  6. import java.nio.channels.ServerSocketChannel;

  7. import java.nio.channels.SocketChannel;

  8.  

  9. public class WebServer {

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

  11.        try {

  12.            //1.通过ServerSocketChannel 的open()方法创建一个ServerSocketChannel对象,open方法的作用:打开套接字通道

  13.            ServerSocketChannel ssc = ServerSocketChannel.open();

  14.            //2.通过ServerSocketChannel绑定ip地址和port(端口号)

  15.            ssc.socket().bind(new InetSocketAddress("127.0.0.1", 3333));

  16.            //通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据

  17.            SocketChannel socketChannel = ssc.accept();

  18.            //3.创建写数据的缓存区对象

  19.            ByteBuffer writeBuffer = ByteBuffer.allocate(128);

  20.            writeBuffer.put("hello WebClient this is from WebServer".getBytes());

  21.            writeBuffer.flip();

  22.            socketChannel.write(writeBuffer);

  23.            //创建读数据的缓存区对象

  24.            ByteBuffer readBuffer = ByteBuffer.allocate(128);

  25.            //读取缓存区数据

  26.            socketChannel.read(readBuffer);

  27.            StringBuilder stringBuffer=new StringBuilder();

  28.            //4.将Buffer从写模式变为可读模式

  29.            readBuffer.flip();

  30.            while (readBuffer.hasRemaining()) {

  31.                stringBuffer.append((char) readBuffer.get());

  32.            }

  33.            System.out.println("从客户端接收到的数据:"+stringBuffer);

  34.            socketChannel.close();

  35.            ssc.close();

  36.        } catch (IOException e) {

  37.            e.printStackTrace();

  38.        }

  39.    }

  40. }

运行效果:

客户端:

服务端:

通过上述实例代码,我们可以大概总结出SocketChannel和ServerSocketChannel的使用的一般使用规则:

考虑到篇幅问题,下面只给出大致步骤,不贴代码,可以结合上述实例理解。

客户端

1.通过SocketChannel连接到远程服务器

2.创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据

3.关闭SocketChannel

服务端

1.通过ServerSocketChannel 绑定ip地址和端口号

2.通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据

3.创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据

4. 关闭SocketChannel和ServerSocketChannel

四 ️DatagramChannel的使用

DataGramChannel,类似于java 网络编程的DatagramSocket类;使用UDP进行网络传输, UDP是无连接,面向数据报文段的协议,对传输的数据不保证安全与完整 ;和上面介绍的SocketChannel和ServerSocketChannel的使用方法类似,所以这里就简单介绍一下如何使用。

1.获取DataGramChannel


 
  1.        //1.通过DatagramChannel的open()方法创建一个DatagramChannel对象

  2.        DatagramChannel datagramChannel = DatagramChannel.open();

  3.        //绑定一个port(端口)

  4.        datagramChannel.bind(new InetSocketAddress(1234));

上面代码表示程序可以在1234端口接收数据报。

2.接收/发送消息

接收消息:

先创建一个缓存区对象,然后通过receive方法接收消息,这个方法返回一个SocketAddress对象,表示发送消息方的地址:


 
  1. ByteBuffer buf = ByteBuffer.allocate(48);

  2. buf.clear();

  3. channel.receive(buf);

发送消息:

由于UDP下,服务端和客户端通信并不需要建立连接,只需要知道对方地址即可发出消息,但是是否发送成功或者成功被接收到是没有保证的;发送消息通过send方法发出,改方法返回一个int值,表示成功发送的字节数:


 
  1. ByteBuffer buf = ByteBuffer.allocate(48);

  2. buf.clear();

  3. buf.put("datagramchannel".getBytes());

  4. buf.flip();

  5. int send = channel.send(buffer, new InetSocketAddress("localhost",1234));

这个例子发送一串字符:“datagramchannel”到主机名为”localhost”服务器的端口1234上。

五 Scatter / Gather

Channel 提供了一种被称为 Scatter/Gather 的新功能,也称为本地矢量 I/O。Scatter/Gather 是指在多个缓冲区上实现一个简单的 I/O 操作。正确使用 Scatter / Gather可以明显提高性能。

大多数现代操作系统都支持本地矢量I/O(native vectored I/O)操作。当您在一个通道上请求一个Scatter/Gather操作时,该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区,减少或避免了缓冲区拷贝和系统调用;

Scatter/Gather应该使用直接的ByteBuffers以从本地I/O获取最大性能优势。

Scatter/Gather功能是通道(Channel)提供的 并不是Buffer。

  • Scatter: 从一个Channel读取的信息分散到N个缓冲区中(Buufer).

  • Gather: 将N个Buffer里面内容按照顺序发送到一个Channel.

Scattering Reads

"scattering read"是把数据从单个Channel写入到多个buffer,如下图所示:

示例代码:


 
  1. ByteBuffer header = ByteBuffer.allocate(128);

  2. ByteBuffer body   = ByteBuffer.allocate(1024);

  3.  

  4. ByteBuffer[] bufferArray = { header, body };

  5.  

  6. channel.read(bufferArray);

read()方法内部会负责把数据按顺序写进传入的buffer数组内。一个buffer写满后,接着写到下一个buffer中。

举个例子,假如通道中有200个字节数据,那么header会被写入128个字节数据,body会被写入72个字节数据;

注意:

无论是scatter还是gather操作,都是按照buffer在数组中的顺序来依次读取或写入的;

Gathering Writes

"gathering write"把多个buffer的数据写入到同一个channel中,下面是示意图:

示例代码:


 
  1. ByteBuffer header = ByteBuffer.allocate(128);

  2. ByteBuffer body   = ByteBuffer.allocate(1024);

  3.  

  4. //write data into buffers

  5.  

  6. ByteBuffer[] bufferArray = { header, body };

  7.  

  8. channel.write(bufferArray);

write()方法内部会负责把数据按顺序写入到channel中。

注意:

并不是所有数据都写入到通道,写入的数据要根据position和limit的值来判断,只有position和limit之间的数据才会被写入;

举个例子,假如以上header缓冲区中有128个字节数据,但此时position=0,limit=58;那么只有下标索引为0-57的数据才会被写入到通道中。

六 通道之间的数据传输

在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel。

  • transferFrom() :transferFrom方法把数据从通道源传输到FileChannel

  • transferTo() :transferTo方法把FileChannel数据传输到另一个channel

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值