NIO系列二:channel| 8月更文挑战

1,239 阅读5分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

channel简介

NIO中Selector是中央控制器,Buffer是承载数据的容器,而Channel是通道。channel是打开到IO设备的连接。类似流,但又有些不同:它既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。 通道可以异步地读写。通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

NIO中通过channel封装了对数据源的操作,通过 channel 我们可以操作数据源,但又不必关心数据源的具体物理结构。这个数据源可能是多种的。比如,可以是文件,也可以是网络socket。在大多数应用中,channel 与文件描述符或者socket 是一一对应的。

channel分类

Java IO可以分为文件类和Stream类两大类。Channel 也相应地分为了FileChannel 和 Socket Channel,其中 socket channel 又分为三大类,一个是用于监听端口的ServerSocketChannel,第二类是用于TCP通信的SocketChannel,第三类是用于UDP通信的DatagramChannel。

Channel的类别与应用

FileChannel: 文件的数据读写;

DatagramChannel: UDP的数据读写;

SocketChannel: TCP的数据读写,一般是客户端实现;

ServerSocketChannel: 可以监听TCP链接请求,每个请求会创建会一个SocketChannel,一般是服务器实现。

FileChannel

使用FileChannel读取数据到Buffer(缓冲区)以及利用Buffer(缓冲区)写入数据到FileChannel。

FileChannel是抽象类,无法直接打开FileChannel,需要通过InputStream、OutputStream或RandomAccessFile获取FileChannel。然后从FileChannel读取/写入数据, 再关闭FileChannel。

public class FileChannel {
	public static void main(String[] args) throws IOException {
		//创建一个RandomAccessFile对象
		RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\user.txt", "rw");
		java.nio.channels.FileChannel fileChannel = randomAccessFile.getChannel();
		//创建一个读数据缓冲区对象
		ByteBuffer byteBufferRead = ByteBuffer.allocate(48);
		//从通道中读取数据
		int bytesRead = fileChannel.read(byteBufferRead);
		//创建一个写数据缓冲区对象
		ByteBuffer byteBufferWrite = ByteBuffer.allocate(48);
		//写数据
		byteBufferWrite.put("FileChannel test".getBytes());
		byteBufferWrite.flip();
		fileChannel.write(byteBufferWrite);
		while( bytesRead != -1) {
			System.out.println("Read "+bytesRead);
			//Buffer在写模式下调用flip()之后,Buffer从写模式变成读模式
			byteBufferRead.flip();
			//读取buffer中的数据
			while (byteBufferRead.hasRemaining()) {
				System.out.print((char) byteBufferRead.get());
			}
			//清空缓冲区(切换到写模式)
			byteBufferRead.clear();
			bytesRead = fileChannel.read(byteBufferRead);
		}
		//关闭RandomAccessFile对象
		randomAccessFile.close();
	}
}

SocketChannel和ServerSocketChannel的使用

服务器端

1.通过ServerSocketChannel 绑定ip地址和端口号;

2.通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据;

3.创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据;

  1. 关闭SocketChannel和ServerSocketChannel;
