码恋 码恋

ALL YOUR SMILES, ALL MY LIFE.

目录
Netty 系列笔记之 NIO 核心组件 Channel
/    

Netty 系列笔记之 NIO 核心组件 Channel

一、开篇

NIO Channel 在 java.nio.channels 包中,Channel 本身不直接访问数据,而是与 Buffer 交互。也就是说,数据可以从 Channel 读取到 Buffer 中,也可以从 Buffer 写到 Channel 中。如下图:

image.png

在 Java NIO 中,Channel 有几种重要实现:

  • FileChannel :用于从文件中读写数据。
  • DatagramChannel :通过 UDP 读写数据。
  • SocketChannel :TCP 中客户端用于发起 TCP 连接的 Channel 。
  • ServerSocketChannel :TCP 中服务端用于监听新进来的连接的 Channel ,每进来一个新连接,都会创建一个与之对应的 SocketChannel 。

因此,本篇就讨论上面几种 Channel 的使用。

二、FileChannel

FileChannel 是一种用于读取、写入、映射和操作文件的通道。

1、打开 FileChannel

在使用之前需要获得 FileChannel ,但是其无法直接获取,我们需要通过使用 InputStream、OutputStream 或 RandomAccessFile 来获取 FileChannel 实例。

  • 通过 RandomAccessFile 来获取 Channel

    RandomAccessFile accessFile = new RandomAccessFile("/123.txt", "rw");
    FileChannel channel = accessFile.getChannel();
    
  • 通过 Stream 来获取 Channel

    FileInputStream inputStream = new FileInputStream("/123.txt");
    FileChannel channel = inputStream.getChannel();
    
2、从 FileChannel 中读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readBytes = channel.read(byteBuffer);

在开篇我们就说了 Channel 本身不直接访问数据,需要与 Buffer 做交互。所以首先通过 ByteBuffer.allocate(1024) 为 ByteBuffer 分配空间,然后调用 channel.read(byteBuffer) 方法读取数据到 ByteBuffer 中。read( ) 方法返回读取的字节数,如果返回 -1 则表示读到了文件末尾。

3、向 FileChannel 中写入数据
String data = "2121213123123123123wwdddd";
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.clear();
buf.put(data.getBytes());
buf.flip();

while(buf.hasRemaining()) {
	channel.write(buf);
}

Channel 写入数据同样与 Buffer 交互,通过调用 write( ) 方法向 Channel 中写入数据。注意写入方法在循环中调用,因为无法保证一次是否写入完成,所以需要重复调用 write() 方法,直到 Buffer 中已经没有尚未写入通道的字节。

4、关闭 FileChannel

如传统 IO 中的输入输出流一样,在 FileChannel 使用后需要调用 close( ) 方法关闭。当然在我们使用 RandomAccessFile 或者流获取 Channel 后会及时关闭 RandomAccessFile 或流,在它们的 close( ) 方法中已经帮我们关闭了 FileChannel ,因此,一般这种情况不需要显式调用 FileChannle#close() 方法。

5、FileChannel#position( ) 方法
public abstract long position() throws IOException;

public abstract FileChannel position(long newPosition) throws IOException;

position() 方法用于返回当前 Channel 的文件位置,即从文件的开头开始计数到当前位置的字节数。

同时 FileChannel 也提供了带有参数的 postion(long newPosition) 方法用于设置当前 Channel 的文件位置。

设置位置可以比该文件的当前大小值大,但并不会改变文件的大小。 这样尝试读取数据将立即返回结束文件的标志 -1 。如果尝试写入数据将导致文件变大以容纳新的字节,先前结束的文件和新写入字节之间的字节的值不确定,即形成所谓的 文件空洞

6、FileChannel#size( ) 方法
public abstract long size() throws IOException;

用于返回当前 Channel 的文件大小。

7、FileChannel#truncate( ) 方法
public abstract FileChannel truncate(long size) throws IOException;

使用该方法可以截取当前 Channel 的一个文件,截取文件时,文件当中指定长度后面的部分将被删除。

8、FileChannel#force( ) 方法
public abstract void force(boolean metaData) throws IOException;

