Preface
Last mentioned to improve our RPC framework, this week take the time to explore the native NIO non-blocking network programming ideas that JDK provides to us.NIO libraries were introduced in JDK 1.4.NIO makes up for the deficiency of the original I/O by providing high-speed, block-oriented I/O in standard Java code.
Main differences between BIO and NIO
1. Flow-oriented and buffer-oriented
The first big difference between Java NIO and BIO is that BIO is stream-oriented and NIO is buffer-oriented.Java IO stream-oriented means that one or more bytes are read from the stream at a time until all bytes are read, and they are not cached anywhere.In addition, it cannot move the data in the stream back and forth.If you need to move the data read from the stream back and forth, you need to cache it in a buffer first.The Java NIO buffer-oriented approach is slightly different.Data is read into a buffer that it processes later and can be moved back and forth as needed.This increases the flexibility of the process.However, you also need to check that the buffer contains all the data you need to process.Also, make sure that when more data is read into the buffer, it does not overwrite data that has not been processed in the buffer.
2. Blocking and non-blocking
Various streams of the Java BIO are blocked.This means that when a thread calls read() or write(), the thread is blocked until some data is read or written completely.The thread cannot do anything else during this time.
The Java NIO's non-blocking mode allows a thread to send requests to read data from a channel, but it only gets data that is currently available and nothing if no data is currently available.Instead of keeping the thread blocked, the thread can continue to do other things until the data becomes readable.The same is true for non-blocking writing.A thread requests to write some data to a channel, but does not have to wait for it to write completely. The thread can do something else at the same time.Threads typically use non-blocking IO idle time to perform IO operations on other channels, so a single thread can now manage multiple input and output channels.
3. NIO-specific Selector mechanism
The Java NIO selector allows a single thread to monitor multiple input channels. You can register multiple channels to use one selector, and then use a separate thread to "select" channels: those channels already have inputs that can be processed, or select channels that are ready for writing.This selection mechanism makes it easy for a single thread to manage multiple channels.
Today, based on the above understanding, we will implement the network programming of an end-to-end non-blocking IO.
Actual Design
Client Section
/**
* @author andychen https://blog.51cto.com/14815984
* @description: NIO Client Core Processor
*/
public class NioClientHandler implements Runnable {
//Server Host
private final String host;
//Service Port
private final int port;
/**Define NIO selectors: used to register and monitor events
* Select the type of event to listen for: OP_READ Read Event / OP_WRITE Write Event
* OP_CONNECT Client Connection Event / OP_ACCEPT Server Receives Channel Connection Events
*/
private Selector selector = null;
//Define Client Connection Channels
private SocketChannel channel = null;
//Is the running state activated
private volatile boolean activated=false;
public NioClientHandler(String host, int port) {
this.port = port;
this.host = host;
this.init();
}
/**
* Processor Initialization
* Responsible for connection preparation
*/
private void init(){
try {
//Create and open selector
this.selector = Selector.open();
//Establish and open listening channel
this.channel = SocketChannel.open();
/**
* Set channel communication mode to non-blocking, NIO defaults to blocking
*/
this.channel.configureBlocking(false);
//Activate Running State
this.activated = true;
} catch (IOException e) {
e.printStackTrace();
this.stop();
}
}
/**
* Connect to Server
*/
private void connect(){
try {
/**
* Connect to the server because the communication mode was set to be non-blocking
* This immediately returns whether the TCP handshake has been established.
*/
if(this.channel.connect(new InetSocketAddress(this.host, this.port))){
//After the connection is established, read event concerns are registered on the channel, and the client triggers processing as soon as it receives data
this.channel.register(this.selector, SelectionKey.OP_READ);
}
else{
//If the connection handshake is not established, continue to focus on the connection event on the channel and continue with subsequent processing logic once the connection is established
this.channel.register(this.selector, SelectionKey.OP_CONNECT);
}
} catch (IOException e) {
e.printStackTrace();
this.stop();
}
}
/**
* Selector Event Iterative Processing
* @param keys Selector Event KEY
*/
private void eventIterator(Set<SelectionKey> keys){
SelectionKey key = null;
//Iterators are used here because key s are removed when iteration is required
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()){
key = it.next();
//Remove event key first to avoid multiple processing
it.remove();
//Handle Iteration Events
this.proccessEvent(key);
}
}
/**
* Handle what happened
* @param key Selector Event KEY
*/
private void proccessEvent(SelectionKey key){
//Handle only valid event types
if(key.isValid()){
try {
//Handle on Event Channel
SocketChannel socketChannel = (SocketChannel) key.channel();
/**Handle connection ready events
* */
if(key.isConnectable()){
//Detect connection completion to avoid NotYetConnectedException exception
if(socketChannel.finishConnect()){
System.out.println("Has completed connection with server..");
/**
* Read events are of particular concern to the channel, and write events of NO are not.
* Reason: Write buffers are considered idle most of the time and are frequently selected by selectors (which wastes CPU resources).
* Therefore, they should not be registered frequently;
* Only after a portion of the data has been flushed out of the buffer when the written data exceeds the free space of the write buffer,
* Notify the application to write when space is available;
* Write events should be closed immediately after the application has finished writing
*/
socketChannel.register(this.selector, SelectionKey.OP_READ);
}else{//If a connection is not established here it is generally considered a network or other reason to temporarily exit
this.stop();
}
}
/**
* Handle Read Events
*/
if(key.isReadable()){
//Create a memory buffer where JVM heap memory is used
ByteBuffer buffer = ByteBuffer.allocate(Constant.BUF_SIZE);
//Read data from channel to buffer
int length = socketChannel.read(buffer);
if(0 < length){
/**
* Read-write conversion, NIO fixed paradigm
*/
buffer.flip();
//Get buffer free space
int size = buffer.remaining();
byte[] bytes = new byte[size];
//Read Buffer
buffer.get(bytes);
//Get Buffer Data
String result = new String(bytes,"utf-8");
System.out.println("Recevied server message: "+result);
}else if(0 > length){
//Cancel focus on current event, close channel
key.cancel();
socketChannel.close();
}
}
} catch (Exception e) {
key.cancel();
if(null != key.channel()){
try {
key.channel().close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
}
}
}
/**
* Write data to peer
* @param data
*/
public void write(String data){
try {
byte[] bytes = data.getBytes();
ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
//Put data in write buffer
buffer.put(bytes);
buffer.flip();
this.channel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Stop running
*/
public void stop(){
this.activated = false;
System.exit(-1);
}
/**
* Client Communication Business Kernel Implementation
*/
@Override
public void run() {
//Set up a server connection
this.connect();
//Continuously monitor events
while (this.activated){
try {
//Listen for events to occur, return directly if they occur; otherwise block until the event occurs
this.selector.select();
} catch (IOException e) {
e.printStackTrace();
this.stop();
}
//Get the type of event that occurred
Set<SelectionKey> keys = this.selector.selectedKeys();
//Iterate event handling
this.eventIterator(keys);
}
//Close selector
if(null != this.selector){
try {
this.selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
this.stop();
}
}
/**
* @author andychen https://blog.51cto.com/14815984
* @description: NIO Client Launcher
*/
public class NioClientStarter {
private static NioClientHandler clientHandler = null;
/*Start Running Client*/
public static void main(String[] args) {
try {
clientHandler = new NioClientHandler(Constant.SERV_HOST, Constant.SERV_PORT);
new Thread(clientHandler).start();
} catch (Exception e) {
e.printStackTrace();
}
/**
* Send real-time data from console to peer
*/
Scanner scanner = new Scanner(System.in);
while (true){
String data = scanner.next();
if(null != data && !"".equals(data)){
clientHandler.write(data);
}
}
}
}
Service End Section
/**
* @author andychen https://blog.51cto.com/14815984
* @description: NIO Server-side Core Processor
*/
public class NioServerHandler implements Runnable{
private final int port;
//Define selector
private Selector selector = null;
/**
* Defining a service-side channel: a client-like approach
*/
private ServerSocketChannel channel = null;
//Is Server Run Activated
private volatile boolean activated = false;
public NioServerHandler(int port) {
this.port = port;
this.init();
}
/**
* Initialize Processor
* Responsible for preparing for running monitoring and receiving
*/
private void init(){
try {
//Create and open selector
this.selector = Selector.open();
//Create and open listening channel
this.channel = ServerSocketChannel.open();
/**
* Set channel communication mode to non-blocking (NIO defaults to blocking)
*/
this.channel.configureBlocking(false);
//Bind listening service ports
this.channel.socket().bind(new InetSocketAddress(this.port));
/**
* Events of first interest registered on a server-side channel
*/
this.channel.register(this.selector, SelectionKey.OP_ACCEPT);
//Set Run State Activation
this.activated = true;
} catch (IOException e) {
e.printStackTrace();
this.stop();
}
}
/**
* Out of Service
*/
public void stop(){
this.activated = false;
try {
//Close selector
if(null != this.selector){
if(this.selector.isOpen()){
this.selector.close();
}
this.selector = null;
}
//Close Channel
if(null != this.channel){
if(this.channel.isOpen()){
this.channel.close();
}
this.channel = null;
}
} catch (IOException e) {
e.printStackTrace();
}
System.exit(-1);
}
/**
* Processing events iteratively
* @param keys The type of event that occurred
*/
private void eventIterator(Set<SelectionKey> keys){
//SelectionKey key = null;
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()){
SelectionKey key = it.next();
/**
* Remove from iterator first to avoid repetition later
*/
it.remove();
//Handle Events
this.proccessEvent(key);
}
}
/**
*
* @param key Select the event KEY to execute
*/
private void proccessEvent(SelectionKey key){
//Processing only for valid event KEY
if(key.isValid()){
try {
/**
* Handle Channel Receive Data Events
*/
if(key.isAcceptable()){
/**
* Note that the channel to receive events here is the server-side channel
*/
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
//Receive Client Socket
SocketChannel channel = serverChannel.accept();
//Set it as non-blocking
channel.configureBlocking(false);
//Then register the read events for this channel
channel.register(this.selector, SelectionKey.OP_READ);
System.out.println("Build connection with client..");
}
/**
* Handle Read Events
*/
if(key.isReadable()){
System.out.println("Reading client data...");
SocketChannel channel = (SocketChannel) key.channel();
//Open up memory space to receive data
ByteBuffer buffer = ByteBuffer.allocate(Constant.BUF_SIZE);
//Read data into buffer
int length = channel.read(buffer);
if(0 < length){
//Read-Write Switching
buffer.flip();
//More Buffer Data to Build Converted Byte Array
byte[] bytes = new byte[buffer.remaining()];
//Read byte data from buffer
buffer.get(bytes);
//Decode data
String data = new String(bytes, "utf-8");
System.out.println("Recevied data: "+data);
//Send receive response to peer
String answer = "Server has recevied data:"+data;
this.reply(channel, answer);
}else if(0 > length){
//Cancel Handled Events
key.cancel();
channel.close();
}
}
/**
* Handle write events
*/
if(key.isWritable()){
SocketChannel channel = (SocketChannel) key.channel();
//Get buffer for write event
ByteBuffer buffer = (ByteBuffer) key.attachment();
//If there is data in the buffer, brush to the opposite end
if(buffer.hasRemaining()){
int length = channel.write(buffer);
System.out.println("Write data "+length+" byte to client.");
}else{
//Continue listening for read events if there is no data
key.interestOps(SelectionKey.OP_READ);
}
}
} catch (IOException e) {
key.cancel();
e.printStackTrace();
}
}
}
/**
* Answer to End
* @param msg Answer message
*/
private void reply(SocketChannel channel, String msg){
//Message Encoding
byte[] bytes = msg.getBytes();
//Open write buffer
ByteBuffer buffer = ByteBuffer.allocate(Constant.BUF_SIZE);
//Write data to buffer
buffer.put(bytes);
//Switch to Read Events
buffer.flip();
/**
* In order to avoid write-empty or write-overflow buffers, write event listening is established while retaining the previous read listening.
* buffer passed in as a listening attachment for write operations
*/
try {
channel.register(this.selector, SelectionKey.OP_WRITE |SelectionKey.OP_READ, buffer);
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
/**
* Server listens for running core business implementations
*/
@Override
public void run() {
while (this.activated){
try {
/**
* Run to this method to block until an event occurs before returning
* */
this.selector.select();
//Get monitored events
Set<SelectionKey> keys = this.selector.selectedKeys();
//In iterators, handle different events
this.eventIterator(keys);
} catch (IOException e) {
e.printStackTrace();
this.stop();
}
}
}
}
/**
* @author andychen https://blog.51cto.com/14815984
* @description: NIO Network Programming Server Start Class
*/
public class NioServerStart {
/**
* Run Server Side Monitoring
* @param args
*/
public static void main(String[] args) {
String serverTag = "server: "+Constant.SERV_PORT;
NioServerHandler serverHandler = null;
try {
serverHandler = new NioServerHandler(Constant.SERV_PORT);
new Thread(serverHandler, serverTag).start();
System.out.println("Starting "+serverTag+" listening...");
} catch (Exception e) {
e.printStackTrace();
if(null != serverHandler){
serverHandler.stop();
}
}
}
}
Multiple Verification Results
summary
From the above facts, we can see that NIO network programming implementation is slightly more complex than BIO.Buffer-oriented mechanisms are indeed much more flexible than flow-oriented mechanisms; services run more smoothly than blocked IO; and unique selector mechanisms allow NIOs to support larger concurrencies, but with slightly higher learning and development costs, they can be used selectively in projects.
At present, the excellent IO framework which is used a lot is not Netty. Many excellent RPC framework's bottom level is also based on Netty expansion and development.Next time, we'll show you the beauty of Netty's web programming.