Java NIO 中的 Channel 详解(下)

简介: Channel是一个通道,可以通过它读取和写入数据,它就像是水管一样,网络数据通过 Channel 进行读取和写入。通道和流的不同之处在与通道是双向的,流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStram 的子类),而且通道上可以用于读,写或者同事用于读写。因为 Channel 是全双工的,所以它可以比流更好的映射底层操作系统的 API。

SokectChannel 特征


(1)对于已经存在的 socket 不能创建 SocketChannel


(2)SocketChannel 中提供的 open 接口创建的 Channel 并没有尽享网络级联,需要使用 conect 接口连接到指定地址


(3)未进行连接的 SocketChannel 执行 I/O 操作时,会抛出 NotYesConnectedException


(4)SocketChannel 支持两种 I/O 模式:阻塞式和非阻塞式


(5)SocketChannel 支持异步关闭。如果 SocketChannel 一个线程上 read 阻塞,另一个线程对该 SocketChannel 调用 shutdownInput. 则读阻塞的线程将返回 -1, 表示没有任何数据;如果 SocketChannel 在一个线程上 write 阻塞,另一个线程对该 SocketChannel 调用 shutdownwrite , 则写阻塞的线程将抛出

AsynchronousCloseException.


(6)SocketChannel 支持设定参数:


SO_SNDBUF 套接字发送缓冲区大小

SO_RCVBUF 套接字接受缓冲区大小

SO_KEEPALIVE 保活连接

O_REUSEADDR 复用地址

SO_LINGER 有数据传输时延迟关闭 Channel (只有在非阻塞模式下有用)

TCP_NODELAY 禁用 Nagle 算法


SocketChannel 使用


(1)创建 SokcetChannel


方式一


//  创建 socketchannel
SocketChannel socketChannel = SocketChannel.open(
    new InetSocketAddress("www.baidu.com", 80));


方式二


SocketChannel socketChannel1 = SocketChannel.open();
socketChannel1.connect(new InetSocketAddress("www.baidu.com", 80));


直接使用有参 open api 或者使用无惨 open api , 但是在无参 open 只是创建了一个 SocketChannel 对象,并没有进行实质性的 tcp 连接


(2)链接校验


// 测试 SocketChannel 是否 open 状态
socketChannel.isOpen();
// 测试 SocketChannel 是否已经被链接
socketChannel.isConnected();
// 测试 SocketChannel 是否正在链接状态
socketChannel.isConnectionPending();
// 校验正在进行套接字连接的 SocketChannel 是否已经完成链接
socketChannel.finishConnect();


(3)读写模式


前面提到 SocketChannel 支持阻塞和非阻塞两种模式


socketChannel.configureBlocking(false);


通过以上方法设置 SocketChannel 的读写模式。 false 表示非阻塞,true 表示阻塞。


(4)读写


SocketChannel socketChannel0 = SocketChannel.open(
    new InetSocketAddress("www.baidu.com", 80));
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel0.read(byteBuffer);
socketChannel0.close();
System.out.println("read over");


以上为阻塞式读,当执行到 read 处,线程将阻塞,控制台将无法打印 “read over”.


SocketChannel socketChannel10 = SocketChannel.open(
    new InetSocketAddress("www.baidu.com", 80));
socketChannel10.configureBlocking(false)
    ByteBuffer byteBuffer1 = ByteBuffer.allocate(16);
socketChannel10.read(byteBuffer1);
socketChannel10.close();
System.out.println("read over");


以上为非阻塞读,控制台将打印 read over

读写都是面向缓冲区,这个读写方式与前文中的 FileChannel 相同。


(5)设置和获取参数


socketChannel5.setOption(StandardSocketOptions.SO_KEEPALIVE, Boolean.TRUE)
    .setOption(StandardSocketOptions.TCP_NODELAY, Boolean.TRUE);


通过 setOption 方法可以设置 socket 套接字相关的参数


socketChannel5.getOption(StandardSocketOptions.SO_KEEPALIVE);
socketChannel5.getOption(StandardSocketOptions.SO_RCVBUF);


可以通过 getOption 获取相关的参数值,如果默认接受缓冲区大小式 8192 byte

SocketChannel 还支持多路复用,多路复用后面内容会详细描述。


DatagramChannel


正如 SocketChannel 对应 Socket,ServerSocketChannel 对应 ServerSocket ,每一个 DatagramChannel 对象也有一个关联的 DatagramSocket 对象。正如 SocketChannel 模拟连接导向的流协议 (如:TCP/IP),Datagram Channel 则模拟包导向的无连接协议(如:UDP/IP)。DatagramChannel 是无连接的,每个数据报(datagram) 都是一个自包含的实体,拥有它自己的目的地址以及不依赖其他数据报的数据负载。与相面流的 socket 不同, DatagramChannel 对象也可以接受来自任意地址的数据包。每个到达的数据都包含有关于它来自何处的信息(源地址)。