将 Channel 中尚未写入磁盘的数据强制写入磁盘。在向 FileChannel 中写入数据时,操作系统会将数据缓存在内存中,所以无法保证写入到 FileChannel 里的数据一定会即时写到磁盘上。要保证这一点,可以调用 force() 方法。

force() 方法有一个 boolean 类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。

9、FileChannel#transferTo & transferFrom 方法
/**
*  将字节从该通道的文件传输到给定的可写字节通道。  
*/
public abstract long transferTo(long position, long count,
                                    WritableByteChannel target)
        throws IOException; 

/**
*  从给定的可读字节通道将字节传输到此通道的文件中。
*/
 public abstract long transferFrom(ReadableByteChannel src,
                                      long position, long count)
        throws IOException;

三、DatagramChannel

DatagramChannel 是一种能够首发 UDP 包的通道。UDP 是无连接的网络协议,所以不能像其它通道一样读取和写入,它发送和接收的是数据包。

1、打开 DatagramChannel
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));

通过 DatagramChannel.open() 方法打开 DatagramChannel , 然后调用 socket() 方法获取 DatagramSocket 用于接收数据包,并绑定 9999 端口。

2、接收数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.clear();
channel.receive(byteBuffer);

通过调用 receive() 方法从 DatagramChannel 中接收数据,将接收到的数据放入指定的 ByteBuffer 中,如果数据超过 ByteBuffer 的容量,则将剩余的数据丢弃。

3、发送数据
String data = "21231wqweqweqewaaaa";
byteBuffer.clear();
byteBuffer.put(data.getBytes());
byteBuffer.flip();
channel.send(byteBuffer, new InetSocketAddress("aysaml.com", 80));

使用 channel.send() 方法向 host 为 aysaml.com 的 80 端口发送数据。

4、连接到指定地址
channel.connect(new InetSocketAddress("aysaml.com", 80));

DatagramChannel 也提供了 read()write() 方法用来接收和发送数据,使用这两个方法前需要通过 connect() 方法连接到指定地址。

四、SocketChannel

SocketChannel 是一个连接到 TCP 网络套接字的通道。可以通过以下2种方式创建 SocketChannel :

  • 打开一个 SocketChannel 并连接到互联网上的某台服务器。
  • 一个新连接到达 ServerSocketChannel 时,会创建一个 SocketChannel 。
1、打开 SocketChannel
SocketChannel channel = SocketChannel.open();
channel.bind(new InetSocketAddress("aysaml.com", 80));
2、从 SocketChannel 读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readBytes = channel.read(byteBuffer);
3、向 SocketChannel 写数据
String data = "212121qdwdqwdas";
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.clear();
        byteBuffer.put(data.getBytes());
        byteBuffer.flip();
        while (byteBuffer.hasRemaining()){
            channel.write(byteBuffer);
        }
4、非阻塞模式

可以将 SocketChannel 设置为非阻塞模式,对于 connect(), read()write() 方法在调用之后会立即返回,采用异步的执行模式。

  • connect()
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("aysaml",80));
while (channel.finishConnect()) {
  // 连接后
}
  • write()
    非阻塞模式下,write() 方法可能在没有写出任何内容的时候就返回了,所以要在循环中一直调用直到 Buffer 中没有要写的了。
  • read()
    非阻塞模式下,同 write() 方法类似,可能在没有读到任何内容的时候就返回了,所以需要根据返回的读取字节数来判断是否读到数据。

五、ServerSocketChannel

对于服务端来说,ServerSocketChannel 是一个可以监听新进来的 TCP 连接的通道。

ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress(9999));
for(;;) {
    SocketChannel socketChannel = channel.accept();
}

上面例子展示了基本用法,与 SocketChannel 基本相同:

  • 4 行:在循环中调用 channel.accept() 方法接收新进来的连接,并创建 SocketChannel 。

同时,ServerSocketChannel 也可以设置成非阻塞模式:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);

for(;;){
    SocketChannel socketChannel =
            serverSocketChannel.accept();

    if(null != socketChannel) {
       // do sth..
    }
}

注意在使用 SocketChannel 时注意判空。

六、结语

关于 Channel 方面我们列举了常用的几种 Channel 的使用方式,其中多次涉及了 Buffer 相关内容,会在下篇继续探讨。



❤ 转载请注明本文地址或来源,谢谢合作 ❤


center