《Java NIO》学习笔记三 通道(Channel)

时间:2022-09-05 14:51:33

一、通道基础

有两种类型的通道:它们是文件(file)通道和套接字(socket)通道。

具体细分,有一个FileChannel类和三个socket通道类:SocketChannelServerSocketChannel和 DatagramChannel

 

在通道之间复制数据:

第一种方法:个人更推崇第一种,因为代码更清晰、更简洁

	private static void channelCopy2(ReadableByteChannel src,WritableByteChannel dest) throws IOException {
		ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
		while (src.read(buffer) != -1) {
			buffer.flip(); // 翻转缓冲区,转换成读模式,为读取缓冲区数据做准备
			//这个while循环中会获取当前填充到缓冲区的数据,写到通道中
			while (buffer.hasRemaining()) {
				dest.write(buffer);
			}
			buffer.clear();// 清空缓冲区,为填充作准备
		}
	}

第二种方法:

	private static void channelCopy1(ReadableByteChannel src,WritableByteChannel dest) throws IOException {
		ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
		while (src.read(buffer) != -1) {
			buffer.flip(); // 翻转缓冲区,转换成读模式,为读取缓冲区数据做准备
			dest.write(buffer); //获取缓冲区数据,将缓冲区数据写到通道中
			//因为收上面的额代码只会将当前缓冲区的部分数据传输到通道中,因此需要用压缩方法
			//如果缓冲区是空的,它的功能和clear()方法一样。
			buffer.compact();
		}
		//因为上面用的是compact()压缩方法,当通道中数据传输到缓冲区传输完之后,再进行获取缓冲区数据传输到
		//另一个通道,然后调用compact()方法,有可能缓冲区会有剩余数据没有传输完
		buffer.flip();
		while (buffer.hasRemaining()) {
			dest.write(buffer);
		} 
	}

二、ScatterGather

Scatter/Gather是指在多个缓冲区上实现一个简单的I/O操作。

对于一个write操作而言,数据是从几个缓冲区按顺序抽取(称为gather)并沿着通道发送的。该gather过程的效果就好比全部缓冲区的内容被连结起来,并在发送数据前存放到一个大的缓冲区中。

对于read操作而言,从通道读取的数据会按顺序被散布(称为scatter)到多个缓冲区,将每个缓冲区填满直至通道中的数据或者缓冲区的最大空间被消耗完。

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;
}

Scatter/Gather应该使用直接的ByteBuffers以从本地I/O获取最大性能优势。

使用示例:

1、将通道中的数据按顺序散步到多个缓冲区中:

ByteBuffer header = ByteBuffer.allocateDirect (10);

ByteBuffer body = ByteBuffer.allocateDirect (80);

ByteBuffer [] buffers = { header, body };

int bytesRead = channel.read (buffers);

一旦read( )方法返回,bytesRead就被赋予值48header缓冲区将包含前10个从通道读取的字节而body缓冲区则包含接下来的38个字节。

2、使用gather操作将多个缓冲区的数据组合并发送到通道中:

long bytesWritten = channel.write (buffers);

 

三、文件通道:

FileChannel对象不能直接创建。一个FileChannel实例只能通过在一个打开的file对象(RandomAccessFileFileInputStream或 FileOutputStream)上调用getChannel( )方法获取。调用getChannel( )方法会返回一个连接到相同文件的FileChannel对象且该FileChannel对象具有与file对象相同的访问权限,然后就可以使用该通道对象来利用强大的FileChannel API了。

 

Channel-to-Channel传输:直接的通道传输,不借助缓冲区

功能:将文件数据批量的从一个位置传输到另一个位置。

FileChannel类添加了这些优化方法来提高该传输过程的效率:

public abstract long transferTo (long position, long count,WritableByteChannel target)

public abstract long transferFrom (ReadableByteChannel src,long position, long count)

transferTo( )transferFrom( )方法允许将一个通道交叉连接到另一个通道,而不需要通过一个中间缓冲区来传递数据。

