Java NIO 通信基础介绍
高性能的 Java 通信,绝对离不开 Java NIO 技术,现在主流的技术框架或中间件服务器,都使 用了 Java NIO 技术,譬如:Tomcat、Jetty、Netty。
Java NIO 由以下三个核心组件组成:
- Channel(通道)
- Buffer(缓冲区)
- Selector(选择器)
NIO 和 OIO 的对比
在 Java 中,NIO 和 OIO 的区别,主要体现在三个方面:
1、OIO 是面向流(Stream Oriented)的,NIO 是面向缓冲区(Buffer Oriented)的。
何谓面向流,何谓面向缓冲区呢?
OIO 是面向字节流或字符流的,在一般的 OIO 操作中,我们以流式的方式顺序地从一个流中读取一个或多个字节,因此,我们不能随意地改变读取指针的位置。而在 NIO 操作中则不同,NIO 中引入了 Channel(通道)和 Buffer(缓冲区)的概念。读取和写入,只需要从通道中读取数据到缓冲区中,或将数据从缓冲区中写入到通道中。NIO 不像 OIO 那样是顺序操作,可以随意地读取 Buffer 中任意位置的数据。
2、OIO 的操作是阻塞的,而 NIO 的操作是非阻塞的。
NIO 如何做到非阻塞的呢?大家都知道,OIO 操作都是阻塞的,例如,我们调用一个 read 方法读取一个文件的内容,那么调用 read 的线程会被阻塞住,直到 read 操作完成。
而在 NIO 的非阻塞模式中,当我们调用 read 方法时,如果此时有数据,则 read 读取数据并返回;如果此时没有数据,则 read 直接返回,而不会阻塞当前线程。NIO 的非阻塞,是如何做到的呢?NIO 使用了通道和通道的多路复用技术。
3、OIO 没有选择器(Selector)概念,而 NIO 有选择器的概念。
NIO 的实现,是基于底层的选择器的系统调用。NIO 的选择器,需要底层操作系统提供支持。
而 OIO 不需要用到选择器。
使用 FileChannel 完成文件复制的实践案例
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class MyCopyFile {
private File inFile;
private File outFile;
private FileInputStream fis = null;
private FileOutputStream fos = null;
private FileChannel fisChannel = null;
private FileChannel fosChannel = null;
//复制文件
public void copyFile(String srcPath, String destPath) throws IOException {
try {
inFile = new File(srcPath);
outFile = new File(destPath);
fis = new FileInputStream(inFile);
fos = new FileOutputStream(outFile);
fisChannel = fis.getChannel();
fosChannel = fos.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int length = -1;
while ((length = fisChannel.read(buffer)) != -1) {
buffer.flip();
int outLenth = 0;
while ((outLenth = fosChannel.write(buffer)) != 0) {
System.out.println("读取的字节数为:" + outLenth);
}
buffer.clear();
}
//强制刷新磁盘
fosChannel.force(true);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
fosChannel.close();
fos.close();
fisChannel.close();
fis.close();
}
}
public static void main(String[] args) throws IOException {
MyCopyFiletest = new MyCopyFile();
String s1 = "D:\\maze.txt";
String s2 = "D:\\maze1.txt";
MyCopyFile.copyFile(s1, s2);
}
}
使用 DatagramChannel 数据包通道发送数据的实践案例
功能:
获取用户的输入数据,通过 DatagramChannel 数据报通道,将数据发送到远程的服务器。
客户端代码:
public class Client {
//Client发送信息
public void send() throws IOException {
DatagramChannel dChannel = DatagramChannel.open();
dChannel.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String s = sc.nextLine();
buf.put(s.getBytes());
buf.flip();
dChannel.send(buf, new InetSocketAddress("127.0.0.1", 9999));
buf.clear();
}
dChannel.close();
}
public static void main(String[] args) throws IOException {
new Client().send();
}
}
服务端代码
public class Server {
//服务端接收 用户发来的信息
public void receive() throws IOException {
DatagramChannel serverChannel = DatagramChannel.open();
//设置成非阻塞模式
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress("127.0.0.1", 9999));
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_READ);
while (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
if (next.isReadable()) {
SocketAddress receive = serverChannel.receive(buffer);
buffer.flip();
String s = new String(buffer.array(), 0, buffer.limit());
System.out.println(s);
buffer.clear();
}
}
iterator.remove();
}
//关闭选择器和通道
selector.close();
serverChannel.close();
}
public static void main(String[] args) throws IOException {
new Server().receive();
}
}
使用 NIO 实现 Discard 服务器的实践案例
功能:
仅仅读取客户端通道的输入数据,读取完成后直接关闭客户端通道;并且读取到的数据直接抛弃掉
Discard 服务器代码:
public class SocketServerDemo {
public void receive() throws IOException {
//创建服务器的通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
//开启选择器
Selector selector = Selector.open();
//绑定链接
serverSocketChannel.bind(new InetSocketAddress(9999));
//将通道的某个IO事件 注册到选择器上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//轮询所有就绪的IO事件
while (selector.select() > 0) {
//逐个获取IO事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
//逐个判断该IO事件是否为想要的
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
if (next.isAcceptable()) {
//如果为该事件为“连接就绪”事件,就获取客户端的链接
SocketChannel clientSocket = serverSocketChannel.accept();
//将客户端的链接设置为非阻塞模式
clientSocket.configureBlocking(false);
//将新的通道的可读事件,注册到选择器上
clientSocket.register(selector, SelectionKey.OP_READ);
} else if (next.isReadable()) {
//若IO事件为“可读事件”,读取数据
SocketChannel clientSocket = (SocketChannel) next.channel();
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int length = 0;
//读取事件 让后丢弃
while ((length = clientSocket.read(buffer)) > 0) {
buffer.flip();
String s = new String(buffer.array(), 0, length);
System.out.println(s);
buffer.clear();
}
clientSocket.close();
}
//移除选择键
iterator.remove();
}
}
serverSocketChannel.close();
}
public static void main(String[] args) throws IOException {
new SocketServerDemo().receive();
}
}
客户端的 DiscardClient 代码:
public class SocketClientDemo {
public void socketClient() throws IOException {
SocketChannel clientSocket = SocketChannel.open(new InetSocketAddress(9999));
//切换成非阻塞模式
clientSocket.configureBlocking(false);
//如果没有连接完成 就一直链接
while (!clientSocket.finishConnect()){
}
//执行到这里说明已经连接完成了
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello SocketService".getBytes());
buffer.flip();
clientSocket.write(buffer);
clientSocket.shutdownInput();
clientSocket.close();
}
public static void main(String[] args) throws IOException {
new SocketClientDemo().socketClient();
}
}
与 Java OIO 相比,Java NIO 编程大致的特点如下:
(1)在 NIO 中,服务器接收新连接的工作,是异步进行的。不像 Java 的 OIO 那样,服务器监听连接,是同步的、阻塞的。NIO 可以通过选择器(也可以说成:多路复用器),后续不断地轮询选择器的选择键集合,选择新到来的连接。
(2)在 NIO 中,SocketChannel 传输通道的读写操作都是异步的。如果没有可读写的数据,负责 IO 通信的线程不会同步等待。这样,线程就可以处理其他连接的通道;不需要像 OIO 那样,线程一直阻塞,等待所负责的连接可用为止。
(3)在 NIO 中,一个选择器线程可以同时处理成千上万个客户端连接,性能不会随着客户端的增加而线性下降。