介紹
理解:通道是一個連接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》