public class Server {
	public static void main(String[] args) {
		try {
			Selector selector = Selector.open();

			ServerSocketChannel serverSocketChannelOne = ServerSocketChannel.open();
			serverSocketChannelOne.socket().bind(new InetSocketAddress("127.0.0.1",8080));
			serverSocketChannelOne.configureBlocking(false);
			//注册channel并指定监听的事件
			serverSocketChannelOne.register(selector, SelectionKey.OP_ACCEPT);

			ServerSocketChannel serverSocketChannelTwo = ServerSocketChannel.open();
			serverSocketChannelTwo.socket().bind(new InetSocketAddress("127.0.0.1",8090));
			serverSocketChannelTwo.configureBlocking(false);
			//注册channel并指定监听的事件
			serverSocketChannelTwo.register(selector, SelectionKey.OP_ACCEPT);

			ByteBuffer readBuff = ByteBuffer.allocate(1024);
			ByteBuffer writeBuff = ByteBuffer.allocate(1024);
			writeBuff.put("received".getBytes());
			writeBuff.flip();

			while (true) {
				int nReady = selector.select();
				Set<SelectionKey> keys = selector.selectedKeys();
				Iterator<SelectionKey> iterator = keys.iterator();

				while (iterator.hasNext()) {
					SelectionKey selectionKey = iterator.next();
					iterator.remove();

					if (selectionKey.isConnectable()) {
						System.out.println(Thread.currentThread().getId()+"start Connectable....");
					} else if (selectionKey.isAcceptable()) {
                        System.out.println(Thread.currentThread().getId()+"start Acceptable....");
						ServerSocketChannel serverSocketChannel = (ServerSocketChannel)selectionKey.channel();
						SocketChannel socketChannel = serverSocketChannel.accept();
						socketChannel.configureBlocking(false);
						socketChannel.register(selector, SelectionKey.OP_READ);
					}else if (selectionKey.isReadable()) {
						SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
						readBuff.clear();
						socketChannel.read(readBuff);

						readBuff.flip();
						System.out.println(Thread.currentThread().getId()+"-received:"+ new String(readBuff.array()));
						selectionKey.interestOps(SelectionKey.OP_WRITE);
					} else if (selectionKey.isWritable()) {
						writeBuff.rewind();
						SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
						socketChannel.write(writeBuff);
						selectionKey.interestOps(SelectionKey.OP_READ);
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

客户端

1.通过SocketChannel连接到远程服务器;

2.创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据;

3.关闭SocketChannel;

public class Client {
	public static void main(String[] args) throws Exception{
		SocketChannel socketChannel = SocketChannel.open();
		socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));

		ByteBuffer writeBuffer = ByteBuffer.allocate(32);
		ByteBuffer readBuffer = ByteBuffer.allocate(32);

		writeBuffer.put("hello".getBytes());
		writeBuffer.flip();
		while (true) {
			writeBuffer.rewind();
			socketChannel.write(writeBuffer);
			readBuffer.clear();
			socketChannel.read(readBuffer);
		}
	}
}

DatagramChannel使用

public class Server {
	public static void main(String[] args) throws IOException {
		Selector selector = Selector.open();
		DatagramChannel channel= DatagramChannel.open();
		channel.configureBlocking(false);
		channel.socket().bind(new InetSocketAddress(8080));
		channel.register(selector, SelectionKey.OP_READ);

		ByteBuffer byteBuffer = ByteBuffer.allocate(65535);
		while (true) {
			int n = selector.select();
			if (n > 0) {
				Iterator iterator = selector.selectedKeys().iterator();
				while (iterator.hasNext()) {
					SelectionKey selectionKey = (SelectionKey) iterator.next();
					//必须手动删除
					iterator.remove();
					if (selectionKey.isReadable()) {
						DatagramChannel datagramChannel = (DatagramChannel) selectionKey.channel();
						byteBuffer.clear();
						//读取数据
						InetSocketAddress inetSocketAddress = (InetSocketAddress)datagramChannel
								.receive(byteBuffer);
						System.out.println(new java.lang.String(byteBuffer.array()));

						//删除缓冲区的数据
						byteBuffer.clear();
						java.lang.String message = "data come from server";
						byteBuffer.put(message.getBytes());
						byteBuffer.flip();

						//发送数据
						datagramChannel.send(byteBuffer, inetSocketAddress);
					}
				}

			}
		}
	}
}

客户端

public class Client {
	public static void main(String[] args) throws IOException {
		Selector selector = Selector.open();
		DatagramChannel datagramChannel = DatagramChannel.open();
		datagramChannel.configureBlocking(false);
		SocketAddress socketAddress = new InetSocketAddress("localhost", 8080);
		datagramChannel.connect(socketAddress);

		datagramChannel.register(selector, SelectionKey.OP_READ);
		datagramChannel.write(Charset.defaultCharset().encode("data come from client123!"));
		ByteBuffer byteBuffer = ByteBuffer.allocate(100);

		while (true) {
			int n = selector.select();
			if (n > 0) {
				Iterator iterator = selector.selectedKeys().iterator();
				while (iterator.hasNext()) {
					SelectionKey selectionKey = (SelectionKey) iterator.next();
					iterator.remove();
					if (selectionKey.isReadable()) {
						datagramChannel = (DatagramChannel) selectionKey.channel();
						datagramChannel.read(byteBuffer);
						System.out.println(new String(byteBuffer.array()));

						byteBuffer.clear();
						datagramChannel.write(Charset.defaultCharset()
								.encode("data come from client456!"));
					}
				}
			}
		}
	}
}

Scatter / Gather

聚集写入( Gathering Writes)是指将多个 Buffer 中的数据“聚集”到 Channel。(照缓冲区的顺序,写入 position 和 limit 之间的数据到 Channel )

分散读取( Scattering Reads)是指从 Channel 中读取的数据“分散” 到多个 Buffer 中。(按照缓冲区的顺序,从 Channel 中读取的数据依次将 Buffer 填满)

Scattering Reads “scattering read”是把数据从单个Channel写入到多个buffer,如下图所示:

channel.PNG

Gathering Writes “gathering write”把多个buffer的数据写入到同一个channel中,下面是示意图:

channel2.PNG

接口定义如下


public interface ScatteringByteChannel extends ReadableByteChannel
{
    public long read(ByteBuffer[] dsts) throws IOException;
 
    public long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
}
 
public interface GatheringByteChannel extends WritableByteChannel
{
    public long write(ByteBuffer[] srcs) throws IOException;
 
    public long write(ByteBuffer[] srcs, int offset, int length) throws IOException;

}

接口中定义的方法都传入了一个Buffer数组。 scatter/gather操作就是聚集(gather)这个Buffer数组并写入到一个通道,或读取通道数据并分散(scatter)到这个Buffer数组中。带offset和length参数的read()和write()方法可以让我们只使用缓冲区数组的子集(offset是缓冲区数组索引,不是Buffer数据的索引,length指的是要使用的缓冲区数量)。

通道之间的数据传输

如果一个channel是FileChannel类型的,那么可以直接把数据传输到另一个channel,方法如下:

  • transferFrom() :transferFrom方法把数据从通道源传输到FileChannel;
  • transferTo() :transferTo方法把FileChannel数据传输到另一个channel;

transferFrom

从源信道读取字节到这个通道的文件中。受限于count,源通道的剩余空间小于 count 个字节时,则所传输的字节数要小于请求的字节数。

    public void transferFromTest() throws Exception{
        FileChannel inChannel = FileChannel.open(Paths.get("D:\\From01.jpg"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("D:\\From2.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
        outChannel.transferFrom(inChannel,0, inChannel.size()); 
        inChannel.close();
        outChannel.close();
    }

transferTo

将字节从这个通道的文件传输到给定的可写字节通道。

    public void transferToTest() throws Exception{
        FileChannel inChannel = FileChannel.open(Paths.get("D:\\to01.jpg"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("D:\\to02.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
        inChannel.transferTo(0, inChannel.size(), outChannel);  
        inChannel.close();
        outChannel.close();
    }
    }