Java基础-NIO(buffer、channel、selector)三大组件学习笔记

又是两天没有学习了,内心十分惭愧,今天又开始学习;

Buffer

一个用于特定基本类型数据的容器。

先看结构


上图一共七个buffer类,java的8大基本数据类型唯独差了boolean,查看源码,他们都是各自对应的数组组成。

属性分析:position、limit、capacity

0 <= 标记 <= 位置 <= 限制 <= 容量 


capacity,它代表这个缓冲区的容量,一旦设定就不可以更改。比如 capacity 为 1024 的 IntBuffer,代表其一次可以存放 1024 个 int 类型的值。一旦 Buffer 的容量达到 capacity,需要清空 Buffer,才能重新写入值。

position 和 limit 是变化的,我们分别看下读和写操作下,它们是如何变化的。

position 的初始值是 0,每往 Buffer 中写入一个值,position 就自动加 1,代表下一次的写入位置。读操作的时候也是类似的,每读一个值,position 就自动加 1。

从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了。

Limit:写操作模式下,limit 代表的是最大能写入的数据,这个时候 limit 等于 capacity。写结束后,切换到读模式,此时的 limit 等于 Buffer 中实际的数据大小,因为 Buffer 不一定被写满了。

其实很好理解了,buffer初始化必须指定一个容量,之后不能变,他分为读模式和写模式,两个模式下的cappacity含义一样,position分别为读到的位子和写到的位子,limit分别表示读模式下的最大读取位子和写模式下的最大能写的位子。

初始化:

		ByteBuffer bf = ByteBuffer.allocate(1024);
		ByteBuffer bf1 = ByteBuffer.wrap(new byte[6]);

查看源码,两个方法最终都会做这个操作:new HeapByteBuffer(capacity, capacity),不过最终实现还是在ByteBuffer里面。

填充:

// 填充一个 byte 值
public abstract ByteBuffer put(byte b);
// 在指定位置填充一个 int 值
public abstract ByteBuffer put(int index, byte b);
// 将一个数组中的值填充进去
public final ByteBuffer put(byte[] src) {...}

public ByteBuffer put(byte[] src, int offset, int length) {...}

上述这些方法需要自己控制 Buffer 大小,不能超过 capacity,超过会抛 java.nio.BufferOverflowException 异常。

对于 Buffer 来说,另一个常见的操作中就是,我们要将来自 Channel 的数据填充到 Buffer 中,在系统层面上,这个操作我们称为读操作,因为数据是从外部(文件或网络等)读到内存中。

 int num = channel.read(buf);

读取:

前面介绍了写操作,每写入一个值,position 的值都需要加 1,所以 position 最后会指向最后一次写入的位置的后面一个,如果 Buffer 写满了,那么 position 等于 capacity(position 从 0 开始)。
如果要读 Buffer 中的值,需要切换模式,从写入模式切换到读出模式。注意,通常在说 NIO 的读操作的时候,我们说的是从 Channel 中读数据到 Buffer 中,对应的是对 Buffer 的写入操作,初学者需要理清楚这个。
调用 Buffer 的 flip() 方法,可以进行模式切换。其实这个方法也就是设置了一下 position 和 limit 值罢了。

public final Buffer flip() {
	limit = position;
	position = 0;
	mark = -1;
	return this;
    }
// 根据 position 来获取数据
public abstract byte get();
// 获取指定位置的数据
public abstract byte get(int index);
// 将 Buffer 中的数据写入到数组中
public ByteBuffer get(byte[] dst)

类似写方法,读方法常用的也是这个

int num = channel.write(buf);

mark() & reset()

    public final Buffer mark() {
	mark = position;
	return this;
    }
    public final Buffer reset() {
        int m = mark;
	if (m < 0)
	    throw new InvalidMarkException();
	position = m;
	return this;
    }
mark 用于临时保存 position 的值,每次调用 mark() 方法都会将 mark 设值为当前的 position,便于后续需要的时候使用。

