基础 | NIO - [Channel]
@
§1 概述
- 是可以对数据源操作的双向通道
- 操作的数据源可以是文件,也可以是网络 IO
- 连接 字节缓冲区 和 另一端的实体
- 主要实现包括
FileChannelAsynchronousFileChannelDatagramChannelSocketChannelServerSocketChannel
与 stream 区别
- 双向
- 异步
- 不直接读写数据(读写数据的工作交给缓冲区)
§2 FileChannel
§2.1 FileChannel
作用
读写文件的通道
常用方法
打开 FileChannel
- 通过
RandomAccessFile获取// 创建 RandomAccessFile 文件 RandomAccessFile raf = new RandomAccessFile(file,mode; // 从 RandomAccessFile 文件获取 FileChannel FileChannel channel = raf.getChannel(); - 通过
FileChannel.open()开启FileChannel.open(path, options);
从 FileChannel 读
int length = channel.read(buf);
- 必须借助缓冲区
- 返回读取到的长度
向 FileChannel 写
channel.write(buf);
channel.write(buf,position);
channel.write(buf[]);
- 必须借助缓冲区
annel.write(buf)默认向FileChannel关联文件的结尾写
效果等同于annel.write(buf,buf.size())- 可以同时使用多个缓冲区以达到分散读取聚集写入的效果
关闭 FileChannel
channel.close();
- 注意处理异常
- 推荐使用 try-with-resource 的写法,可以省略
close()
try (
RandomAccessFile raf = new RandomAccessFile(path + file,"rw");
FileChannel channel = raf.getChannel()
){
} catch (Exception e){ /* 异常处理 */ }
操作 FileChannel 位置
// 获取 FileChannel 的位置
long pos = channel.position();
// 设置 FileChannel 的位置
channel.position(pos + 100);
- 通过
position()可以实现读写FileChannel的指定位置 - 设置 position 时,若超出了文件结束符,并向其中写入数据,会形成文件空洞
获取 FileChannel 关联文件的大小
channel.size();
RandomAccessFile.length()的返回相同
截取 FileChannel 关联文件
channel.truncate(length);
- 用于截取
FileChannel关联的文件的前 length 个字节
强制写 FileChannel 关联文件
channel.force();
- 用于将
FileChannel中未完成写操作的内容强制写入磁盘
NIO 是异步的,默认不保证写入FileChannel的数据可以实时写入磁盘
当需要保证数据即时写入磁盘时,需要调用force() - 可以传入 boolean 参数,以指定文件元数据是否也强制写入磁盘
在 FileChannel 间传递数据
channel.transferFrom(fromChannel,0,fromChannel.size());
channel.transferTo(0,channel.size(), toChannel);
transferFrom()和transferTo()中任一都可以实现在FileChannel间传递数据- 通过
transferFrom()或transferTo()传输数据不需要缓冲区- 方法内部会自动使用一个 8M/2G 的缓冲区
- 若传输数据长度小于 8M/2G ,缓冲区大小就使用此长度
向 Selector 注册
参考 基础 | NIO - [Selector]#注册
标准写法
读文件
public void read(String path, String file, int buffer) {
try (
RandomAccessFile raf = new RandomAccessFile(path + file,"rw");
// 获取 FileChannel
FileChannel channel = raf.getChannel()
){
// 创建读取时的 缓冲区
ByteBuffer buf = ByteBuffer.allocate(buffer);
// 读取 channel.read(buf)
for(int length = channel.read(buf); length != -1; length = channel.read(buf)){
buf.flip();
//读取后的动作
for(;buf.hasRemaining();)
System.out.print((char) buf.get());
buf.clear();
}
} catch (Exception e){ /* 异常处理 */ }
}
写文件
public void write(String path, String file, int buffer, String content) {
try (
RandomAccessFile raf = new RandomAccessFile(path + file,"rw");
// 获取 FileChannel
FileChannel channel = raf.getChannel();
){
// 创建读取时的 缓冲区
ByteBuffer buf = ByteBuffer.allocate(buffer);
buf.clear();
buf.put(content.getBytes(StandardCharsets.UTF_8));
buf.flip();
for(;buf.hasRemaining();){
channel.write(buf);
}
} catch (Exception e){ /* 异常处理 */ }
}
复制文件
public void copy(String srcFilePath, String descFilePath){
try (
RandomAccessFile srcFile = new RandomAccessFile(srcFilePath,"r");
RandomAccessFile descFile = new RandomAccessFile(descFilePath,"rw");
FileChannel rc = srcFile.getChannel();
FileChannel wc = descFile.getChannel();
) {
ByteBuffer buf = ByteBuffer.allocate(1024);
for(int length=rc.read(buf) ; ; buf.clear(), length= rc.read(buf)){
if(-1==length)
break;
buf.flip();
while (buf.hasRemaining()) {
wc.write(buf);
}
}
} catch (Exception e){ /* 异常处理 */ }
}
复制文件,基于 transfer
public void copyByTransfer(String srcFilePath, String descFilePath){
try (
RandomAccessFile srcFile = new RandomAccessFile(srcFilePath,"r");
RandomAccessFile descFile = new RandomAccessFile(descFilePath,"rw");
FileChannel rc = srcFile.getChannel();
FileChannel wc = descFile.getChannel();
) {
rc.transferTo(0,rc.size(),wc);
} catch (Exception e){ /* 异常处理 */ }
}
复制文件,基于 Scatter / Gather
public void copyScatterGather(String srcFilePath, String descFilePath){
try (
RandomAccessFile srcFile = new RandomAccessFile(srcFilePath,"r");
RandomAccessFile descFile = new RandomAccessFile(descFilePath,"rw");
FileChannel rc = srcFile.getChannel();
FileChannel wc = descFile.getChannel();
) {
ByteBuffer[] bufs = new ByteBuffer[5];
Arrays.setAll(bufs, e->ByteBuffer.allocate(100));
for(long length=rc.read(bufs) ; length!=-1 ; length=rc.read(bufs)){
Arrays.stream(bufs).forEach(ByteBuffer::flip);
wc.write(bufs);
Arrays.stream(bufs).forEach(ByteBuffer::clear);
}
} catch (Exception e){ /* 异常处理 */ }
}
§2.2 AsynchronousFileChannel
作用
异步读写文件的通道
常用方法
创建
open()
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path,StandardOpenOption.READ);
异步读
future()
Future<Integer> count = channel.read(buf, pos);
标准写法
异步读文件
public void read(String path, String file, int buffer) {
try (
AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get(path+file), StandardOpenOption.READ)
){
ByteBuffer buf = ByteBuffer.allocate(buffer);
long pos = 0;// 文件读取位置
List<byte[]> datas = new ArrayList<>();
for(Future<Integer> count = channel.read(buf, pos); pos<channel.size();pos+= count.get(), count = channel.read(buf,pos)){
while(!count.isDone()){
TimeUnit.MILLISECONDS.sleep(200);
}
buf.flip();
//读取后的动作
datas.add(ArrayUtil.sub(buf.array(),0,Math.min(buf.array().length,count.get())));
buf.clear();
}
System.out.println(new String(ArrayUtil.addAll(datas.toArray(new byte[][]{})), StandardCharsets.UTF_8));
} catch (Exception e){ /* 异常处理 */ }
}
异步读文件(CompletionHandler)
案例比想象中的复杂,因此另开了一个帖子
详情参考 坑 | NIO - [AsynchronousFileChannel + CompletionHandler]
§3 SocketChannel
下面三个 channel 都属于 SocketChannel
DatagramChannelSocketChannelServerSocketChannel
DatagramChannel |
SocketChannel |
ServerSocketChannel |
|
|---|---|---|---|
| 可被多路复用 | √ | √ | √ |
| 可读写 | √ | √ | × |
Socket 和 SocketChannel
Socket不需要继续实现对应SocketChannel的API- 可以从
SocketChannel实例中通过getSocket()获取对应的socket()对象 - 也可以在
Socket实例通过getChannel()获取关联的SocketChannel SocketChannel可以通过AbstractSelectableChannel实现多路复用,达到异步效果
§3.1 ServerSocketChannel
特点
- 是基于通道的 socket 监听器,功能类似
ServerSocket,相当于非阻塞版 ServerSocketChannel通过获取其socket实例以绑定端口进行监听ServerSocketChannel的accept()方法返回可以在非阻塞模式下运行的SocketChannel
常用方法
打开 ServerSockerChannel
ServerSocketChannel channel = ServerSocketChannel.open();
- 通过
ServerSocketChannel.open()打开
关闭 ServerSockerChannel
channel.close();
- 通过
ServerSocketChannel.close()关闭 - 推荐使用 try-with-resource 语法
建立 ServerSockerChannel 与端口的绑定
channel.bind(new InetSocketAddress(ip,port));
- 可以直接调用
ServerSocketChannel.bind() - 可以直接通过获取其对应
socket并在此实例上bind()
监听网络连接
SocketChannel accepted = channel.accept();
- 工作在阻塞模式下的
ServerSockerChannel会阻塞在accept()方法上 - 工作在非阻塞模式下的
ServerSockerChannel不会阻塞,但因此可能得到 null ,需要判断 accept()会返回一个工作在非阻塞模式下的SockerChannel
标准写法
public void listen(String ip, int port,String content){
try (
ServerSocketChannel channel = ServerSocketChannel.open();
){
channel.bind(new InetSocketAddress(ip,port));
channel.configureBlocking(false);
SocketChannel accepted = null;
while(true){
accepted = channel.accept();
if(null==accepted) {
TimeUnit.SECONDS.sleep(1);
continue;
}
System.out.println(accepted.socket().getRemoteSocketAddress());
accepted.close();
}
} catch (Exception e) { /* 异常处理 */ }
}
§3.2 SocketChannel
特点
- 是对
socket的异步包装,用于处理网络 IO 的通道,基于 TCP 连接 - 通过
open()打开,通过connect()连接到指定地址,对未连接的SocketChannel进行 IO 操作会抛异常 - 支持阻塞和非阻塞模式,可以被多路复用
- 支持异步关闭
- 读阻塞时,其他线程通过
shutdownInput中断阻塞,没读到数据时阻塞线程返回 -1 - 写阻塞时,其他线程通过
shutdownWrite中断阻塞,阻塞线程抛出AsynchronousCloseException
- 读阻塞时,其他线程通过
常用参数
- SO_SNDBUF 发送缓冲区大小
- SO_RCVBUF 接受缓冲区大小
- SO_KEEPALIVE 连接保持存活
- O_REUSEADDR 复用地址
- SO_LINGER 非阻塞模式下,数据传输过程中延时关闭
SocketChannel - TCP_NODELY 禁用 Nagle 算法,在不确认数据时,不启用缓存去缓存数据
常用方法
打开 SockerChannel并连接
SocketChannel channel = SocketChannel.open(new InetSocketAddress(ip,port));
// 或
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress(ip,port));
- 通过
SocketChannel.open()打开 - 可以在
open()的同时连接,也可以open()后通过connect()连接
关闭 SockerChannel
channel.close();
- 通过
SocketChannel.close()关闭 - 推荐使用 try-with-resource 语法
测试 SockerChannel连接
boolean opened = channel.isOpen();
boolean connecting = channel.isConnectionPending();
boolean connected = channel.isConnected();
boolean finished = channel.finishConnect();
- 是否已经打开
- 是否建立连接中
- 是否已经建立连接
- 是否已经结束连接
设置 SockerChannel 是否阻塞
channel.configureBlocking(false);
从 SockerChannel 中读
channel.read(buf);
从 SockerChannel 中获取参数
channel.getOptition(StandardSocketOptions.SO_KEEPALIVE);
- 参数见上文 常见参数
- 参数已被
StandardSocketOptions封装
向 SockerChannel 设置参数
channel.setOption(StandardSocketOptions.SO_KEEPALIVE,Boolean.TRUE);
- 参数见上文 常见参数
- 参数已被
StandardSocketOptions封装
监听网络连接
SocketChannel accepted = channel.accept();
- 工作在阻塞模式下的
ServerSockerChannel会阻塞在accept()方法上 - 工作在非阻塞模式下的
ServerSockerChannel不会阻塞,但因此可能得到 null ,需要判断 accept()会返回一个工作在非阻塞模式下的SockerChannel
标准写法
略
§3.3 DatagramChannel
特点
- 是对
datafram的异步包装,用于处理网络 IO 的通道,基于 UDP 连接 - 通过
open()打开,DatagramChannel是无连接的- 可以发送数据报给不同地址
- 可以接受不同地址发来的数据报
- 每个数据报都包含原地址
常用方法
打开 DatagramChannel
DatagramChannel channel = DatagramChannel.open();
- 通过
DatagramChannel.open()打开 - 可以在
open()的同时连接,也可以open()后通过connect()连接
关闭 DatagramChannel
channel.close();
- 通过
DatagramChannel.close()关闭 - 推荐使用 try-with-resource 语法
DatagramChannel 绑定
channel.bind(new InetSocketAddress(ip,port));
- 建立绑定后可以使用
DatagramChannel进行receive()、send()操作
DatagramChannel 绑定
channel.bind(new InetSocketAddress(ip,port));
- 建立连接后可以使用
DatagramChannel进行read()、write()操作 - 因为
DatagramChannel是基于 UDP 的,因此不存在真正意义上的连接
这里的连接只是为了可以进行读写操作 - 未连接使用读写方法会抛出
NotYetConnectedException read()没有接收到数据报时,抛出PortUnreachableException
通过 DatagramChannel 接收数据
ByteBuffer buf = ByteBuffer.allocate(1024);
SocketAddress received = channel.receive(buf);
通过 DatagramChannel 发送数据
ByteBuffer buf = ByteBuffer.wrap(content.getBytes());
channel.send(buf,new InetSocketAddress(ip,port));
从 DatagramChannel 中读
// 先
channel.connect(new InetSocketAddress(ip,port));
// 然后才可以
int leangth = channel.read(buf);
向 DatagramChannel 中写
// 先
channel.connect(new InetSocketAddress(ip,port));
// 然后才可以
channel.write(buf);
标准写法
本地给本地发包
public void receive(int port){
try(
DatagramChannel channel = DatagramChannel.open();
) {
channel.bind(new InetSocketAddress(port));
for(ByteBuffer buf = ByteBuffer.allocate(1024);;buf.clear()){
SocketAddress received = channel.receive(buf);
if(null == received){
TimeUnit.SECONDS.sleep(1);
}
buf.flip();
System.out.println(received.toString());
System.out.println(Charset.forName("UTF-8").decode(buf));
}
} catch (Exception e) { e.printStackTrace();}
}
public void send(String ip,int port){
try(
DatagramChannel channel = DatagramChannel.open();
) {
ByteBuffer buf = null;
String content = null;
for(;;buf.clear()){
content = String.valueOf(System.currentTimeMillis());
buf = ByteBuffer.wrap(content.getBytes());
channel.send(buf,new InetSocketAddress(ip,port));
System.out.println(content);
TimeUnit.SECONDS.sleep(1);
}
} catch (Exception e) { e.printStackTrace(); }
}

浙公网安备 33010602011771号