preface
We often deal with I/O in the development process, many people in the process of learning I/O model and I/O programming process, many concepts may not be clear, especially such a high-level language like Java, it is the bottom operating system of a variety of I/O model encapsulation, so that we can very easily develop. But as a convenience, have you had a good look at the various I/O models in Java and how they relate to the operating system?
What is the I/O?
I/O refers to Input/Output in computer. For example, if you want to read data from disk to user space, the data transfer operation is an I/O operation, which is a file I/O operation. We browse various web pages every day. Each time we request a web page, the server sends us a packet of data over the network. The process of the application copying the data from the TCP buffer to user space is also an I/O, which is a network I/O. You can see that I/O is so important that it’s always there.
Liunx network I/O model
According to the classification of I/O models in UNIX network programming, UNIX provides five I/O models, which are as follows:
Blocking I/O model
This is the most traditional I/O model, which blocks while reading and writing data. As you can see from the diagram, the application process is blocked until the recvFROM call, the system call, and the data is copied from the kernel to user space. This model is suitable for the delay insensitive system with small amount of concurrency.
Non-blocking I/O model
The application process will continue to deal with the kernel through recvFROM calls until the data is ready, copying it into user space. If the recvFROM call returns an EWOULDBLOCK error when no data is found, this operation is called polling. This usually takes a lot of CPU time.
I/O multiplexing model
Liunx gives us a select/poll, which is a pipe, so we can block them on one of these two system calls, instead of blocking on a real I/O call, we block the select call when the data returns a readable condition, The data is copied to the application buffer through the recvFROM call. Multiplex I/O multiplexing is not inherently non-blocking, it has no advantage over the blocking I/O model, the fact that using SELECT requires two systems rather than just one call, I/O multiplexing actually has a slight disadvantage, it can handle more connections (waiting for multiple I/ OS to be ready)
Signal driven I/O model
We first enable signal-driven I/O on the socket, install a signal-handling function via the sigAction system call, the system call returns immediately, the process continues, the kernel generates a SIGIO signal notification when the packet is ready, and we read the datagram via the RecvFROM call. The advantage of the signal-driven I/O model is that we don’t block the process while the datagram arrives, we just wait for the signal handler to notify us
Asynchronous I/O model
Tell the kernel to notify us when an operation has started, including copying data from the kernel into its own buffer. The signal-driven model is where the kernel tells us when to start an I/O operation, while the asynchronous I/O model is where the kernel tells us when the I/O is finished
Comparison between synchronous AND asynchronous I/O
Synchronizing I/O operations: Causes the request process to block until the I/O operation is complete
Asynchronous I/O operations: Do not block the request process
Blocking I/O model, non-blocking I/O model, I/O multiplexing model, and signal-driven model are synchronous I/O models. Their true I/O operations block the process. Only asynchronous I/O models are asynchronous I/O operations
Java I/O model
History of Java I/O
Before JDK 1.4, all Socket communication based on Java uses the Blocking I/O mode. This request-response communication model simplifies the upper-level development, but there is a huge bottleneck in performance and reliability, and it does not support high concurrency and low latency
After JDK 1.4, the New NIO(New I/O) class library was introduced, and Java can also support non-blocking I/O. The java.nio package was added, providing a number of apis and libraries for asynchronous I/O development.
With the release of JDK 1.7, the original NIO class library was upgraded to provide AIO functionality, support for file-based asynchronous I/O operations and socket asynchronous I/O operations, among other features
BIO programming
A server using the BIO communication model normally listens for client connections through a separate Acceptor thread. After listening for client connection requests, it creates a new thread link for each client to process. After processing, it responds to the client via an output stream. The following code is used to analyze the BIO pattern. The client sends the message and the server sends the message back to us.
Server:
int port = 3000;
try(ServerSocket serverSocket = new ServerSocket(port)) {
Socket socket = null;
while (true) {
// The main program blocks on the Accept operation
socket = serverSocket.accept();
new Thread(newBioExampleServerHandle(socket)).start(); }}catch (Exception e) {
e.printStackTrace();
}
Copy the code
private Socket socket;
public BioExampleServerHandle(Socket socket) {
this.socket = socket;
}
@Override
public void run(a) {
try(BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)) {
String message = reader.readLine();
System.out.println("Client message received:" + message);
writer.println("answer: " + message);
} catch(Exception e) { e.printStackTrace(); }}Copy the code
Client:
String host = "127.0.0.1";
int port = 3000;
try(Socket socket = new Socket(host, port);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)) {
Scanner input = new Scanner(System.in);
System.out.println("Type in what you want to say:");
String message = input.nextLine();
writer.println(message);
String answer = reader.readLine();
System.out.println(answer);
} catch (Exception e) {
e.printStackTrace();
}
Copy the code
The running results are as follows:
Client:
Every time a connection comes in we need a new thread to process it
Our program would not be able to meet the performance requirements if we had high concurrency, and we lacked management of thread creation
int port = 3000;
ThreadPoolExecutor socketPool = null;
try(ServerSocket serverSocket = new ServerSocket(port)) {
Socket socket = null;
int cpuNum = Runtime.getRuntime().availableProcessors();
socketPool = new ThreadPoolExecutor(cpuNum, cpuNum * 2.1000,
TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1000));
while (true) {
socket = serverSocket.accept();
socketPool.submit(newBioExampleServerHandle(socket)); }}catch (Exception e) {
e.printStackTrace();
} finally {
socketPool.shutdown();
}
Copy the code
Can see that every time a new connection access, we will be delivered to his thread pool to process, because we set the thread pool size and blocking queue size, so in the case of concurrent collapse will not lead to service, but if the concurrency is greater than the blocking queue size, or the server processing when the connection is slow, blocking queue cannot continue to process, can cause the client connection timeout, The user experience is affected.
NIO programming
NIO makes up for synchronous blocking I/O by providing high-speed, block-oriented I/O. Here are some concepts:
Buffer: The Buffer is used to interact with the NIO channel. Data is read from the Channel to the buffer and written from the buffer to the Channel, and its main purpose is to interact with the Channel.
Channel: A Channel is a Channel through which data can be read and written. A Channel is bidirectional. A Channel can be used for reading, writing, or both.
Selector: The Selector will continuously poll the channes registered on it, and if there is a new connection read or write event on the Channel, it will be polling out. A Selector can register multiple channels, and only one thread is responsible for polling the Selector, so it can support thousands of connections. It can be said to provide a good support for the development of high concurrency servers.
Let’s demonstrate the use of NIO in actual code:
Server code:
int port = 3000;
ServerSocketChannel socketChannel = null;
Selector selector = null;
try {
selector = Selector.open();
socketChannel = ServerSocketChannel.open();
// Set the connection mode to non-blocking
socketChannel.configureBlocking(false);
socketChannel.socket().bind(new InetSocketAddress(port));
// Register the channel on the selector and listen for connection events
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// Set selector to scan all channels every second
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterable = selectionKeys.iterator();
SelectionKey key = null;
while (iterable.hasNext()) {
key = iterable.next();
// Process the key
try {
handlerKey(key, selector);
} catch (Exception e) {
if (null! = key) { key.cancel();if (null! = key.channel()) { key.channel().close(); } } } } } }catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null! = selector) { selector.close(); }if (null != socketChannel) {
socketChannel.close();
}
} catch (Exception e) {
throw newRuntimeException(e); }}Copy the code
HandlerKey code:
private void handlerKey(SelectionKey key, Selector selector) throws IOException {
if (key.isValid()) {
// Determine whether it is a connection request and process all connection requests
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
// Register the channel on the selector and listen for read events
channel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
// Allocate a 1024 byte buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readBytes = channel.read(byteBuffer);
if (readBytes > 0) {
// Switch from write mode to read mode
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String message = new String(bytes, "UTF-8");
System.out.println("Client message received:" + message);
// Reply to the client
message = "answer: " + message;
byte[] responseByte = message.getBytes(); ByteBuffer writeBuffer = ByteBuffer.allocate(responseByte.length); writeBuffer.put(responseByte); writeBuffer.flip(); channel.write(writeBuffer); }}}}Copy the code
Client code:
int port = 3000;
String host = "127.0.0.1";
SocketChannel channel = null;
Selector selector = null;
try {
selector = Selector.open();
channel = SocketChannel.open();
channel.configureBlocking(false);
if (channel.connect(new InetSocketAddress(host, port))) {
channel.register(selector, SelectionKey.OP_READ);
write(channel);
} else {
channel.register(selector, SelectionKey.OP_CONNECT);
}
while (true) {
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
SelectionKey key = null;
while (iterator.hasNext()) {
try {
key = iterator.next();
handle(key, selector);
} catch (Exception e) {
e.printStackTrace();
if (null! = key.channel()) { key.channel().close(); }if (null! = key) { key.cancel(); } } } } }catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null! = channel) { channel.close(); }if (null != selector) {
selector.close();
}
} catch (Exception e) {
throw newRuntimeException(e); }}Copy the code
The write method:
private void write(SocketChannel channel) throws IOException {
Scanner in = new Scanner(System.in);
System.out.println("Type in what you want to say:");
String message = in.next();
byte[] bytes = message.getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);
byteBuffer.put(bytes);
byteBuffer.flip();
channel.write(byteBuffer);
}
Copy the code
Handle method:
private void handle(SelectionKey key, Selector selector) throws IOException {
if (key.isValid()) {
SocketChannel channel = (SocketChannel) key.channel();
if (key.isConnectable()) {
if(channel.finishConnect()) { channel.register(selector, SelectionKey.OP_READ); write(channel); }}else if (key.isReadable()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readBytes = channel.read(byteBuffer);
if (readBytes > 0) {
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String message = new String(bytes, "UTF-8");
System.out.println(message);
} else if (readBytes < 0) { key.cancel(); channel.close(); }}}}Copy the code
NIO is much more complex than BIO, but the advantage of NIO is that it is worth trying. Instead of a BIO client connection that is asynchronous, we can register an OP_CONNECT event and wait for the result without being blocked synchronously. Channel reads and writes asynchronously, there’s no waiting for data, it doesn’t wait for direct return, we don’t need to create threads frequently to handle client connections compared to BIO, we can handle multiple client connections through one Selector, and it’s very performance friendly for high performance server development
AIO programming
NIO2.0 introduces the concept of asynchronous Channels, providing asynchronous file Channels and asynchronous socket Channels implementation, we can use the Future class to represent the results of asynchronous operations, we can also perform asynchronous operations when passing a Channels, implement the CompletionHandler interface for the operation callback. The sample code is as follows
Server:
int port = 3000;
AsynchronousServerSocketChannel socketChannel = null;
try {
socketChannel = AsynchronousServerSocketChannel.open();
socketChannel.bind(new InetSocketAddress(port));
// Receive a client connection, passing an AcceptCompletionHandler as a callback to receive the connection message
socketChannel.accept(socketChannel, new AcceptCompletionHandler());
Thread.currentThread().join();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != socketChannel) {
socketChannel.close();
}
} catch (Exception e1) {
throw newRuntimeException(e1); }}Copy the code
AcceptCompletionHandler class:
public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel.AsynchronousServerSocketChannel> {
@Override
public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
// Continue to accept connection requests from other clients, forming a loop
attachment.accept(attachment, this);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// Call the read operation to do an asynchronous read, passing the ReadCompletionHandler as a callback
result.read(byteBuffer, byteBuffer, new ReadCompletionHandler(result));
}
@Override
public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
// Exception failure is handled here}}Copy the code
ReadCompletionHandler class
public class ReadCompletionHandler implements CompletionHandler<Integer.ByteBuffer> {
private AsynchronousSocketChannel channel;
public ReadCompletionHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
}
@Override
public void completed(Integer result, ByteBuffer byteBuffer) {
try {
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String message = new String(bytes, "UTF-8");
System.out.println("Client message received: :" + message);
write(message);
} catch(UnsupportedEncodingException e) { e.printStackTrace(); }}@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (Exception e) {
throw newRuntimeException(e); }}private void write(String message) {
message = "answer: " + message;
byte[] bytes = message.getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);
byteBuffer.put(bytes);
byteBuffer.flip();
channel.write(byteBuffer, byteBuffer, newWriteCompletionHandler(channel)); }}Copy the code
Client:
int port = 3000;
String host = "127.0.0.1";
AsynchronousSocketChannel channel = null;
try {
channel = AsynchronousSocketChannel.open();
channel.connect(new InetSocketAddress(host, port), channel, new AioClientHandler());
Thread.currentThread().join();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != channel) {
channel.close();
}
} catch (Exception e) {
throw newRuntimeException(e); }}Copy the code
AioClientHandler class (I use nested classes here because the client is simpler) :
public class AioClientHandler implements CompletionHandler<Void.AsynchronousSocketChannel> {
@Override
public void completed(Void result, AsynchronousSocketChannel channel) {
Scanner in = new Scanner(System.in);
System.out.println("Type in what you want to say:");
String message = in.next();
byte[] bytes = message.getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);
byteBuffer.put(bytes);
byteBuffer.flip();
channel.write(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
// Determine if you have finished writing if you have not continued writing
if (buffer.hasRemaining()) {
channel.write(buffer, buffer, this);
} else {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
channel.read(readBuffer, readBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
try {
attachment.flip();
byte[] bytes1 = new byte[attachment.remaining()];
attachment.get(bytes1);
String message = new String(bytes1, "UTF-8");
System.out.println(message);
System.exit(1);
} catch(UnsupportedEncodingException e) { e.printStackTrace(); }}@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (Exception e) {
throw newRuntimeException(e); }}}); }}@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (Exception e) {
throw newRuntimeException(e); }}}); }@Override
public void failed(Throwable exc, AsynchronousSocketChannel attachment) {}Copy the code
AIO is simpler than BIO because we don’t need to create a separate I/O thread to handle read and write operations. AsynchronousSocketChannel, AsynchronousServerSocketChannel JDK thread pool bottom is responsible for correction of read and write operations.
contrast
Synchronous blocking I/O(BIO) | Pseudo asynchronous I/O | Non-blocking I/O (NIO) | Asynchronous I/O (AIO) | |
---|---|---|---|---|
Whether blocking | is | is | no | no |
Whether the synchronization | is | is | is | No (Asynchronous) |
Programmer friendliness | simple | simple | It’s very difficult to | More difficult |
reliability | Very poor | poor | high | high |
throughput | low | In the | high | high |
conclusion
By learning Lunix low-level I/O model and JavaI/O model, we found that the upper layer is just the abstraction and encapsulation of the lower layer, BIO is actually the implementation of blocking I/O model, NIO is the implementation of I/O multiplexing model, AIO is the implementation of signal-driven I/O, understand the underlying I/O model, in the actual development should be very easy. If you think it is good, please like it. If there are bugs, please criticize and correct them. Your appreciation and criticism are good partners on the way to progress.