介绍
理解:通道是一个连接I/O服务的管道并提供与该服务交互的方法。
Channel类似于传统的”流”,但是Channel不能直接访问数据,需要和缓冲区Buffer进行交互。
通道和传统流的区别:
1. 通道可以是双向的,既可以读取数据,又可以写数据到通道。但流的读写通常是单项的
2. 通道可以异步的读写
3. 通道不能直接访问数据,需要和Buffer进行交互
通道可以简单的分为两大类:文件通道和Socket通道
文件通道
文件通道的主要实现是FileChannel。文件通道总是阻塞的,因此不能被置于非阻塞模式。
FileChannel的创建
FileChannel对象不能直接创建。一个FileChannel实例只能通过一个打开的file对象(RandomAccessFil、FileInputStraem、FileOutputStream等)上调用getChannel(0方法获取。调用getChannel()方法返回一个连接到相同文件的FileChannel对象且该FileChannel对象具有于file对象相同的访问权限。
创建FileChannel示例:
//创建一个RandomAccessFile(随机访问文件)对象,
RandomAccessFile raf=new RandomAccessFile("D:\\test.txt", "rw");
//通过RandomAccessFile对象的getChannel()方法。FileChannel是抽象类。
FileChannel inChannel=raf.getChannel();
常用方法
package java.nio.channels;
public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
{
// This is a partial API listing
// All methods listed here can throw java.io.IOException
//从FileChannel读取数据
public abstract int read(ByteBuffer dst)
public abstract int read (ByteBuffer dst, long position);
//向FileChannel写数据
public abstract int write(ByteBuffer src)
public abstract int write (ByteBuffer src, long position);
//获取文件大小
public abstract long size();
//获取位置
public abstract long position();
//设置位置
public abstract void position (long newPosition);
//用于文件截取
public abstract void truncate (long size);
//将通道里尚未写入磁盘的数据强制写到磁盘上
public abstract void force (boolean metaData);
//文件锁定,position-开始位置,size-锁定区域的大小,shared-表示锁是否共享(false为独占),lock()锁定整个文件
public final FileLock lock();
public abstract FileLock lock (long position, long size, boolean shared);
public final FileLock tryLock();
public abstract FileLock tryLock (long position, long size, boolean shared);
//内存映射文件
public abstract MappedByteBuffer map (MapMode mode, long position, long size);
public static class MapMode;
public static final MapMode READ_ONLY;
public static final MapMode READ_WRITE;
public static final MapMode PRIVATE;
//用于通道之间的数据传输
public abstract long transferTo (long position, long count, WritableByteChannel target);
public abstract long transferFrom (ReadableByteChannel src, long position, long count);
}
public abstract MappedByteBuffer map (MapMode mode, long position, long size)
该map()方法可以在一个打开的文件和一个特殊类型的ByteBuffer之间建立虚拟内存映射。
通过内存映射机制来访问一个文件会比使用常规方法读写高效得多,甚至比使用通道的效率都高。
transferTo()将数据从FileChannel传输到其他的Channel中。
transferFrom()从其他Channel获取数据
transferTo()和transferFrom()方法允许将一个通道交叉连接到另一个通道,而不需要通过一个中间缓存来传递数据。只有FileChannel类有这两个方法。
transferFrom()示例
package test;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
public class FileChannelTest2 {
public static void main(String[] args) throws IOException {
RandomAccessFile aFile = new RandomAccessFile("d:\\ fromFile.txt", "rw");
FileChannel fromChannel = aFile.getChannel();
RandomAccessFile bFile = new RandomAccessFile("d:\\ toFile.txt", "rw");
FileChannel toChannel = bFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position, count);
aFile.close();
bFile.close();
System.out.println("over!");
}
}
transferTo()示例
package test;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
public class FileChannelTest3 {
public static void main(String[] args) throws IOException {
RandomAccessFile aFile = new RandomAccessFile("d:\\fromFile.txt", "rw");
FileChannel fromChannel = aFile.getChannel();
RandomAccessFile bFile = new RandomAccessFile("d:\\toFile.txt", "rw");
FileChannel toChannel = bFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
aFile.close();
bFile.close();
System.out.println("over!");
}
}
FileChannel示例
package test;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelTest {
public static void main(String[] args) throws IOException {
RandomAccessFile aFile = new RandomAccessFile("d:\\test.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
System.out.println("wan");
}
}
Socket通道
常用的Socket通道
DatagramChannel:用于UDP的数据读写
SocketChannel: 用于TCP的数据读写,一般是客户端实现
ServerSocketChannel: 允许我们监听TCP连接请求,每个请求会创建会一个SocketChannel,一般是服务器实现
以上Channel都继承AbstractSelectableChannel,于是这三个Channel都是可以设置成非阻塞模式的。Socket通道(DatagramChannel、SocketChannel和ServerSocketChannel)在被实例化时都会创建一个对等的socket对象,即我们熟悉的java.net的类型(Socket、ServerSocket和DatagramSocket)。
1.非阻塞模式
Socket通道可以在非阻塞模式下运行,跟非阻塞/阻塞相关的函数(SelecableChannel类下)
//配置是否是阻塞模式(block为true,则为阻塞模式。block为false,则设置为非阻塞模式)
public abstract SelectableChannel configureBlocking(boolean block)
//获取当前是否是阻塞模式
public abstract boolean isBlocking();
//获取 configureBlocking和register方法同步的锁
public abstract Object blockingLock();
2.ServerSocketChannel
ServerSocketChannel用于监听TCP连接请求,常用的API如下:
public abstract class ServerSocketChannel
extends AbstractSelectableChannel
implements NetworkChannel
{
//静态方法,用于创建一个新的ServerSocketChannel对象,后续还需要跟ServerSocket进行绑定操作
public static ServerSocketChannel open()
//获取关联该ServerSocketChannel的server socket
public abstract ServerSocket socket()
//当创建ServerSocketChannel对象并绑定一个ServerSocket关联的通道之后,
//调用该方法可以监听客户端的连接请求
public abstract SocketChannel accept()
//并绑定指定端口的ServerSocket,(jdk1.7以上才有)
public final ServerSocketChannel bind(SocketAddress local)
//同选择器一起使用,获取感兴趣的操作
public final int validOps()
}
静态方法open()用于创建一个新的ServerSocketChannel对象,将会返回一个未绑定的ServerSocket关联的通道。该对等ServerSocket可以通过在返回的ServerSocketChannel上调用socket()方法获取。jdk1.7以前ServerSocketChannel没有bind()方法,因此必须取出对等的socket并使用它来绑定一个端口开始监听连接
ServerSocketChannel ssc = ServerSocketChannel.open();
ServerSocket serverSocket = ssc.socket();
serverSocket.bind(new InetSocketAddress(1234));
jdk1.7以后ServerSocketChannel提供了bind()方法,所以以上可以简化为
ServerSocketChannel ssc = ServerSocketChannel.open().bind(new InetSocketAddress(1234));
简单的ServerSocketChannel示例:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ServerSocketChannelDemo {
public static final String GREETING = "Hello I must be going.\r\n";
public static void main(String[] args) throws IOException, InterruptedException {
int port = 8088;
if (args.length > 0){
port = Integer.parseInt(args[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);
}else {
System.out.println("Incoming connection form:"+sc.socket().getRemoteSocketAddress());
buffer.rewind();
sc.write(buffer);
sc.close();
}
}
}
}
3. SocketChannel
SocketChannel 常见的API如下:
public abstract class SocketChannel extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel,NetworkChannel
{
//静态方法,打开套接字通道(创建SocketChannel实例)
public static SocketChannel open()
//静态方法,打开套接字通道并将其连接到远程地址
public static SocketChannel open(SocketAddress remote)
//返回一个操作集,标识此通道所支持的操作
public final int validOps()
//用于将Socket绑定到一个端口
public abstract SocketChannel bind(SocketAddress local)
//获取该SocketChannel关联的Socket(套接字)
public abstract Socket socket()
//判断是否已连接此通道的网络套接字
public abstract boolean isConnected()
//判断此通道上是否正在进行连接操作。
public abstract boolean isConnectionPending()
//用于SocketChannel连接到远程地址
public abstract boolean connect(SocketAddress remote)
//从通道中读取数据到缓冲区中
public abstract int read(ByteBuffer dst)
public abstract long read(ByteBuffer[] dsts, int offset, int length)
//将缓冲区的数据写入到通道中
public abstract int write(ByteBuffer src)
public abstract long write(ByteBuffer[] srcs, int offset, int length)
}
创建SocketChannel对象并连接到远程地址
SocketChannel sc = SocketChannel.open(new InetSocketAddress(ip,port));
等价于
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress(ip,port));
线程在连接建立好或超时之前都保持阻塞。
示例:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class SocketChannelDemo {
public static void main(String[] args) {
int port = 8088;
String ip = "127.0.0.1";
ByteBuffer readBuffer = ByteBuffer.allocate(512);
try {
SocketChannel socketChannel = SocketChannel.open();//创建一个socketChannel
socketChannel.connect(new InetSocketAddress(ip,port));
while (!socketChannel.isConnected()){
System.out.println("connecting ...");
Thread.sleep(1000);
}
System.out.println("connected");
socketChannel.write(ByteBuffer.wrap("Hello,I am Client".getBytes()));
while (socketChannel.read(readBuffer) > 0){
System.out.println(readBuffer.toString());
readBuffer.flip();
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4. DatagramChannel
SocketChannel模拟面向连接的流协议(如TCP/IP),而DatagramChannel则面向无连接的协议(如UDP/IP)
public abstract class DatagramChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel
{
//创建DatagramChannel实例
public static DatagramChannel open()
public static DatagramChannel open(ProtocolFamily family)
public final int validOps()
//将通道的套接字绑定到本地地址
public abstract DatagramChannel bind(SocketAddress local)
//获取该DatagramChannel关联的DatagramSocket对象
public abstract DatagramSocket socket()
//是否已连接到套接字
public abstract boolean isConnected()
//用于DatagramChannel连接到远程地址
public abstract DatagramChannel connect(SocketAddress remote)
//通道此DatagramChannel接受到的数据包
public abstract SocketAddress receive(ByteBuffer dst)
//通过此DatagramChannel发送数据包
public abstract int send(ByteBuffer src, SocketAddress target)
//从此通道读取数据包
public abstract int read(ByteBuffer dst)
public abstract long read(ByteBuffer[] dsts, int offset, int length)
//将数据写入到此通道
public abstract int write(ByteBuffer src)
public final long write(ByteBuffer[] srcs) throws IOException
}
打开DatagramChannel
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetAddress(8088));
DatagramChannel对应的DatagramSocket如果未调用bind()绑定端口号,也是可以通讯的,因为系统默认会动态分配一个端口号
receive()方法,接受数据包
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.clear();
channel.receive(buffer);
send()方法,发送数据包
String newData = "New String to send,time:" + System.currentTimeMillis();
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.clear();
buffer.put(newData.getBytes());
buffer.flip();
int bytesSent = channel.send(buffer, new InetAddress("jenkov.com", 80));
Connecting to a Specific Address
DatagramChannel是可以“连接”到网络上特定地址的。因为UDP是无连接的,所以这种“连接”不是真正像TCP那样的和远程地址建立了一个连接。不如说是它将锁定你的DatagramChannel,以便你只能向一个特定的地址发送和接收数据包。
连接
channel.connect(new InetAddress("jenkov.com", 80));
当“连接”建立后,你可以像使用传统的Channel一样调用read()和write()方法。只是对于发送的数据,你不会得到任何关于交付的保证。下面是一些例子:
int bytesRead = channel.read(buffer); //读取数据
int bytesWrite = channel.write(buffer); //写数据
以上内容主要整理自:《Java NIO》