Java NIO系列知识(三) Channel

假如我们把NIO比作整个铁路系统,Channel(通道)就是整个系统中的轨道,作为NIO的核心组件之一,其承担着传输数据的作用。和标准IO相比,我们用stream(流)来传输数据,两者的区别在于Channel是双向的,而stream是单向的。另外,可直接向stream写入数据或从中读取数据,而Channel却不能,它需要和Buffer配合使用,就像乘客不能直接在轨道上传输,需要坐在火车上(这里的火车就相当于Buffer)进行传输。

Channel的分类

java.nio.*为Channel提供了很多的实现类,这里主要介绍下面四个实现类。
在这里插入图片描述

  • FileChannel:从文件中读取数据
  • DatagramChannel:通过UDP读写网络中的数据
  • SocketChannel:通过TCP读写网络中的数据
  • ServerSocketChannel:用于监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel
Channel的基本用法

使用Channel时,要与上一篇介绍的 Buffer配合使用。网上有很多文章有这种类似的说法:

  • Channel中的数据写入Buffer或者将Buffer中的数据读取到Channel

  • 您永远不会将字节直接写入通道中,也不会直接从通道中读取字节

这两个说法感觉有点矛盾,既然通道里面不能写入数据,何来将通道中的数据写入缓冲区呢?所以这里我按照自己的理解来叙述。

  • 写操作时,我们将 Buffer 中的数据写入到 Channel 连接的数据目的地(Channel中并没有直接存储数据)

在这里插入图片描述

  • 读操作时,我们将Channel连接的数据源中的数据填充到 Buffer 中(Channel中并没有直接存储数据)。

在这里插入图片描述

FileChannel的使用

FileChannel是一个用于读取,写入,映射和操作文件的通道,它本身是阻塞式的,不能设置为非阻塞状态,也因此它是线程安全的。下面就用代码简单的演示通过FileChannel对文件的读写。

  1. 读取文件:从指定文件channel.txt中读取数据到缓冲区
package cn.lincain.filechannel.read;

import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelDemo {
	public static void main(String[] args) throws Exception {
        // 通过RandomAccessFile获取文件通道,这里也可以通过FileInputStream获取
		RandomAccessFile file = 
				new RandomAccessFile("channel.txt", "rw");
		FileChannel channel = file.getChannel();
        
        //  创建缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(1024);
        
        // 通过channel将数据源的数据读入缓冲区
		channel.read(buffer);
        
        // 切换状态,从缓冲区中读取数据
		buffer.flip();
		while(buffer.hasRemaining()) {
			System.out.print((char)buffer.get());
		}
		file.close();
	}
}

运行结果:

读取的字节数:135
--------------文件内容--------------
package cn.lincain.filechannel;

import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
  1. 写入文件:将缓冲区中的数据写入指定的文件channel.txt
package cn.lincain.filechannel.write;

import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelDemo {
public static void main(String[] args) throws Exception {
		// 通过RandomAccessFile获取文件通道,这里也可以通过FileOutputStream获取
		RandomAccessFile file = 
				new RandomAccessFile("channel.txt", "rw");
		FileChannel channel = file.getChannel();
    
    	// 创建缓冲区,并向其中写入数据
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		String context = "package cn.lincain.arts.week3;\r\n" + 
				"\r\n" + 
				"import java.io.RandomAccessFile;\r\n" + 
				"import java.nio.ByteBuffer;\r\n" + 
				"import java.nio.channels.FileChannel;";
		buffer.put(context.getBytes());
    
    	// 切换状态,并将Buffer中的数据通过Channel写入数据目的地
		buffer.flip();
		channel.write(buffer);
		file.close();
	}
}

执行上述代码后,会在项目classpath路径下产生channel.txt,其中的内容即为代码中的字符串。
这里可以简单总结一下FileChannel的使用步骤:

  1. 调用RandomAccessFile(InputStream、OutputStream也可以)的getChannel()开启FileChannel通道
  2. 创建ByteBuffer对象用于从通道读取数据,或者向通道写入数据
  1. 读写结合:将channel.txt中的数据拷贝到channel-copy.txt
package cn.lincain.filechannel.copy;

