上一篇博客我们介绍了NIO的核心原理、FileChannel和Buffer, 对Buffer的用法有了清晰的了解。上篇博客:
Java 输入与输出之 NIO【非阻塞式IO】【NIO核心原理】探索之【一】
本篇博客我们将继续来探索NIO,介绍如何使用SocketChannel和ServerSocketChannel来实现TCP协议的非阻塞套接字(Socket)网络通信程序编程。NIO的强大功能部分来自于Channel的非阻塞特性,套接字的某些操作可能会无限期地阻塞。例如,对accept()方法的调用可能会因为等待一个客户端连接而阻塞;对read()方法的调用可能会因为没有数据可读而阻塞,直到连接的另一端传来新的数据。总的来说,创建/接收连接或读写数据等I/O调用,都可能无限期地阻塞等待。
NIO的Channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现非阻塞式的信道。
下面这行代码设置了通道的非阻塞模式:
channel.configureBlocking(false)
在非阻塞式信道上调用一个方法总是会立即返回。这种调用的返回值指示了所请求的操作完成的程度。例如,在一个非阻塞式ServerSocketChannel上调用accept()方法,如果有连接请求来了,则返回客户端SocketChannel,否则返回null。
三、利用NIO编写网络通信程序(Selector的核心应用)
网络通信相关的三大核心组件:
- 通道(channel):负责管道节点的连接及数据的运输
- 缓冲区(buffer):负责数据的存取
- 选择器(selector):是selectableChannel的多路复用器,用于监控SelectableChannel的IO状况。
SocketChannel: 是通道Channel重要的实现类,用于TCP协议的网络通信,用于实现网络通讯客户端的应用程序;
ServerSocketChannel: 是通道Channel重要的实现类,用于监听TCP连接请求,主要用于实现网络通讯服务器端的应用程序。
阻塞与非阻塞
- 阻塞式
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()
时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不
能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会
阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,
当服务器端需要处理大量客户端时,性能急剧下降。 - 非阻塞式
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数
据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时
间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入
和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同
时处理连接到服务器端的所有客户端。
利用Java NIO网络通讯应用程序的操作步骤和示意图
Java 在使用NIO进行网络编程时,多线程应用程序的操作步骤和示意图如下所示:
处理步骤:
- 创建通道:
打开一个或多个通道,例如FileChannel、SocketChannel等。
创建缓冲区:为每个通道创建一个或多个缓冲区,用于读取或写入数据。 - 注册通道:
将通道注册到选择器,以便选择器可以监控这些通道的状态。 - 选择就绪通道:
选择器等待通道就绪事件,一旦有通道准备好进行I/O操作,选择器将通知应用程序。 - 读取/写入数据:
应用程序从通道读取数据或将数据写入通道,使用缓冲区来传输数据。
其中,通道和缓冲区是一对一的关系。每个通道都有一个与之对应的缓冲区,用于存储数据。
选择器(Selector)可以同时监视多个通道的状态。一个选择器可以绑定多个通道,以实现多路复用。
我们先来看一个网络通信应用的例程,客户端采用NIO实现,而服务端依旧使用IO实现。
采用NIO的客户端程序源代码:
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.TimeUnit;
public class SocketClientNIO {public static void client(){ByteBuffer buffer = ByteBuffer.allocate(1024);SocketChannel socketChannel = null;try{SocketAddress server = new InetSocketAddress("127.0.0.1",8080);socketChannel = SocketChannel.open();socketChannel.configureBlocking(false); //非阻塞式socketChannel.connect(server);//与服务端连接成功if(socketChannel.finishConnect()) {int i=0;while(true){TimeUnit.SECONDS.sleep(1);String info = "客户端发送信息,第 "+i++ +"条";buffer.clear();buffer.put(info.getBytes("GBK"));buffer.flip(); //切换while(buffer.hasRemaining()){System.out.println(buffer);socketChannel.write(buffer);}}}}catch (IOException | InterruptedException e){e.printStackTrace();}finally{try{if(socketChannel!=null){socketChannel.close();}}catch(IOException e){e.printStackTrace();}}}public static void main(String[] args) {SocketClientNIO.client();}
}
采用标准的IO实现的阻塞式网络通信服务器端程序的源代码:
package nio;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;public class SocketServerIO {public static void server(){ServerSocket serverSocket = null;InputStream in = null;try{serverSocket = new ServerSocket(8080);System.out.println("服务器开启,等待接收客户端连接");int recvMsgSize = 0;byte[] recvBuf = new byte[1024];while(true){Socket client = serverSocket.accept();SocketAddress clientAddr = client.getRemoteSocketAddress();System.out.println("接收到客户端,连接IP: "+clientAddr);in = client.getInputStream();while((recvMsgSize=in.read(recvBuf))!=-1){byte[] temp = new byte[recvMsgSize];System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);System.out.println("报文:"+new String(temp,"GBK"));}}}catch (IOException e){e.printStackTrace();}finally{try{if(serverSocket!=null){serverSocket.close();}if(in!=null){in.close();}}catch(IOException e){e.printStackTrace();}}}public static void main(String[] args) {SocketServerIO.server();}}
首先,编译后执行服务端应用程序,开启服务器服务,以接受客户端的请求;然后,编译执行客户端的应用程序。下面是服务端的测试效果:
未完待续
参考文献&博客:
- 参考文献之一
攻破JAVA NIO技术壁垒 - 参考文献之二
Java NIO全面详解(看这篇就够了)