Java NIO中,一个socket连接使用一个Channel(通道)来表示。然而,从更广泛的层面来说,一个通道封装了一个底层的文件描述符,例如硬件设备、文件、网络连接等。所以,与文件描述符相对应,Java NIO的通道分为很多类型。但是Java的通道更加的细化,例如,对应到不同的网络传输协议类型,在Java中都有不同的NIO Channel(通道)相对应。
Channel(通道)的主要类型有:FileChannel、SocketChannel、ServerSocketChannel、
DatagramChannel。
这四种通道的说明如下:
这个四种通道,涵盖了文件IO、TCP网络、UDP IO三类基础IO读写操作。
FileChannel是专门操作文件的通道。通过FileChannel,既可以从一个文件中读取数据,也可以将数据写入到文件中。特别申明一下,FileChannel为阻塞模式,不能设置为非阻塞模式。
//创建一个文件输入流
String srcFile = "a.txt";
FileInputStream fis = new FileInputStream(srcFile);
//获取文件流的通道
FileChannel inChannel = fis.getChannel();
//创建一个文件输出流
String destFile = "b.txt";
FileOutputStream fos = new FileOutputStream(destFile);
FileChannel outChannel = fos.getChannel();
也可以通过RandomAccessFile文件随机访问类,获取FileChannel文件通道实例,代码如下:
// 创建 RandomAccessFile 随机访问对象
RandomAccessFile accessFile = new RandomAccessFile("a.txt","rw");
//获取文件流的通道(可读可写)
FileChannel channel = accessFile.getChannel();
// 创建 RandomAccessFile 随机访问对象
RandomAccessFile accessFile = new RandomAccessFile("a.txt","rw");
//获取文件流的通道(可读可写)
FileChannel channel = accessFile.getChannel();
//获取字节缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
int length = -1;
//调用通道的 read 方法,读取数据并买入字节类型的缓冲区
while ((length = channel.read(buf)) != -1) {
//……省略 buf 中的数据处理
}
以上代码channel.read(buf)虽然是读取通道的数据,对于通道来说是读取模式,但是对于ByteBuffer缓冲区来说则是写入数据,这时,ByteBuffer缓冲区处于写入模式。
写入数据到通道,在大部分应用场景,都会调用通道的write(ByteBuffer)方法,此方法的参数是一个ByteBuffer缓冲区实例,是待写数据的来源。
write(ByteBuffer)方法的作用,是从ByteBuffer缓冲区中读取数据,然后写入到通道自身,而返回值是写入成功的字节数。
//如果 buf 处于写入模式(如刚写完数据),需要 flip 翻转 buf,使其变成读取模式
buf.flip();
int outlength = 0;
//调用 write 方法,将 buf 的数据写入通道
while ((outlength = outChannel.write(buf)) != 0) {
System.out.println("写入的字节数:" + outlength);
}
在以上的outchannel.write(buf)调用中,对于入参buf实例来说,需要从其中读取数据写入到outchannel通道中,所以入参buf必须处于读取模式,不能处于写入模式。
当通道使用完成后,必须将其关闭。关闭非常简单,调用close( )方法即可。
//关闭通道
channel.close( );
在将缓冲区写入通道时,出于性能原因,操作系统不可能每次都实时将写入数据落地(或刷新)到磁盘,完成最终的数据保存。
如果在将缓冲数据写入通道时,需要保证数据能落地写入到磁盘,可以在写入后调用一下FileChannel的force()方法。
//强制刷新到磁盘
channel.force(true);
public static void main(String[] args) throws Exception {
//新建 buf,处于写入模式
ByteBuffer buf = ByteBuffer.allocate(1024);
FileInputStream in = new FileInputStream("a.txt");
FileOutputStream out = new FileOutputStream("b.txt");
FileChannel inChannel = in.getChannel();
FileChannel outChannel = out.getChannel();
int length = -1;
//从输入通道读取到 buf
while ((length=inChannel.read(buf))!=-1){
//buf 第一次模式切换:翻转 buf,从写入模式变成读取模式
buf.flip();
int outLength = 0;
//将 buf 写入到输出的通道
while ((outLength=outChannel.write(buf))!=0){
System.out.println("写入的字节数:"+outLength);
}
//将 buf 写入到输出的通道
buf.clear();
}
//强制刷新到磁盘
outChannel.force(true);
//关闭所有的可关闭对象
outChannel.close();
out.close();
inChannel.close();
in.close();
}
除了FileChannel的通道操作外,还需要注意代码执行过程中隐藏的ByteBuffer的模式切换。由于新建的ByteBuffer是写入模式,才可作为inChannel.read(ByteBuffer)方法的参数,inChannel.read(…)方法将从通道inChannel读到的数据写入到ByteBuffer。然后,需要调用缓冲区的flip方法,将ByteBuffer从写入模式切换成读取模式,才能作为outchannel.write(ByteBuffer)方法的参数,以便从ByteBuffer读取数据,最终写入到outchannel输出通道。
完成一次复制之后,在进入下一次复制前,还要进行一次缓冲区的模式切换。此时,需要将通过clear方法将Buffer切换成写入模式,才能进入下一次的复制。所以,在示例代码中,每一轮外层的while循环,都需要两次ByteBuffer模式切换:第一次模式切换时,翻转buf,变成读取模式;第二次模式切换时,清除buf,变成写入模式。
作为文件复制的程序来说,以上实战代码的效率不是最高的。更高效的文件复制,可以调用
文件通道的transferFrom方法。具体的代码如下:
public static void testCopyQuick() throws Exception {
FileInputStream inputStream = new FileInputStream("a.txt");
FileOutputStream outputStream = new FileOutputStream("b.txt");
FileChannel inChannel = inputStream.getChannel();
FileChannel outChannel = outputStream.getChannel();
outChannel.transferFrom(inChannel,0,inChannel.size());
outChannel.close();
inChannel.close();
}
在NIO中,涉及网络连接的通道有两个:
其中,NIO中的SocketChannel传输通道,与OIO中的Socket类对应;NIO中的ServerSocketChannel监听通道,对应于OIO中的ServerSocket类。
ServerSocketChannel仅仅应用于服务器端,而SocketChannel则同时处于服务器端和客户端,所以,对应于一个连接,两端都有一个负责传输的SocketChannel传输通道。
无论是ServerSocketChannel,还是SocketChannel,都支持阻塞和非阻塞两种模式,其设置方法如下:
在阻塞模式下,SocketChannel通道的connect连接、read读、write写操作,都是同步的和阻塞式的,在效率上与Java旧的OIO的面向流的阻塞式读写操作相同。
在非阻塞模式下,通道的操作是异步、高效率的,这也是相对于传统的OIO的优势所在。
在客户端,先通过SocketChannel静态方法open()获得一个套接字传输通道;然后,将socket套接字设置为非阻塞模式;最后,通过connect()实例方法,对服务器的IP和端口发起连接。代码如下:
//获得一个套接字传输通道
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞模式
socketChannel.configureBlocking(false);
//对服务器的 IP 和端口发起连接
socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));
非阻塞情况下,与服务器的连接可能还没有真正建立,socketChannel.connect方法就返回了,因此需要不断地自旋,检查当前是否是连接到了主机:
//获得一个套接字传输通道
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞模式
socketChannel.configureBlocking(false);
//对服务器的 IP 和端口发起连接
boolean connect = socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
while (!socketChannel.finishConnect()){
//不断地自旋、等待,或者做一些其他的事情……
}
在服务器端,获取与客户端对应的传输套接字方式如下:
在连接建立的事件到来时,服务器端的ServerSocketChannel能成功地查询出这个新连接事件,并且通过调用服务器端ServerSocketChannel监听套接字的accept()方法,来获取新连接的套接字通道:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
SocketChannel socketChannel1 = serverSocketChannel.accept();
socketChannel1.configureBlocking(false);
NIO 套 接 字 通 道 , 主 要 用 于 非 阻 塞 的 传 输 场 景 。 所 以 , 基 本 上 都 需 要 调 用 通 道 的configureBlocking(false)方法,将通道从阻塞模式切换为非阻塞模式。
当SocketChannel传输通道可读时,可以从SocketChannel读取数据,具体方法与前面的文件通道读取方法是相同的。调用read方法,将数据读入缓冲区ByteBuffer。
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buf);
在读取时,因为是异步的,因此我们必须检查read的返回值,以便判断当前是否读取到了数据。read()方法的返回值是读取的字节数,如果返回-1,那么表示读取到对方的输出结束标志,对方已经输出结束,准备关闭连接。实际上,通过read方法读数据,本身是很简单的,比较困难的是,在非阻塞模式下,如何知道通道何时是可读的呢?这就需要用到NIO的新组件——Selector通道选择器.
和前面的把数据写入到FileChannel文件通道一样,大部分应用场景都会调用通道的int write(ByteBuffer buf)方法。
//写入前需要读取缓冲区,要求 ByteBuffer 是读取模式
buf.flip();
socketChannel.write(buf);
在关闭SocketChannel传输通道前,如果传输通道用来写入数据,则建议调用一次 shutdownOutput()终止输出方法,向对方发送一个输出的结束标志(-1)。然后调用 socketChannel.close()方法,关闭套接字连接。
//调用终止输出方法,向对方发送一个输出的结束标志
socketChannel.shutdownOutput();
//关闭套接字连接
socketChannel.close();
使用FileChannel文件通道读取本地文件内容,然后在客户端使用SocketChannel套接字通道,把文件信息和文件内容发送到服务器。客户端的完整代码如下:
public static void sendFile() throws Exception {
File file = new File("a.txt");
FileChannel fileChannel = new FileInputStream(file).getChannel();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",8888));
socketChannel.configureBlocking(false);
//发送文件名称
ByteBuffer fileNameByteBuffer = Charset.defaultCharset().encode("b.txt");
ByteBuffer buffer = ByteBuffer.allocate(100000);
//发送文件名称长度
int fileNameLen = fileNameByteBuffer.capacity();
buffer.putInt(fileNameLen);
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
//发送文件名称
socketChannel.write(fileNameByteBuffer);
//发送文件长度
buffer.putLong(file.length());
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
//发送文件内容
int length = 0;
long progress = 0;
while ((length=fileChannel.read(buffer))>0){
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
progress += length;
}
fileChannel.close();
socketChannel.shutdownOutput();
socketChannel.close();
}
在Java中使用UDP协议传输数据,比TCP协议更加简单。和Socket套接字的TCP传输协议不同,UDP协议不是面向连接的协议。使用UDP协议时,只要知道服务器的IP和端口,就可以直接向对方发送数据。在Java NIO中,使用DatagramChannel数据报通道来处理UDP协议的数据传输。
获取数据报通道的方式很简单,调用DatagramChannel类的open静态方法即可。然后调用configureBlocking(false)方法,设置成非阻塞模式。
//获取 DatagramChannel 数据报通道
DatagramChannel channel = DatagramChannel.open();
//设置为非阻塞模式
datagramChannel.configureBlocking(false);
如果需要接收数据,还需要调用bind方法绑定一个数据报的监听端口,具体如下:
//调用 bind 方法绑定一个数据报的监听端口
channel.socket().bind(new InetSocketAddress(18080));
当DatagramChannel通道可读时,可以从DatagramChannel读取数据。和前面的SocketChannel读取方式不同,这里不调用read方法,而是调用receive(ByteBufferbuf)方法将数据从DatagramChannel读入,再写入到ByteBuffer缓冲区中。
//创建缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//从 DatagramChannel 读入,再写入到 ByteBuffer 缓冲区
SocketAddress clientAddr= datagramChannel.receive(buf);
通道读取receive(ByteBufferbuf)方法虽然读取了数据到buf缓冲区,但是其返回值是 SocketAddress类型,表示返回发送端的连接地址(包括IP和端口)。通过receive方法读取
数据非常简单,但是,在非阻塞模式下,如何知道DatagramChannel通道何时是可读的呢? 和SocketChannel一样,同样需要用到NIO的新组件—Selector通道选择器
向DatagramChannel发送数据,和向SocketChannel通道发送数据的方法也是不同的。这里不是调用write方法,而是调用send方法。示例代码如下:
//把缓冲区翻转到读取模式
buffer.flip();
//调用 send 方法,把数据发送到目标 IP+端口
datagramChannel.send(buffer, new InetSocketAddress("127.0.0.1",18899));
//清空缓冲区,切换到写入模式
buffer.clear();
由于UDP是面向非连接的协议,因此,在调用send方法发送数据的时候,需要指定接收方的地址(IP和端口)。
//简单关闭即可
datagramChannel.close();
使用DatagramChannel数据包通到发送数据的客户端示例程序代码。其功能是:获取用户的输入数据,通过DatagramChannel数据报通道,将数据发送到远程的服务器。客户端的完整程序代码如下:
public void send() throws IOException {
//获取 DatagramChannel 数据报通道
DatagramChannel dChannel = DatagramChannel.open();
//设置为非阻塞
dChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(10000);
Scanner scanner = new Scanner(System.in);
Print.tcfo("UDP 客户端启动成功!");
Print.tcfo("请输入发送内容:");
while (scanner.hasNext()) {
String next = scanner.next();
buffer.put((Dateutil.getNow() + " >>" + next).getBytes());
buffer.flip();
//通过DatagramChannel 数据报通道发送数据
dChannel.send(buffer,new InetSocketAddress("127.0.0.1",18899));
buffer.clear();
}
//操作四:关闭 DatagramChannel 数据报通道
dChannel.close();
}
服务器端通过DatagramChannel数据包通道接收数据的程序代码如下:
public void receive() throws IOException {
//获取 DatagramChannel 数据报通道
DatagramChannel datagramChannel = DatagramChannel.open();
//设置为非阻塞模式
datagramChannel.configureBlocking(false);
//绑定监听地址
datagramChannel.bind(new InetSocketAddress("127.0.0.1",18899));
Print.tcfo("UDP 服务器启动成功!");
//开启一个通道选择器
Selector selector = Selector.open();
//将通道注册到选择器
datagramChannel.register(selector, SelectionKey.OP_READ);
//通过选择器,查询 IO 事件
while (selector.select() > 0) {
Iterator iterator = selector.selectedKeys().iterator();
ByteBuffer buffer =ByteBuffer.allocate(1000);
//迭代 IO 事件
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//可读事件,有数据到来
if (selectionKey.isReadable()) {
//读取 DatagramChannel 数据报通道的数据
SocketAddress client = datagramChannel.receive(buffer);
buffer.flip();
Print.tcfo(new String(buffer.array(), 0, buffer.limit()));
buffer.clear();
}
}
iterator.remove();
}
//关闭选择器和通道
selector.close();
datagramChannel.close();
}
在服务器端,首先调用了bind方法绑定datagramChannel的监听端口。当数据到来后,调用了receive方法,从datagramChannel数据包通道接收数据,再写入到ByteBuffer缓冲区中。