4.通道(Channel)
4.1Channel的只要实现类
通道(Channel):由 java.nio.channels 包定义的。Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据,Channel 只能与Buffer 进行交互。
- FileChannel:用于读取、写入、映射和操作文件的通道。
- DatagramChannel:通过 UDP 读写网络中的数据通道。
- SocketChannel:通过 TCP 读写网络中的数据。
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。
4.2 获取通道
- 获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:
- FileInputStream
- FileOutputStream
- RandomAccessFile
- DatagramSocket
- Socket
- ServerSocket
- 获取通道的其他方式是java 1.7中的NIO2使用 Files 类的静态方法 newByteChannel() 获取字节通道。
- 或者通过java 1.7中的NIO2提供的各个通道的静态方法 open() 打开并返回指定通道。
4.3通道的数据传输
-
将 Buffer 中数据写入 Channel, int bytesWritten = inChannel.write(buf);
-
从 Channel 读取数据到 Buffer, int bytesRead = inChannel.read(buf);
-
利用通道复制文件demo:
@Test public void testChannel() { //输入 输出流 FileInputStream fis = null; FileOutputStream fos = null; //通道 FileChannel inChannel = null; FileChannel outChannel = null; try { //1,创建channel fis = new FileInputStream("下载.jpg"); inChannel = fis.getChannel(); fos = new FileOutputStream("2.jpg"); outChannel = fos.getChannel(); //2,创建buffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //3,读取 while (inChannel.read(byteBuffer) != -1){ //4 调换buffer byteBuffer.flip(); //5 将缓存区数据写入通道 outChannel.write(byteBuffer); //6 清空buffer byteBuffer.clear(); } } catch (Exception e) { e.printStackTrace(); }finally { //关闭各种通道 try { if(fis != null){ fis.close(); } if(fos != null){ fos.close(); } if(inChannel != null){ inChannel.close(); } if(outChannel != null){ outChannel.close(); } } catch (IOException e) { System.out.println("关闭通道异常!"); e.printStackTrace(); } } }
-
利用直接缓存区复制文件demo:
/**
*使用直接缓存区进行复制文件demo
* demo中没有处理异常,正常代码中应该处理异常
*/
@Test
public void testChannel2() throws IOException {
//1 创建channel
FileChannel inChannel = FileChannel.open(Paths.get("下载.jpg"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("3.jpg"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//2 创建物理映射buffer
MappedByteBuffer inMap = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMap = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
//3 直接对缓冲区进行操作
byte[] bytes = new byte[inMap.limit()];
inMap.get(bytes);
outMap.put(bytes);
//4 关闭通道
inChannel.close();
outChannel.c lose();
}
-
通道直接的数据传输
-
transferTo( ):将A传输到B A.transferTo()
-
transferFrom( ):从A传输到B B.transferForm()
/**
* 直接使用通道进行传输
*/
@Test
public void transferTest() throws IOException {
FileChannel inChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.READ);
//此处先创建要被传输到的文件,要不然报 没有此文件的异常
Files.createFile(Paths.get("5.jpg"));
FileChannel outChannel = FileChannel.open(Paths.get("5.jpg"), StandardOpenOption.WRITE, StandardOpenOption.READ);
//直接传输
//inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.transferFrom(inChannel, 0, inChannel.size());
//关闭流
inChannel.close();
outChannel.close();
}
4.4 分散读取和聚合写入
-
分散读取(Scattering Reads)是指从 Channel 中读取的数据“分散”到多个 Buffer 中。注意:按照缓冲区的顺序,从 Channel 中读取的数据依次将 Buffer 填满。
-
聚集写入(Gathering Writes)是指将多个 Buffer 中的数据“聚集”到 Channel。注意:按照缓冲区的顺序,写入 position 和 limit 之间的数据到 Channel 。
demo:
/** * * 分算读取,聚合写入的测试 * @author * @version 1.00 * @time 2020/3/17 0017 下午 10:49 */ public class ScattergatherTest { /** * 采用随机读取文件流进行测试 */ @Test public void test() throws Exception { //1 获取通道 RandomAccessFile file = new RandomAccessFile("1.txt", "rw"); FileChannel inChannel = file.getChannel(); //2 分配多个缓冲区 ByteBuffer buffer1 = ByteBuffer.allocate(10); ByteBuffer buffer2 = ByteBuffer.allocate(10); ByteBuffer buffer3 = ByteBuffer.allocate(10); //3 分散读取 ByteBuffer[] buffers = {buffer1, buffer2, buffer3}; inChannel.read(buffers); for (ByteBuffer buffer : buffers) { //翻转 buffer.flip(); } System.out.println(new String(buffers[0].array(), 0, buffer1.limit())); System.out.println("-----------------------------"); System.out.println(new String(buffers[1].array(), 0, buffer2.limit())); System.out.println(new String(buffers[2].array(), 0, buffer3.limit())); //4 聚合写入 RandomAccessFile file1 = new RandomAccessFile("2.txt", "rw"); FileChannel outChannel = file1.getChannel(); outChannel.write(buffers); inChannel.close(); outChannel.close(); } }
5.NIO 的非阻塞式网络通信
- 传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
- Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入
和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
5.1 选择器(Selector)
- 选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
- SelectableChannle 的结构:
- SelectableChannel
- AbstractSelectableChannel
- SocketChannel
- ServerSocketChannel
- DatagramChannel
- AbstractSelectableChannel
- SelectableChannel
5.1.1 Selector的应用
-
创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。
Selector selector = Selector.open();
-
向选择器注册通道:SelectableChannel.register(Selector sel, int ops)
-
当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。
-
可以监听的事件类型(用 可使用 SelectionKey 的四个常量 表示):
-
读: SelectionKey.OP_READ (1)
-
写:SelectionKey.OP_WRITE (4)
-
连接: SelectionKey.OP_CONNECT (8)
-
接收: SelectionKey.OP_ACCEPT(16)
-
若注册时不止监听一个事件,则可以使用“位或”操作符连接。
SelectionKey.OP_READ|SelectionKey.OP_WRITE;
5.1.2 SelectionKey
- SelectionKey:表示 SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。
方 法 | 描 述 |
---|---|
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测 Channal 中读事件是否就绪 |
boolean isWritable() | 检测 Channal 中写事件是否就绪 |
boolean isConnectable() | 检测 Channel 中连接是否就绪 |
boolean isAcceptable() | 检测 Channel 中接收是否就绪 |
5.1.3 Selector 的常用方法
方 法 | 描 述 |
---|---|
Set keys() | 所有的 SelectionKey 集合。代表注册在该Selector上的Channel |
selectedKeys() | 被选择的 SelectionKey 集合。返回此Selector的已选择键集 |
int select() | 监控所有注册的Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应得的 SelectionKey 加入被选择的SelectionKey 集合中,该方法返回这些Channel 的数量。 |
int select(long timeout) | 可以设置超时时长的 select() 操作 |
int selectNow() | 执行一个立即返回的 select() 操作,该方法不会阻塞线程 |
Selector wakeup() | 使一个还未返回的 select() 方法立即返回 |
void close() | 关闭该选择器 |
5.2 SocketChannel、ServerSocketChannel、DatagramChannel
5.2.1 SocketChannel
- Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。
- 操作步骤:
- 打开 SocketChannel
- 读写数据
- 关闭 SocketChannel
5.2.2 ServerSocketChannel
- Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道,就像标准IO中的ServerSocket一样。
/**
* 使用非阻塞的方式数据传输
*
* @author
* @version 1.00
* @time 2020/3/18 0018 下午 9:20
*/
public class NioTest {
public static void main(String[] args) {
try {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9527));
//切换为非阻塞模式
socketChannel.configureBlocking(false);
//分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//数据填充
buffer.put((LocalDateTime.now().toString()).getBytes());
//翻转
buffer.flip();
//发送
socketChannel.write(buffer);
buffer.clear();
//关闭
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void server() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2 切换为非阻塞模式
serverSocketChannel.configureBlocking(false);
//3 绑定
serverSocketChannel.bind(new InetSocketAddress(9527));
//4 创建选择器
Selector selector = Selector.open();
//5 将通道注册到选择器上 设置监听事件为 接收
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//6 轮询的获取选择器上已经准备就绪的事件
while (selector.select() > 0) {
//7 被选择的 SelectionKey 集合。返回此Selector的已选择键集
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
//8 获取准备就绪的时间
SelectionKey key = iterator.next();
//9 判断具体是什么事件准备完毕
if (key.isAcceptable()) {
//10 若接收就绪 获取客户端连接
SocketChannel acceptChannel = serverSocketChannel.accept();
//11 切换非阻塞模式
acceptChannel.configureBlocking(false);
//12 将该选择器注册到选择器上 读操作
acceptChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {//读就绪
//13 获取当前选择器上的读就绪的通道
SocketChannel inChannel = (SocketChannel) key.channel();
//读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Integer len = 0;
while( (len = inChannel.read(byteBuffer)) != -1){
byteBuffer.flip();
//打印
System.out.println(new String(byteBuffer.array(), 0, len));
byteBuffer.clear();
}
}
//取消已选择的key 必须取消 此步骤很重要
iterator.remove();
}
}
}
}
5.2.3 DatagramChannel
- Java NIO中的DatagramChannel是一个能收发UDP包的通道。
- 操作步骤:
- 打开 DatagramChannel
- 接收/发送数据
6.管道(Pipe)
-
Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uTtvZWX4-1584702199139)(C:\Users\Administrator\Desktop\Md文档\nio\pipe.jpg)]
-
向通道写入数据;
-
从通道读取数据;
System.out.println(new String(byteBuffer.array(), 0, len)); byteBuffer.clear(); } } //取消已选择的key 必须取消 此步骤很重要 iterator.remove(); } }
}
}
### 5.2.3 DatagramChannel
* Java NIO中的DatagramChannel是一个能收发UDP包的通道。
* 操作步骤:
* 打开 DatagramChannel
* 接收/发送数据
# 6.管道(Pipe)
* Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
[外链图片转存中...(img-uTtvZWX4-1584702199139)]
* 向通道写入数据;
* 从通道读取数据;