那到底什么时候用呢?考虑以下场景,我们在 position 为 5 的时候,先 mark() 一下,然后继续往下读,读到第 10 的时候,我想重新回到 position 为 5 的地方重新来一遍,那只要调一下 reset() 方法,position 就回到 5 了。

rewind() & clear() & compact()

   public final Buffer rewind() {
	position = 0;
	mark = -1;
	return this;
    }
   public final Buffer clear() {
	position = 0;
	limit = capacity;
	mark = -1;
	return this;
    }
   rewind():会重置 position 为 0,通常用于重新从头读写 Buffer。
   clear():有点重置 Buffer 的意思,相当于重新实例化了一样。
   通常,我们会先填充 Buffer,然后从 Buffer 读取数据,之后我们再重新往里填充新的数据,我们一般在重新填充之前先调用 clear()。
   compact():和 clear() 一样的是,它们都是在准备往 Buffer 填充新的数据之前调用。
   前面说的 clear() 方法会重置几个属性,但是我们要看到,clear() 方法并不会将 Buffer 中的数据清空,只不过后续的写入会覆盖掉原来的数据,也就相当于清空了数据了。
而 compact() 方法有点不一样,调用这个方法以后,会先处理还没有读取的数据,也就是 position 到 limit 之间的数据(还没有读过的数据),先将这些数据移到左边,然后在这个基础上再开始写入。很明显,此时 limit 还是等于 capacity,position 指向原来数据的右边。

 

Channel

用于 I/O 操作的连接。


FileChannel:文件通道,用于文件的读和写
DatagramChannel:用于 UDP 连接的接收和发送
SocketChannel:把它理解为 TCP 连接通道,简单理解就是 TCP 客户端
ServerSocketChannel:TCP 对应的服务端,用于监听某个端口进来的请求

Channel 经常翻译为通道,类似 IO 中的流,用于读取和写入。它与前面介绍的 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。


FileChannel

文件复制demo
	private static void copyFileUsingFileChannels() throws Exception {
		// 文件复制,d预先不存在,所以new了下
		File file1 = new File("d:c.jpg");
		File file2 = new File("d:d.jpg");
		file2.createNewFile();
		FileChannel inputChannel = null;
		FileChannel outputChannel = null;
		try {
			inputChannel = new FileInputStream(file1).getChannel();
			outputChannel = new FileOutputStream(file2).getChannel();
			outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
		} finally {
			inputChannel.close();
			outputChannel.close();
		}
		System.out.println("copy over!");
	}

SocketChannel和ServerSocketChannel

简易io服务

public class IDServerDemo {
	public static void main(String[] args) throws IOException {
		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
		// 监听 8080 端口进来的 TCP 链接
		serverSocketChannel.socket().bind(new InetSocketAddress(8888));
		while (true) {
			// 这里会阻塞,直到有一个请求的连接进来
			SocketChannel socketChannel = serverSocketChannel.accept();
			// 开启一个新的线程来处理这个请求,然后在 while 循环中继续监听 8080 端口
			IOServerHandler handler = new IOServerHandler(socketChannel);
			new Thread(handler).start();
		}
	}
}
public class IOServerHandler implements Runnable {
	private SocketChannel socketChannel;

	public IOServerHandler(SocketChannel socketChannel) {
		this.socketChannel = socketChannel;
	}