只有FileChannel类有这两个方法,因此channel-to-channel传输中通道之一必须是FileChannel

直接的通道传输不会更新与某个FileChannel关联的position值。请求的数据传输将从position参数指定的位置开始,传输的字节数不超过count参数的值。实际传输的字节数会由方法返回,可能少于您请求的字节数。

使用示例:

	private static void catFiles(WritableByteChannel target, String[] files)throws Exception {
		for (int i = 0; i < files.length; i++) {
			FileInputStream fis = new FileInputStream(files[i]);
			FileChannel channel = fis.getChannel();
			channel.transferTo(0, channel.size(), target);
			channel.close();
			fis.close();
		}
	}


四、Socket通道:

DatagramChannelSocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。

ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,它本身从不传输数据。 

 

1、非阻塞模式:

关于通道的非阻塞模式的方法:

//设置通道的阻塞模式,true为阻塞模式,false为非阻塞模式

public abstract void configureBlocking (boolean block)throws IOException;

//调用isBlocking( )方法来判断某个socket通道当前处于哪种模式

public abstract boolean isBlocking( );

public abstract Object blockingLock( );

 

2SocketChannel:

1int bytesReaded=socketChannel.read(buffer);        //从套接字通道(信道)读取数据

执行以上方法后,通道会从socket读取的数据填充此缓冲区,它返回成功读取并存储在缓冲区的字节数.在默认情况下,这至少会读取一个字节,或者返回-1指示数据结束.

2socketChannel.write(buffer); //向套接字通道(信道)写入数据。此方法以一个ByteBuffer为参数,将该缓冲区中剩余的字节写入信道.

3、SocketChannel clntChan = SocketChannel.open();   //打开套接字通道。

4、connect(SocketAddress remote)                  //连接此通道的套接字。

5、finishConnect()                              //完成套接字通道的连接过程。

6、clntChan.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize));

//将选择器注册到连接到的客户端信道,并指定该信道key值的属性为OP_READ,同时为该信道指定关联的附件 


nio 远程主机强迫关闭了一个现有的连接解决方案:
原因:客户端自己关闭了连接(没有调用SocketChannel的close方法),服务器还在read事件中,这个时候读取客户端的时候会报错。
解决办法:
在客户端合适的时候,调用SocketChannel的close方法.

 

3ServerSocketChannel:

open()    打开服务器套接字通道。

socket()   获取与此通道关联的服务器套接字。

accept()   接受到此通道套接字的连接。 //此方法返回的是客户端的SocketChannel实例

listnChannel.register(selector, SelectionKey.OP_ACCEPT);    //将选择器注册到各个信道

 

由于ServerSocketChannel没有bind( )方法,因此有必要取出对等的socket并使用它来绑定到一个端口以开始监听连接。

ServerSocketChannel ssc = ServerSocketChannel.open( );

ServerSocket serverSocket = ssc.socket( );

serverSocket.bind (new InetSocketAddress (1234));  // Listen on port 1234

 

一旦您创建了一个ServerSocketChannel并用对等socket绑定了它,然后您就可以在其中一个上调用accept( )。调用accept( )方法则会返回SocketChannel类型的对象,返回的对象能够在非阻塞模式下运行。

 

使用ServerSocketChannel的非阻塞accept( )方法:

	public static void main(String[] argv) throws Exception {
		int port = 1234; 
		if (argv.length > 0) {
			port = Integer.parseInt(argv[0]);
		}
		ByteBuffer buffer = ByteBuffer.wrap(GREETING.getBytes());
		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.socket().bind(new InetSocketAddress(port));
		ssc.configureBlocking(false);
		while (true) {
			System.out.println("Waiting for connections");
			SocketChannel sc = ssc.accept();
			if (sc == null) {
				Thread.sleep(2000);  // no connections, snooze a while
			} else {
				System.out.println("Incoming connection from: "+ sc.socket().getRemoteSocketAddress());
				buffer.rewind();
				sc.write(buffer);
				sc.close();
			}
		}
	}