假如我们把NIO
比作整个铁路系统,Channel
(通道)就是整个系统中的轨道,作为NIO
的核心组件之一,其承担着传输数据的作用。和标准IO
相比,我们用stream(流)
来传输数据,两者的区别在于Channel
是双向的,而stream
是单向的。另外,可直接向stream
写入数据或从中读取数据,而Channel
却不能,它需要和Buffer
配合使用,就像乘客不能直接在轨道上传输,需要坐在火车上(这里的火车就相当于Buffer
)进行传输。
Channel
的分类
java.nio.*
为Channel提供了很多的实现类,这里主要介绍下面四个实现类。
- FileChannel:从文件中读取数据
- DatagramChannel:通过
UDP
读写网络中的数据 - SocketChannel:通过
TCP
读写网络中的数据 - ServerSocketChannel:用于监听新进来的
TCP
连接,对每一个新进来的连接都会创建一个SocketChannel
。
Channel
的基本用法
使用Channel
时,要与上一篇介绍的 Buffer
配合使用。网上有很多文章有这种类似的说法:
将
Channel
中的数据写入Buffer
或者将Buffer
中的数据读取到Channel
您永远不会将字节直接写入通道中,也不会直接从通道中读取字节
这两个说法感觉有点矛盾,既然通道里面不能写入数据,何来将通道中的数据写入缓冲区呢?所以这里我按照自己的理解来叙述。
- 写操作时,我们将
Buffer
中的数据写入到Channel
连接的数据目的地(Channel
中并没有直接存储数据)
- 读操作时,我们将
Channel
连接的数据源中的数据填充到Buffer
中(Channel
中并没有直接存储数据)。
FileChannel
的使用
FileChannel
是一个用于读取,写入,映射和操作文件的通道,它本身是阻塞式的,不能设置为非阻塞状态,也因此它是线程安全的。下面就用代码简单的演示通过FileChannel
对文件的读写。
- 读取文件:从指定文件
channel.txt
中读取数据到缓冲区
package cn.lincain.filechannel.read;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelDemo {
public static void main(String[] args) throws Exception {
// 通过RandomAccessFile获取文件通道,这里也可以通过FileInputStream获取
RandomAccessFile file =
new RandomAccessFile("channel.txt", "rw");
FileChannel channel = file.getChannel();
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 通过channel将数据源的数据读入缓冲区
channel.read(buffer);
// 切换状态,从缓冲区中读取数据
buffer.flip();
while(buffer.hasRemaining()) {
System.out.print((char)buffer.get());
}
file.close();
}
}
运行结果:
读取的字节数:135
--------------文件内容--------------
package cn.lincain.filechannel;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
- 写入文件:将缓冲区中的数据写入指定的文件
channel.txt
package cn.lincain.filechannel.write;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelDemo {
public static void main(String[] args) throws Exception {
// 通过RandomAccessFile获取文件通道,这里也可以通过FileOutputStream获取
RandomAccessFile file =
new RandomAccessFile("channel.txt", "rw");
FileChannel channel = file.getChannel();
// 创建缓冲区,并向其中写入数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
String context = "package cn.lincain.arts.week3;\r\n" +
"\r\n" +
"import java.io.RandomAccessFile;\r\n" +
"import java.nio.ByteBuffer;\r\n" +
"import java.nio.channels.FileChannel;";
buffer.put(context.getBytes());
// 切换状态,并将Buffer中的数据通过Channel写入数据目的地
buffer.flip();
channel.write(buffer);
file.close();
}
}
执行上述代码后,会在项目classpath
路径下产生channel.txt
,其中的内容即为代码中的字符串。
这里可以简单总结一下FileChannel
的使用步骤:
- 调用RandomAccessFile(InputStream、OutputStream也可以)的getChannel()开启FileChannel通道
- 创建ByteBuffer对象用于从通道读取数据,或者向通道写入数据
- 读写结合:将
channel.txt
中的数据拷贝到channel-copy.txt
中
package cn.lincain.filechannel.copy;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelDemo {
public static void main(String[] args) throws Exception {
// 通过RandomAccessFile获取相应的通道
RandomAccessFile sourFile = new RandomAccessFile("channel.txt", "rw");
RandomAccessFile destFile = new RandomAccessFile("channel-copy.txt", "rw");
FileChannel sourChannel = sourFile.getChannel();
FileChannel destChannel = destFile.getChannel();
// 创建缓冲区,并clear()
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
// 通过sourChannel将channel.txt中的数据读取到buffer中,
while(sourChannel.read(buffer) != -1) {
// 切换状态
buffer.flip();
// 然后通过destChannel将buffer中数据写入到channel-copy.txt中
while(buffer.hasRemaining()) {
destChannel.write(buffer);
}
buffer.clear();
}
// 关闭资源
sourFile.close();
destFile.close();
}
}
执行上述代码后,会在项目classpath
路径下产生channel-copy.txt
,其中的内容和channel.txt
一致,即完成了复制操作。
ServerSocketChannel
和SocketChannel
的使用
从java api
上可以看到ServerSocketChannel
和SocketChannel
分别是针对面向流的侦听套接字的可选择通道和针对面向流的连接套接字的可选择通道。和ServerSocket
与Socket
类似的,后者可通过相应方法进行数据的传输,前者只是负责监听传入的SocketChannel
,而不能传输数据。
ServerSocketChannel
通过accept()
创建SocketChannel
对象从客户端读写数据,SocketChannel
通过connect()
连接服务器向其读写数据。下面用代码对两者的使用进行简单的演示。
- 服务端代码:
package cn.lincain.nio.socket;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ChannelServer {
private static final int PORT = 20000;
private static final int SLEEP_TIME = 5;
public static void main(String[] args) throws Exception {
// 创建ServerSocketChannel对象,设置端口号,并设置为非阻塞状态
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.socket().bind(new InetSocketAddress(PORT));
ssChannel.configureBlocking(false);
// 创建缓冲区用于存储数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
SocketChannel sChannel = null;
// 创建集合用于存储SocketChannel
List<SocketChannel> channels = new ArrayList<SocketChannel>();
for (;;) {
// 尝试获取socket连接通道
sChannel = ssChannel.accept();
if (sChannel != null) {
// 如果连接成功,则将该通道加入集合中
channels.add(sChannel);
} else {
// 如果连接不成功,休息5秒后,继续尝试连接
TimeUnit.SECONDS.sleep(SLEEP_TIME);
sChannel = ssChannel.accept();
if (sChannel != null)
// 如果连接成功,则将该通道加入集合中
channels.add(sChannel);
}
// 如果集合不为空,遍历集合,将每个通道的数据进行打印
if (!channels.isEmpty()) {
for (SocketChannel socketChannel : channels) {
String address = socketChannel.socket().getRemoteSocketAddress().toString();
System.out.println("Incoming connection from: " + address);
buffer.clear();
// 将通道内的数据读入缓冲区
socketChannel.read(buffer);
// 转换状态
buffer.flip();
// 从缓冲区中读取数据
while (buffer.hasRemaining())
System.out.print((char) buffer.get());
}
}
}
}
}
- 客户端代码:
package cn.lincain.nio.socket;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.TimeUnit;
public class ChannelClient {
private static final int PORT = 20000;
private static final int SLEEP_TIME = 5;
private static final String IP_ADD = "127.0.0.1";
public static void main(String[] args) throws Exception {
// 创建SocketChannel对象,设置ip地址和端口号,并设置为非阻塞状态
SocketChannel sChannel = SocketChannel.open();
sChannel.connect(new InetSocketAddress(IP_ADD, PORT));
sChannel.configureBlocking(false);
// 创建缓冲区用于存储数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
int i = 0;
// 确认是否连接成功,如果成功,则循环发送数据
while (sChannel.finishConnect()) {
System.out.println("准备发送第" + (++i) + "次发送信息");
buffer.clear();
// 向缓冲区中写入数据
String context = "Hello Server,I'm come from Client-" + i + System.lineSeparator();
buffer.put(context.getBytes());
// 转换状态
buffer.flip();
// 由缓冲区向通道内写入数据
while (buffer.hasRemaining()) {
sChannel.write(buffer);
}
TimeUnit.SECONDS.sleep(SLEEP_TIME);
}
}
}
执行代码:
Incoming connection from: /127.0.0.1:4776
Hello Server,I'm come from Client-1
Incoming connection from: /127.0.0.1:4776
Hello Server,I'm come from Client-2
Incoming connection from: /127.0.0.1:4776
Hello Server,I'm come from Client-3
针对以上两个Socket通道,这里简单总结一下它们的使用步骤:
- 服务端
- 创建ServerSocketChannel实例对象,绑定ip地址和端口号(设置阻塞状态可选)
- 调用accept()创建一个SocketChannel实例对象,用于向客户端读/写数据
- 创建数据缓冲区来读取客户端数据或向客户端发送数据
- 客户端
- 创建SocketChannel实例对象,并连接到指定的服务器
- 创建数据缓冲区来读取客户端数据或向客户端发送数据
DatagramChannel
的使用
DatagramChannel
是针对面向数据报套接字的通道,和DatagramSocket
相对应的,它也是无连接。不需要建立连接即可收发数据,下面通过一个简单代码示例进行演示。
- 服务端代码:
package cn.lincain.arts.nio.socket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
public class DataServer {
private static final int PORT = 20000;
private static final String IP_ADD = "127.0.0.1";
public static void main(String[] args) throws Exception {
// 创建一个DatagramChannel实例对象,并绑定到相应的接口
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(PORT));
// 创建两个ByteBuffer,分别用于数据的读写
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
// 从通道内接收数据,并在控制台打印
readBuffer.clear();
// 通过receive()接收消息,返回一个SocketAddress对象,表示发送消息方的地址
SocketAddress address = channel.receive(readBuffer);
System.out.println(address);
readBuffer.flip();
while (readBuffer.hasRemaining()) {
System.out.print((char) readBuffer.get());
}
// 向通道发送数据
writeBuffer.clear();
String context = "This is come from Server...";
writeBuffer.put(context.getBytes());
writeBuffer.flip();
// 通过send()发送数据,需要指定发送的ip和端口号
channel.send(writeBuffer, new InetSocketAddress(IP_ADD, 20001));
}
}
- 客户端代码:
package cn.lincain.arts.week3.datagram;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
public class DataClient {
private static final int PORT = 20001;
private static final String IP_ADD = "127.0.0.1";
public static void main(String[] args) throws Exception {
// 创建一个DatagramChannel实例对象,并绑定到相应的接口
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(PORT));
// 创建两个ByteBuffer,分别用于数据的读写
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
// 向通道发送数据
writeBuffer.clear();
String context = "This is come from Client...";
writeBuffer.put(context.getBytes());
writeBuffer.flip();
// 通过send()发送数据,需要指定发送的ip和端口号
channel.send(writeBuffer,new InetSocketAddress(IP_ADD,20000));
// 从通道内接收数据,并在控制台打印
readBuffer.clear();
// 通过receive()接收消息,返回一个SocketAddress对象,表示发送消息方的地址
SocketAddress address = channel.receive(readBuffer);
System.out.println(address);
readBuffer.flip();
System.out.print("客户端:");
while(readBuffer.hasRemaining()) {
System.out.print((char)readBuffer.get());
}
}
}
首先开启服务端,然后开启客户端,结果如下:
/127.0.0.1:20001
服务器:This is come from Client...
----------------------------------
/127.0.0.1:20000
客户端:This is come from Server...
仔细看看上面的代码,发现和前面的SocketChannel
读写数据有些区别,它们并没有采用connect()
方法进行连接,并且读写数据也不是调用read()
和write()
方法。因为UDP
是无连接的,它不需要像TCP
一样建立连接后才能读写数据。
另外我们也可以通过调用connect()
方法连接特定地址,只是并不是像TCP通道那样会创建一个真正的连接,它本质是将DatagramChannel
锁定,让其只能从特定地址收发数据。方法如下:
// 和指定的地址建立连接
channel.connect(new InetSocketAddress("127.0.0.1", 20000));
// 读写数据
channel.read(readBuffer);
channel.write(writeBuffer);
虽然DatagramChannel
也是Socket
通道,但是它是无连接的,所以使用方法相对简单一些,这里也简单总结一下它的使用步骤:
- 调用静态方法open()创建DatagramChannel的实例对象
- 创建ByteBuffer对象用于从通道读取数据,或者向通道写入数据