Java NIO之Channel詳細理解

介紹

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

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章