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》

 

 

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