	@Override
	public void run() {
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		try {
			// 将请求数据读入 Buffer 中
			int num;
			while ((num = socketChannel.read(buffer)) > 0) {
				// 读取 Buffer 内容之前先 flip 一下
				buffer.flip();
				// 提取 Buffer 中的数据
				byte[] bytes = new byte[num];
				buffer.get(bytes);
				String re = new String(bytes, "UTF-8");
				System.out.println("收到请求:" + re);
				// 回应客户端
				ByteBuffer writeBuffer = ByteBuffer.wrap(("我已经收到你的请求,你的请求内容是:" + re).getBytes());
				socketChannel.write(writeBuffer);
				buffer.flip();
			}
		} catch (IOException e) {
			try {
				socketChannel.close();
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		}
	}
}
public class IOClientDemo {
	public static void main(String[] args) throws IOException {
		SocketChannel socketChannel = SocketChannel.open();
		socketChannel.connect(new InetSocketAddress("localhost", 8888));
		// 发送请求
		ByteBuffer buffer = ByteBuffer.wrap("1234567890".getBytes());
		socketChannel.write(buffer);
		// 读取响应
		ByteBuffer readBuffer = ByteBuffer.allocate(1024);
		int num;
		if ((num = socketChannel.read(readBuffer)) > 0) {
			readBuffer.flip();
			byte[] re = new byte[num];
			readBuffer.get(re);
			String result = new String(re, "UTF-8");
			System.out.println("返回值: " + result);
		}
	}
}

DatagramChannel

UDP 是面向无连接的,不需要和对方握手,不需要通知对方,就可以直接将数据包投出去,至于能不能送达,它是不知道的

网上搜索了一个例子:

public class UdpServer {
	public static final int PORT = 30000;
	// 定义每个数据报的最大大小为4KB
	private static final int DATA_LEN = 4096;
	// 定义接收网络数据的字节数组
	byte[] inBuff = new byte[DATA_LEN];
	// 以指定字节数组创建准备接收数据的DatagramPacket对象
	private DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
	// 定义一个用于发送的DatagramPacket对象
	private DatagramPacket outPacket;

	public void init() throws IOException {
		try {
			// 创建DatagramSocket对象
			DatagramSocket socket = new DatagramSocket(PORT);
			// 采用循环接收数据
			while (true) {
				// 读取Socket中的数据,读到的数据放入inPacket封装的数组里
				socket.receive(inPacket);
				// 判断inPacket.getData()和inBuff是否是同一个数组
				System.out.println(inBuff == inPacket.getData());
				// 将接收到的内容转换成字符串后输出
				System.out.println(new String(inBuff, 0, inPacket.getLength()));
				byte[] sendData = "测试返回".getBytes();
				// 以指定的字节数组作为发送数据,以刚接收到的DatagramPacket的
				// 源SocketAddress作为目标SocketAddress创建DatagramPacket
				outPacket = new DatagramPacket(sendData, sendData.length, inPacket.getSocketAddress());
				// 发送数据
				socket.send(outPacket);
			}
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
	}

	public static void main(String[] args) throws IOException {
		new UdpServer().init();
	}
}
public class UdpClient {
	// 定义发送数据报的目的地
	public static final int DEST_PORT = 30000;
	public static final String DEST_IP = "127.0.0.1";
	// 定义每个数据报的最大大小为4KB
	private static final int DATA_LEN = 4096;
	// 定义接收网络数据的字节数组
	byte[] inBuff = new byte[DATA_LEN];
	// 以指定的字节数组创建准备接收数据的DatagramPacket对象
	private DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
	// 定义一个用于发送的DatagramPacket对象
	private DatagramPacket outPacket = null;