打开 DatagramChannel


DatagramChannel server = DatagramChannel.open();
server.socket().bind(new InetSocketAddress(25000));


这个例子是打开 25000 端口接受 UDP 数据包。


接受数据


通过 receive() 接受 UDP 包


ByteBuffer receiveBuffer = ByteBuffer.allocate(64);
receiveBuffer.clear();
SocketAddress receiveAddr = server.receive(receiveBuffer);


SocketAddress 可以获取发包的 ip 、端口等信息,通过 toString 查看,格式如下:

/127.0.0.1:57126


发送数据


通过 send() 发送 UDP 包


DatagramChannel server1 = DatagramChannel.open();
ByteBuffer sendBuffer = ByteBuffer.wrap("client hello!".getBytes());
server1.send(sendBuffer, new InetSocketAddress("127.0.0.1", 25000));


连接


UDP 不存在整整意义上的连接,这里的连接是向特定地址用 read 和 write 接受发送数据包。


client.connect(new InetSocketAddress("127.0.0.1", 25000));
int readSize = client.read(sendBuffer);
server.write(sendBuffer);


read() 和 write() 自由 connect() 后才能使用,不能会抛出 NotYetConnectedException 异常。用 read() 接受时, 如果没有接受到包, 会抛出 PortNnreachableException 异常。


DatagramChannel 实例


客户端发送,服务端接受的例子:


// 发送客户端
public class DatagramChannelClient {
  public static void main(String[] args) throws IOException, InterruptedException {
    DatagramChannel sendChannel = DatagramChannel.open();
    InetSocketAddress socketAddress =
        new InetSocketAddress("127.0.0.1", 25000);
    while (true) {
      ByteBuffer buffer = ByteBuffer.wrap("发送 DataGram 测试数据".getBytes(StandardCharsets.UTF_8));
      sendChannel.send(buffer, socketAddress);
      System.out.println("send success!");
      TimeUnit.SECONDS.sleep(2);
    }
  }
}
// 接受服务端
public class DatagramChannelServer {
  public static void main(String[] args) throws IOException {
    DatagramChannel server = DatagramChannel.open();
    server.socket().bind(new InetSocketAddress(25000));
    ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
    while (true) {
      receiveBuffer.clear();
      SocketAddress receiveAddress = server.receive(receiveBuffer);
      receiveBuffer.flip();
      System.out.println(receiveAddress.toString());
      System.out.println(StandardCharsets.UTF_8.decode(receiveBuffer));
    }
  }
}


服务端打印:


image.png


客户端打印:


image.png


连接测试代码


public class DatagramChannelClient2 {
  public static void main(String[] args) throws IOException, InterruptedException {
    DatagramChannel channel = DatagramChannel.open();
    InetSocketAddress socketAddress =
        new InetSocketAddress("127.0.0.1", 25000);
    channel.bind(socketAddress);
    channel.connect(socketAddress);
    ByteBuffer buffer = ByteBuffer.wrap("发送 DataGram 测试数据".getBytes(StandardCharsets.UTF_8));
    channel.write(buffer);
    System.out.println("write success!");
    // buffer
    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    while (true) {
      readBuffer.clear();
      channel.read(readBuffer);
      readBuffer.flip();
      System.out.println(StandardCharsets.UTF_8.decode(readBuffer));
      TimeUnit.SECONDS.sleep(2);
    }
  }
}


数据打印


image.png


Scatter/Gather


Java NIO 开始支持 scatter/gather , scatter/gather 用于描述从 channel 中读取或者写入到 channel 操作。


分散(scatter) 从channel 中读取是指在读操作时会将读取的数据写入多个 buffer 中,因此,channel 将从 channel z中读取的数据 “分散(scatter)” 到多个 buffer 中


聚集(gather) 写入 channel 是指在写操作时候将多个 buffer 的数据写同一个channel , 因此,channel 将多个 buffer 的数据“聚集(gather)”后发送到 channel

scatter/gather 经常用于需要将传输的数据分开处理得场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的 buffer 中,这样你可以方便的处理消息头和消息体。


Scattering Reads


Scattering Reads 是指从一个 channel 读取到多个 buffer 中。如下图描述:


image.png


ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = [heander, body];
channel.read(bufferArray);


注意 buffer 首先被插入到数组,然后再将数组作为 channel.read() 的输入参数。


read() 方法按照 buffer 在数组中的顺序从 channel 中读取的数据写入到 buffer , 当一个 buffer 被写满后,channel 紧接着向另外一个 buffer 中写。


Scattering Reads 在移动下一个 buffer 之前,必须填满当前的 buffer , 这就意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息同和消息体,消息头必须完成填充(例如 128 byte),Scattering Reads 才能正常工作。


Gathing Writes


Gathering Writes 是指从多个 buffer 写入到同一个 channel。如下图描述:


image.png


ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = [heander, body];
channel.write(bufferArray);


buffers 数组是 write 方法的入参,write() 方法会按照 buffer 数组中的顺序,将数据写入到 channel, 注意只有 position 和 limit 之间的数据才会被写入。因此,如果一个 buffer 的容量为 128byte, 但是仅仅包含了 58 byte 的数据将被写入到 channel 中。Scattering Reads 相反,Gathering Writes 能较好的处理动态消息。


相关文章
|
1月前
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
46 3
|
1月前
|
存储 监控 Java
Java的NIO体系
通过本文的介绍,希望您能够深入理解Java NIO体系的核心组件、工作原理及其在高性能应用中的实际应用,并能够在实际开发中灵活运用这些知识,构建高效的Java应用程序。
47 5
|
6月前
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
2月前
|
消息中间件 缓存 Java
java nio,netty,kafka 中经常提到“零拷贝”到底是什么?
零拷贝技术 Zero-Copy 是指计算机执行操作时,可以直接从源(如文件或网络套接字)将数据传输到目标缓冲区, 而不需要 CPU 先将数据从某处内存复制到另一个特定区域,从而减少上下文切换以及 CPU 的拷贝时间。
java nio,netty,kafka 中经常提到“零拷贝”到底是什么?
|
4月前
|
存储 网络协议 Java
Java NIO 开发
本文介绍了Java NIO(New IO)及其主要组件,包括Channel、Buffer和Selector,并对比了NIO与传统IO的优势。文章详细讲解了FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel及Pipe.SinkChannel和Pipe.SourceChannel等Channel实现类,并提供了示例代码。通过这些示例,读者可以了解如何使用不同类型的通道进行数据读写操作。
Java NIO 开发
|
3月前
|
Java
让星星⭐月亮告诉你,Java NIO之Buffer详解 属性capacity/position/limit/mark 方法put(X)/get()/flip()/compact()/clear()
这段代码演示了Java NIO中`ByteBuffer`的基本操作,包括分配、写入、翻转、读取、压缩和清空缓冲区。通过示例展示了`position`、`limit`和`mark`属性的变化过程,帮助理解缓冲区的工作原理。
53 2
|
5月前
|
Java
"揭秘Java IO三大模式:BIO、NIO、AIO背后的秘密!为何AIO成为高并发时代的宠儿,你的选择对了吗?"
【8月更文挑战第19天】在Java的IO编程中,BIO、NIO与AIO代表了三种不同的IO处理机制。BIO采用同步阻塞模型,每个连接需单独线程处理,适用于连接少且稳定的场景。NIO引入了非阻塞性质,利用Channel、Buffer与Selector实现多路复用,提升了效率与吞吐量。AIO则是真正的异步IO,在JDK 7中引入,通过回调或Future机制在IO操作完成后通知应用,适合高并发场景。选择合适的模型对构建高效网络应用至关重要。
116 2
|
5月前
|
网络协议 C# 开发者
WPF与Socket编程的完美邂逅:打造流畅网络通信体验——从客户端到服务器端,手把手教你实现基于Socket的实时数据交换
【8月更文挑战第31天】网络通信在现代应用中至关重要,Socket编程作为其实现基础,即便在主要用于桌面应用的Windows Presentation Foundation(WPF)中也发挥着重要作用。本文通过最佳实践,详细介绍如何在WPF应用中利用Socket实现网络通信,包括创建WPF项目、设计用户界面、实现Socket通信逻辑及搭建简单服务器端的全过程。具体步骤涵盖从UI设计到前后端交互的各个环节,并附有详尽示例代码,助力WPF开发者掌握这一关键技术,拓展应用程序的功能与实用性。
187 0
|
6月前
|
安全 Java Linux
(七)Java网络编程-IO模型篇之从BIO、NIO、AIO到内核select、epoll剖析!
IO(Input/Output)方面的基本知识,相信大家都不陌生,毕竟这也是在学习编程基础时就已经接触过的内容,但最初的IO教学大多数是停留在最基本的BIO,而并未对于NIO、AIO、多路复用等的高级内容进行详细讲述,但这些却是大部分高性能技术的底层核心,因此本文则准备围绕着IO知识进行展开。
210 1
|
5月前
|
存储 网络协议 Java
【Netty 神奇之旅】Java NIO 基础全解析:从零开始玩转高效网络编程!
【8月更文挑战第24天】本文介绍了Java NIO,一种非阻塞I/O模型,极大提升了Java应用程序在网络通信中的性能。核心组件包括Buffer、Channel、Selector和SocketChannel。通过示例代码展示了如何使用Java NIO进行服务器与客户端通信。此外,还介绍了基于Java NIO的高性能网络框架Netty,以及如何用Netty构建TCP服务器和客户端。熟悉这些技术和概念对于开发高并发网络应用至关重要。
108 0