import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelDemo {
	public static void main(String[] args) throws Exception {
        // 通过RandomAccessFile获取相应的通道
		RandomAccessFile sourFile = new RandomAccessFile("channel.txt", "rw");
		RandomAccessFile destFile = new RandomAccessFile("channel-copy.txt", "rw");
		
		FileChannel sourChannel = sourFile.getChannel();
		FileChannel destChannel = destFile.getChannel();
		
		// 创建缓冲区,并clear()
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		buffer.clear();
		
		// 通过sourChannel将channel.txt中的数据读取到buffer中,
		while(sourChannel.read(buffer) != -1) {
            // 切换状态
			buffer.flip();
            // 然后通过destChannel将buffer中数据写入到channel-copy.txt中
			while(buffer.hasRemaining()) {
				destChannel.write(buffer);
			}
			buffer.clear();
		}
		
		// 关闭资源	
		sourFile.close();
		destFile.close();
    }
}

执行上述代码后,会在项目classpath路径下产生channel-copy.txt,其中的内容和channel.txt一致,即完成了复制操作。

ServerSocketChannelSocketChannel的使用

java api上可以看到ServerSocketChannelSocketChannel分别是针对面向流的侦听套接字的可选择通道和针对面向流的连接套接字的可选择通道。和ServerSocketSocket类似的,后者可通过相应方法进行数据的传输,前者只是负责监听传入的SocketChannel,而不能传输数据。

ServerSocketChannel通过accept()创建SocketChannel对象从客户端读写数据,SocketChannel通过connect()连接服务器向其读写数据。下面用代码对两者的使用进行简单的演示。

  • 服务端代码:
package cn.lincain.nio.socket;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class ChannelServer {
	private static final int PORT = 20000;
	private static final int SLEEP_TIME = 5;

	public static void main(String[] args) throws Exception {
		// 创建ServerSocketChannel对象,设置端口号,并设置为非阻塞状态
		ServerSocketChannel ssChannel = ServerSocketChannel.open();
		ssChannel.socket().bind(new InetSocketAddress(PORT));
		ssChannel.configureBlocking(false)// 创建缓冲区用于存储数据
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		SocketChannel sChannel = null;
        // 创建集合用于存储SocketChannel
		List<SocketChannel> channels = new ArrayList<SocketChannel>();
        
		for (;;) {
            // 尝试获取socket连接通道
			sChannel = ssChannel.accept();
			if (sChannel != null) {
                // 如果连接成功,则将该通道加入集合中
				channels.add(sChannel);
			} else {
                // 如果连接不成功,休息5秒后,继续尝试连接
				TimeUnit.SECONDS.sleep(SLEEP_TIME);
				sChannel = ssChannel.accept();
				if (sChannel != null)
                    // 如果连接成功,则将该通道加入集合中
					channels.add(sChannel);
			}
            // 如果集合不为空,遍历集合,将每个通道的数据进行打印
			if (!channels.isEmpty()) {
				for (SocketChannel socketChannel : channels) {
					String address = socketChannel.socket().getRemoteSocketAddress().toString();
					System.out.println("Incoming connection from: " + address);
					buffer.clear();
                    // 将通道内的数据读入缓冲区
					socketChannel.read(buffer);
                    // 转换状态
					buffer.flip();
                    // 从缓冲区中读取数据
					while (buffer.hasRemaining())
						System.out.print((char) buffer.get());
				}
			}
		}
	}
}
  • 客户端代码:
package cn.lincain.nio.socket;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.TimeUnit;

public class ChannelClient {
	private static final int PORT = 20000;
	private static final int SLEEP_TIME = 5;
	private static final String IP_ADD = "127.0.0.1";

	public static void main(String[] args) throws Exception {
        // 创建SocketChannel对象,设置ip地址和端口号,并设置为非阻塞状态
		SocketChannel sChannel = SocketChannel.open();
		sChannel.connect(new InetSocketAddress(IP_ADD, PORT));
		sChannel.configureBlocking(false);
         // 创建缓冲区用于存储数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
		int i = 0;
        // 确认是否连接成功,如果成功,则循环发送数据
		while (sChannel.finishConnect()) {
			System.out.println("准备发送第" + (++i) + "次发送信息");
			buffer.clear();
            // 向缓冲区中写入数据
            String context = "Hello Server,I'm come from Client-" + i + System.lineSeparator();
			buffer.put(context.getBytes());
            // 转换状态
			buffer.flip();
            // 由缓冲区向通道内写入数据
			while (buffer.hasRemaining()) {
				sChannel.write(buffer);
			}
			TimeUnit.SECONDS.sleep(SLEEP_TIME);
		}
	}
}

执行代码:

Incoming connection from: /127.0.0.1:4776
Hello Server,I'm come from Client-1
Incoming connection from: /127.0.0.1:4776
Hello Server,I'm come from Client-2
Incoming connection from: /127.0.0.1:4776
Hello Server,I'm come from Client-3