	public void init() throws IOException {
		try {
			// 创建一个客户端DatagramSocket,使用随机端口
			DatagramSocket socket = new DatagramSocket();
			// 初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
			outPacket = new DatagramPacket(new byte[0], 0, InetAddress.getByName(DEST_IP), DEST_PORT);
			// 创建键盘输入流
			Scanner scan = new Scanner(System.in);
			// 不断地读取键盘输入
			while (scan.hasNextLine()) {
				// 将键盘输入的一行字符串转换成字节数组
				byte[] buff = scan.nextLine().getBytes();
				// 设置发送用的DatagramPacket中的字节数据
				outPacket.setData(buff);
				// 发送数据报
				socket.send(outPacket);
				// 读取Socket中的数据,读到的数据放在inPacket所封装的字节数组中
				socket.receive(inPacket);
				System.out.println(new String(inBuff, 0, inPacket.getLength()));
			}
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
	}

	public static void main(String[] args) throws IOException {
		new UdpClient().init();
	}

}
仔细研究会发现,udp传输协议不像tcp一样通道有两个Socket组成,都是一样的,DatagramSocket和DatagramPacket两个类搞定。


Selector

Selector 建立在非阻塞的基础之上,大家经常听到的 多路复用 在 Java 世界中指的就是它,用于实现一个线程管理多个 Channel。
public static void main(String[] args) throws IOException {
		Selector selector = Selector.open();
		ServerSocketChannel server = ServerSocketChannel.open();
		server.socket().bind(new InetSocketAddress(8888));
		// 将其注册到 Selector 中,监听 OP_ACCEPT 事件
		server.configureBlocking(false);
		server.register(selector, SelectionKey.OP_ACCEPT);
		while (true) {
			// 需要不断地去调用 select() 方法获取最新的准备好的通道
			int readyChannels = selector.select();
			if (readyChannels == 0) {
				continue;
			}
			Set<SelectionKey> readyKeys = selector.selectedKeys();
			// 遍历
			Iterator<SelectionKey> iterator = readyKeys.iterator();
			while (iterator.hasNext()) {
				SelectionKey key = iterator.next();
				iterator.remove();
				if (key.isAcceptable()) {
					// 有已经接受的新的到服务端的连接
					SocketChannel socketChannel = server.accept();

					// 有新的连接并不代表这个通道就有数据,
					// 这里将这个新的 SocketChannel 注册到 Selector,监听 OP_READ 事件,等待数据
					socketChannel.configureBlocking(false);
					socketChannel.register(selector, SelectionKey.OP_READ);
				} else if (key.isReadable()) {
					// 有数据可读
					// 上面一个 if 分支中注册了监听 OP_READ 事件的 SocketChannel
					SocketChannel socketChannel = (SocketChannel) key.channel();
					ByteBuffer readBuffer = ByteBuffer.allocate(1024);
					int num = socketChannel.read(readBuffer);
					if (num > 0) {
						// 处理进来的数据...
						System.out.println("收到数据:" + new String(readBuffer.array()).trim());
						socketChannel.register(selector, SelectionKey.OP_WRITE);
					} else if (num == -1) {
						// -1 代表连接已经关闭
						socketChannel.close();
					}
				} else if (key.isWritable()) {
					// 通道可写
					// 给用户返回数据的通道可以进行写操作了
					SocketChannel socketChannel = (SocketChannel) key.channel();
					ByteBuffer buffer = ByteBuffer.wrap("返回给客户端的数据...".getBytes());
					socketChannel.write(buffer);

					// 重新注册这个通道,监听 OP_READ 事件,客户端还可以继续发送内容过来
					socketChannel.register(selector, SelectionKey.OP_READ);
				}
			}
		}
	}
首先,我们开启一个 Selector。你们爱翻译成选择器也好,多路复用器也好。
将 Channel 注册到 Selector 上。前面我们说了,Selector 建立在非阻塞模式之上,所以注册到 Selector 的 Channel 必须要支持非阻塞模式,FileChannel 不支持非阻塞,我们这里讨论最常见的 SocketChannel 和 ServerSocketChannel。
register 方法的第二个 int 型参数(使用二进制的标记位)用于表明需要监听哪些感兴趣的事件,共以下四种事件:
SelectionKey.OP_READ
对应 00000001,通道中有数据可以进行读取   1<<0
SelectionKey.OP_WRITE
对应 00000100,可以往通道中写入数据 1<<2
SelectionKey.OP_CONNECT
对应 00001000,成功建立 TCP 连接  1<<3
SelectionKey.OP_ACCEPT
对应 00010000,接受 TCP 连接  1<<4
我们可以同时监听一个 Channel 中的发生的多个事件,比如我们要监听 ACCEPT 和 READ 事件,那么指定参数为二进制的 00010001 即十进制数值 17 即可。
注册方法返回值是 SelectionKey 实例,它包含了 Channel 和 Selector 信息,也包括了一个叫做 Interest Set 的信息,即我们设置的我们感兴趣的正在监听的事件集合。
调用 select() 方法获取通道信息。用于判断是否有我们感兴趣的事件已经发生了。
  






















没有更多推荐了,返回首页