NIO, No blocking IO, non blocking io. It means that IO operations will not cause blocking, and NIO can be used to deal with high concurrency and high access scenarios. In contrast, BIO, Blocking IO, is standard blocking io.
BIO
A classic BIO example is the simplest C/S model program. The following is the code:
public class BIODemoServer { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(); ss.bind(new InetSocketAddress(9999)); System.out.println("wait for connecting ..."); Socket socket = ss.accept(); // Blocking occurs here System.out.println("a connector is accessed!"); InputStream input = socket.getInputStream(); input.read(); System.out.println("we had some data!"); } } public class BIODemoClient { public static void main(String[] args) throws IOException { Socket socket = new Socket(); System.out.println("trying to connect ..."); socket.connect(new InetSocketAddress("127.0.0.1", 9999)); System.out.println("connect success!"); } }
Both accept() and connect() are blocking methods. When called, a blocking will be started. The accept() method will release the blocking when waiting for the client to connect; The connect() method releases the block when connecting to the server.
Similarly, getInputStream() of ServerSocket gets the blocked IO stream. After calling the read() method, if the client does not output data to the server for a long time, the server will remain stuck here until the client writes data to the server.
In fact, the write() method of the client's OutputStream is also blocked. If the server does not receive data. Then, the write() method will write data out (usually the buffer of the network card device) until it is written to a certain amount, and no party receives it, so blocking occurs.
NIO
Compared with BIO, NIO can realize non blocking io. That is, when IO occurs, no blocking will occur.
The core of NIO is the Channel in which IO is implemented. The characteristic of Channel is that it can be turned on or off. The open Channel is connected to an entity (device, file, network socket, etc.) and can read and write data through IO operations on the Channel. And you can configure that the whole read-write process is not blocked.
Channel allows multiple threads to access at the same time, and can save the whole process, which is thread safe.
Channel is an interface in java. The most used implementation classes are serversocketchannel and socketchannel. The two underlying classes are based on TCP protocol.
The following is the simplest example of NIO usage:
public class NIODemoServer { public static void main(String[] args) throws IOException { ServerSocketChannel ssc = ServerSocketChannel.open(); // To achieve non blocking, this statement must be executed, otherwise it is still blocked ssc.configureBlocking(false); ssc.bind(new InetSocketAddress(8888)); System.out.println("a client has been connected!"); ssc.accept(); // This is no longer blocked } } public class NIODemoClient { public static void main(String[] args) throws IOException { SocketChannel sc = SocketChannel.open(); sc.configureBlocking(false); // Set non blocking sc.connect(new InetSocketAddress("127.0.0.1", 8888)); // This is no longer blocked System.out.println("connect success!"); } }
When the Channel performs IO operations, the metadata used is ByteBuffer, which is a byte buffer. Is a finite sequence of elements of a specific basic type. It is itself an abstract class that calls the allocate(int) static method to return the object.
It contains three important attributes:
- capacity: the maximum number of elements that the cache can hold
- Limit: limit the maximum number of data that can be fetched from the cache, starting from 0. If it exceeds, throw Java nio. Bufferunderflowexception exception.
- Position: current position. It refers to the location from which the data is extracted.
ByteBuffer is actually the data carrier in the Channel. Just like cars in tunnels, people are data, cars are ByteBuffer, and tunnels are channels.
The allocate(int) method creates a ByteBuffer of a specified length.
ByteBuffer has a put(byte) method, which accepts one byte of data, which means writing the data to the cache. To get the data, call the get() method.
Every time a byte data is put into the byte buffer, position will be incremented by 1. The next put or get continues from position, so the following code outputs two zeros instead of 1 (all data in the cache is 0 by default)
public class ByteBufferDemo { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(10); byte a = 1; byte b = 2; buffer.put(a); buffer.put(b); System.out.println(buffer.get()); System.out.println(buffer.get()); } }
If you want to fetch a and b correctly, you can modify the position position through the position(int) method, and limit the number of fetched data through the limit(int) method:
public class ByteBufferDemo { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(10); byte a = 1; byte b = 2; buffer.put(a); buffer.put(b); buffer.position(0); // Start with the first data buffer.limit(2); // Only two data can be fetched System.out.println(buffer.get()); System.out.println(buffer.get()); // System.out.println(buffer.get()); // Report exception } }
Consider why the third output throws an exception. Because the limit limits that only two byte s of data with position 0-1 can be retrieved. The third output attempts to fetch the data with position 2, and naturally throws an exception.
ByteBuffer also has some interesting methods:
- flip(): reverse the cache, set limit to position and position to 0.
- clear(): clear the cache, set position to 0 and limit to capacity. It can be seen that this method does not really empty the cache, but sets the state of the cache to the initial value.
- hasRemaining(): Returns whether there is still valid data left in the cache. Its essence is to judge whether position is less than limit.
Then, the code for traversing the valid data in the cache is as follows:
public class ByteBufferDemo { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(10); buffer.put((byte) 1); buffer.put((byte) 2); buffer.put((byte) 3); buffer.flip(); while (buffer.hasRemaining()) { System.out.println(buffer.get()); } } }
We can directly encapsulate a byte array into a byte buffer. Call the static method warp(byte []) to return a ByteBuffer whose size is just the size of the byte array.
Now that we have a car (ByteBuffer) and know how to load data into the car (put method), the next step is how to make the car pass through the tunnel.
The accept() method of ServerSocketChannel will return an instance of SocketChannel. Call the read(ByteBuffer) of this instance to read the data.
Note whether the returned instance is non blocking by default, or whether you need to call configure blocking (Boolean) to set whether it is blocked.
However, because aceept() is not blocked, null will be returned if no client connects to the server. You can use this to determine whether there is a client connection.
Or we can artificially set a block at accept() to ensure that a client is connected to the server.
public class NIODemoServer { public static void main(String[] args) throws IOException { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.bind(new InetSocketAddress(8888)); // To achieve non blocking, this statement must be executed, otherwise it is still blocked ssc.configureBlocking(false); System.out.println("wait for client..."); SocketChannel sc = null; while (sc == null) { // Manual blocking sc = ssc.accept(); } System.out.println("a client has been connected!"); ByteBuffer buffer = ByteBuffer.allocate(10); sc.configureBlocking(false); // Set the channel to non blocking sc.read(buffer); System.out.println("We have read some data!"); System.out.println("data: " + new String(buffer.array())); } } public class NIODemoClient { public static void main(String[] args) throws IOException { SocketChannel sc = SocketChannel.open(); sc.configureBlocking(false); // Set non blocking System.out.println("trying to connect..."); sc.connect(new InetSocketAddress("127.0.0.1", 8888)); if (!sc.isConnected()) { // If the connection is not successful, continue the connection sc.finishConnect(); } System.out.println("connect Server success!"); // Start writing data to the server ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes()); sc.write(buffer); System.out.println("write success!"); while (true); // Keep connected } }
Selector
Once NIO is used, a little carelessness will produce various exceptions. Therefore, we need a design pattern that can deal with the situation of NIO.
The simplest way is that when a client connects to the server, the server starts a separate thread to process the client's request.
This model has the following disadvantages:
- If the amount of concurrency is large, it will lead to too many threads on the server, which is likely to lead to downtime.
- If the client does not perform any operation after connecting to the server, the connection will remain and will occupy too much useless CPU scheduling.
- Then, the threads that really need to be processed cannot be serviced in time.
In order to solve the problem of idle threads, we can set idle threads to blocking state. In this way, the CPU will not schedule such threads during scheduling. Therefore, it is necessary to introduce an event monitoring mechanism.
A selector (multiplexer) is introduced here. It plays the role of event monitoring.
The Selector listens to all threads connected to the server in real time. If the thread doesn't do anything, the Selector will let the thread sleep. If the thread has the behavior of accept(),connect(),write(),read(), the Selector will wake the thread up and perform the operation.
Selector reduces the pressure on the thread scheduler to a certain extent, so that the CPU can do meaningful things most of the time.
In the case of processing short requests, in order to solve the problem of too many threads, one thread can be used to process all client requests. NIO technology can be used to realize multi-user concurrency, and Selector is used to judge the occurrence of events.
java has provided the Selector. Instantiating the Selector uses the Selector's open() static method.
To enable the Selector to provide listening, you need to register events. These events are defined in the SelectionKey and are int constants. Common events are:
- OP_ACCEPT: indicates to receive an event
- OP_CONNCT: indicates a connection event
- OP_WRITE: indicates a write event
- OP_READ: indicates a read event
The Selector can be registered through the register(Selector,int) method of ServerSocketChannel.
Calling the select() method of the Selector will generate a block until a registered listening event is triggered.
When any event is triggered, we can get the collection of trigger events through selectedKeys(). This represents a collection of clients connected to the server. Then traversing this collection will achieve the function of traversing all connected clients.
This collection holds the SelectionKey object. Each object can determine whether an event has occurred (the above four events). Through this judgment, we can deal with different events.
After the accept() event occurs, you need to call the channel() method of SelectionKey to get the ServerSocketChannel object of the current session (which needs to be coerced) and then call accept().
The handling skills of other events are similar, but the channel() method will return the socketchannel, which is the socketchannel obtained through ServerSocketChannel after accept(), so there is no need to set non blocking here.
In this example, after the client connects to the server (that is, after the ACCEPT event occurs), the server can WRITE data to the client. That is, the WRITE event of the client that maintains the connection always occurs.
Because of NIO, the server can continuously write data to the client. Therefore, when writing data, we need to do some processing to prevent the server from over writing data to the client.
Through some simple binary calculations, the events that have occurred can be removed after the WRITE or READ event occurs. Here, you can get the existing event through the interestOps() method of SelectionKey, and then perform a bitwise and calculation with the negation result of the current event.
public class Server { // implement a Selector Server public static void main(String[] args) throws IOException { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.configureBlocking(false); // use no-blocking. ssc.bind(new InetSocketAddress(6666)); // create a selector Selector selector = Selector.open(); // register the selector and event ACCEPT. ssc.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select(); // block here ... wait for client connect ... // get all events' set. Set<SelectionKey> set = selector.selectedKeys(); Iterator<SelectionKey> ite = set.iterator(); while (ite.hasNext()) { SelectionKey key = ite.next(); if (key.isAcceptable()) { // ACCEPT event. // the serverSocketChannel for new client. ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); SocketChannel socketChannel = null; while (socketChannel == null) { socketChannel = serverSocketChannel.accept(); } socketChannel.configureBlocking(false); System.out.println("Thread " + Thread.currentThread().getId() + " is now servicing a client ..."); // register WRITE and READ event for socketChannel. socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); } if (key.isReadable()) { // READ event. SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(10); socketChannel.read(buffer); System.out.println("Server has read some data: " + new String(buffer.array())); // remove current event. socketChannel.register(selector, key.interestOps() & ~SelectionKey.OP_WRITE); } // WRITE event, this will happen after connecting success. if (key.isWritable()) { SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.wrap("nihao!".getBytes()); socketChannel.write(buffer); System.out.println("Server has wrote some data to client."); // remove current event. socketChannel.register(selector, key.interestOps() & ~SelectionKey.OP_WRITE); } // delete this event, to prevent repeated calls to the event. ite.remove(); } } } } public class Client { public static void main(String[] args) throws IOException, InterruptedException { SocketChannel sc = SocketChannel.open(); sc.configureBlocking(false); sc.connect(new InetSocketAddress( "127.0.0.1", 6666)); while (!sc.isConnected()) { sc.finishConnect(); } Thread.sleep(2000); // write data to Server. System.out.println("Write some data to Server..."); ByteBuffer buffer = ByteBuffer.wrap("hello!".getBytes()); sc.write(buffer); Thread.sleep(2000); // read data from Server. ByteBuffer receive = ByteBuffer.allocate(20); sc.read(receive); System.out.println("Client received some data: " + new String(receive.array())); while (true); } }
Due to the non blocking characteristics of NIO, it is very expensive to realize normal data interaction. The above code delays the acquisition of data through the sleep() method, but it is impossible to use this in practice