针对以上两个Socket通道,这里简单总结一下它们的使用步骤:

  • 服务端
  1. 创建ServerSocketChannel实例对象,绑定ip地址和端口号(设置阻塞状态可选)
  2. 调用accept()创建一个SocketChannel实例对象,用于向客户端读/写数据
  3. 创建数据缓冲区来读取客户端数据或向客户端发送数据
  • 客户端
  1. 创建SocketChannel实例对象,并连接到指定的服务器
  2. 创建数据缓冲区来读取客户端数据或向客户端发送数据
DatagramChannel的使用

DatagramChannel是针对面向数据报套接字的通道,和DatagramSocket相对应的,它也是无连接。不需要建立连接即可收发数据,下面通过一个简单代码示例进行演示。

  • 服务端代码:
package cn.lincain.arts.nio.socket;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;

public class DataServer {
	private static final int PORT = 20000;
	private static final String IP_ADD = "127.0.0.1";

	public static void main(String[] args) throws Exception {
        // 创建一个DatagramChannel实例对象,并绑定到相应的接口
		DatagramChannel channel = DatagramChannel.open();
		channel.socket().bind(new InetSocketAddress(PORT));
		
        // 创建两个ByteBuffer,分别用于数据的读写
		ByteBuffer readBuffer = ByteBuffer.allocate(1024);
		ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
		
        // 从通道内接收数据,并在控制台打印
		readBuffer.clear();
        // 通过receive()接收消息,返回一个SocketAddress对象,表示发送消息方的地址
		SocketAddress  address = channel.receive(readBuffer);
		System.out.println(address);
		readBuffer.flip()while (readBuffer.hasRemaining()) {
			System.out.print((char) readBuffer.get());
		}
		
        // 向通道发送数据
		writeBuffer.clear();
		String context = "This is come from Server...";
		writeBuffer.put(context.getBytes());
		writeBuffer.flip();
        // 通过send()发送数据,需要指定发送的ip和端口号
		channel.send(writeBuffer, new InetSocketAddress(IP_ADD, 20001));
	}
}
  • 客户端代码:
package cn.lincain.arts.week3.datagram;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;

public class DataClient {
	private static final int PORT = 20001;
	private static final String IP_ADD = "127.0.0.1";
	
	public static void main(String[] args) throws Exception {
        // 创建一个DatagramChannel实例对象,并绑定到相应的接口
		DatagramChannel channel = DatagramChannel.open();
		channel.socket().bind(new InetSocketAddress(PORT));
		// 创建两个ByteBuffer,分别用于数据的读写
		ByteBuffer readBuffer = ByteBuffer.allocate(1024);
		ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
		
		// 向通道发送数据
		writeBuffer.clear();
		String context = "This is come from Client...";
		writeBuffer.put(context.getBytes());
		writeBuffer.flip();
        // 通过send()发送数据,需要指定发送的ip和端口号
		channel.send(writeBuffer,new InetSocketAddress(IP_ADD,20000));
        
		// 从通道内接收数据,并在控制台打印
		readBuffer.clear();
        // 通过receive()接收消息,返回一个SocketAddress对象,表示发送消息方的地址
		SocketAddress  address = channel.receive(readBuffer);
		System.out.println(address);
		readBuffer.flip();
		System.out.print("客户端:");
		while(readBuffer.hasRemaining()) {
			System.out.print((char)readBuffer.get());
		}
	}
}

首先开启服务端,然后开启客户端,结果如下:

/127.0.0.1:20001
服务器:This is come from Client...
----------------------------------
/127.0.0.1:20000
客户端:This is come from Server...

仔细看看上面的代码,发现和前面的SocketChannel读写数据有些区别,它们并没有采用connect()方法进行连接,并且读写数据也不是调用read()write()方法。因为UDP是无连接的,它不需要像TCP一样建立连接后才能读写数据。

另外我们也可以通过调用connect()方法连接特定地址,只是并不是像TCP通道那样会创建一个真正的连接,它本质是将DatagramChannel锁定,让其只能从特定地址收发数据。方法如下:

// 和指定的地址建立连接
channel.connect(new InetSocketAddress("127.0.0.1", 20000));
// 读写数据
channel.read(readBuffer);
channel.write(writeBuffer);

虽然DatagramChannel也是Socket通道,但是它是无连接的,所以使用方法相对简单一些,这里也简单总结一下它的使用步骤:

  1. 调用静态方法open()创建DatagramChannel的实例对象
  2. 创建ByteBuffer对象用于从通道读取数据,或者向通道写入数据
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值