preface

Recently, I have been reading BIO and NIO in Java, and I have read many blogs. I found that all kinds of concepts about NIO are very complete, but after reading the whole thing, I still have a vague understanding of NIO, so this article will not mention many concepts. Instead, I will write some of my own opinions about NIO from a practical point of view. I will have a better understanding of the concept when I look back at the concept from the height of the practice.

Implement a simple single thread server

To understand BIO and NIO, we should first implement a simple, single-threaded server that is not too complex.

  • Why use a single thread as a demonstration

    Because one of the differences between BIO and NIO is well illustrated in a single-threaded environment, OF course I will also demonstrate BIO’s so-called one-thread-per-request situation in a real environment.

  • The service side

    public class Server {
    	public static void main(String[] args) {
    		byte[] buffer = new byte[1024];
    		try {
    			ServerSocket serverSocket = new ServerSocket(8080);
    			System.out.println("Server started and listening on port 8080");
    			while (true) {
    				System.out.println();
    				System.out.println("Server is waiting to connect...");
    				Socket socket = serverSocket.accept();
    				System.out.println("Server has received a connection request...");
    				System.out.println();
    				System.out.println("Server is waiting for data...");
    				socket.getInputStream().read(buffer);
    				System.out.println("Server has received data");
    				System.out.println();
    				String content = new String(buffer);
    				System.out.println("Data received :"+ content); }}catch (IOException e) {
    			// TODO Auto-generated catch blocke.printStackTrace(); }}}Copy the code
  • The client

    public class Consumer {
    	public static void main(String[] args) {
    		try {
    			Socket socket = new Socket("127.0.0.1".8080);
    			socket.getOutputStream().write("Send data to server".getBytes());
    			socket.close();
    		} catch (IOException e) {
    			// TODO Auto-generated catch blocke.printStackTrace(); }}}Copy the code
  • Code parsing

    We first create a server class that instantiates a SocketServer and binds port 8080. The Accept method is then called to receive the connection request, and the read method is called to receive the data sent by the client. Finally, the received data is printed.

    After completing the server design, we implement a client, first instantiate the Socket object, bind IP to 127.0.0.1 (native), port number to 8080, and call write method to send data to the server.

  • The results

    When we start the server, but the client has not yet initiated a connection to the server, the console results are as follows:

    When the client starts and sends data to the server, the console results are as follows:

  • conclusion

    At the very least, we can see from the above results that the server will block until a client requests to connect to the server due to the accept method after the server starts.

Extend the client function

In this paper, we implement the client logic is mainly to establish a Socket — — — — > > connection server sends data, our data is sent immediately after the connect to the server, and now we come to the client for an extension, when we connect to the server, not immediately to send data, but after waiting for the console input data manually, And send it to the server. (The server-side code remains the same)

  • code

    public class Consumer {
    	public static void main(String[] args) {
    		try {
    			Socket socket = new Socket("127.0.0.1".8080);
    			String message = null;
    			Scanner sc = new Scanner(System.in);
    			message = sc.next();
    			socket.getOutputStream().write(message.getBytes());
    			socket.close();
    			sc.close();
    		} catch (IOException e) {
    			// TODO Auto-generated catch blocke.printStackTrace(); }}}Copy the code
  • test

    When the server is started and the client has not requested a connection to the server, the console results are as follows:

    When the server starts and the client connects to the server but does not send data, the console results are as follows:

    When the server starts, the client connects to the server, and sends data, the console results are as follows:

  • conclusion

    From the results of the above we can see that the server after the start, first of all need to wait for the client connection requests (block) for the first time, if there is no client connection, the server will block waiting, then when the client connection, the server will wait for the client sends data blocking (second), if the client didn’t send data, The server will block waiting for the client to send data. The server will block twice from starting to receiving data from the client. This is a very important feature of the BIO, which blocks twice, the first while waiting for a connection and the second while waiting for data.

BIO

  • Weaknesses of BIO in single-threaded conditions

    In this paper, we implemented a simple server, this simple server is a single thread running, actually it is easy to see that, when our server receives a connection, and did not receive the client sends data, will be blocked in the read () method, then come again if a client request at this time, The server cannot respond. In other words, the BIO cannot handle multiple client requests without considering multithreading.

  • How does BIO handle concurrency

    In the previous server implementation, we implemented a single-threaded version of the BIO server. As you can see, the single-threaded version of the BIO cannot handle multiple client requests. How can we make the BIO handle multiple client requests?

    This is why one of the concepts in BIO is that the server implementation pattern is one connection per thread. That is, when the client has a connection request, the server needs to start a thread to process it.

  • Multithreaded BIO server simple implementation

    public class Server {
    	public static void main(String[] args) {
    		byte[] buffer = new byte[1024];
    		try {
    			ServerSocket serverSocket = new ServerSocket(8080);
    			System.out.println("Server started and listening on port 8080");
    			while (true) {
    				System.out.println();
    				System.out.println("Server is waiting to connect...");
    				Socket socket = serverSocket.accept();
    				new Thread(new Runnable() {
    					@Override
    					public void run(a) {
    						System.out.println("Server has received a connection request...");
    						System.out.println();
    						System.out.println("Server is waiting for data...");
    						try {
    							socket.getInputStream().read(buffer);
    						} catch (IOException e) {
    							// TODO Auto-generated catch block
    							e.printStackTrace();
    						}
    						System.out.println("Server has received data");
    						System.out.println();
    						String content = new String(buffer);
    						System.out.println("Data received :"+ content); } }).start(); }}catch (IOException e) {
    			// TODO Auto-generated catch blocke.printStackTrace(); }}}Copy the code
  • The results

    Obviously, the state of our server is now one thread per request, in other words, the server creates a thread for each connection request to process.

  • Disadvantages of multithreaded BIO server

    The multithreaded BIO server addresses the problem that single-threaded BIO cannot handle concurrency, but it also presents a problem: If there are a large number of requests that connect to our server but do not send messages, our server will also create a separate thread for those requests that do not send messages, so if the number of connections is small, the number of connections can be extremely stressful on the server. So if the number of inactive threads is high, we should adopt a single-threaded solution, but the single-thread can’t handle concurrency, which leads to a paradoxical state, hence NIO.

NIO

  • The introduction of NIO

    Let’s take a look at the code for the BIO server in single-threaded mode. In fact, the most fundamental problem NIO needs to solve is the two blocks that exist in the BIO: the block while waiting for connections and the block while waiting for data.

    public class Server {
    	public static void main(String[] args) {
    		byte[] buffer = new byte[1024];
    		try {
    			ServerSocket serverSocket = new ServerSocket(8080);
    			System.out.println("Server started and listening on port 8080");
    			while (true) {
    				System.out.println();
    				System.out.println("Server is waiting to connect...");
    				// Block 1: blocks while waiting to connect
    				Socket socket = serverSocket.accept();
    				System.out.println("Server has received a connection request...");
    				System.out.println();
    				System.out.println("Server is waiting for data...");
    				// Block 2: blocks while waiting for data
    				socket.getInputStream().read(buffer);
    				System.out.println("Server has received data");
    				System.out.println();
    				String content = new String(buffer);
    				System.out.println("Data received :"+ content); }}catch (IOException e) {
    			// TODO Auto-generated catch blocke.printStackTrace(); }}}Copy the code

    The old point is that if a single-threaded server is blocked while waiting for data, it cannot respond to a second connection request when it arrives. If it is a multi-threaded server, then there will be a large number of idle requests to generate new threads, resulting in threads occupying system resources, thread waste.

    The problem then shifts to how a single-threaded server can receive new client connections while waiting for client data to arrive.

  • Simulate the NIO solution

    If you want to solve the problem mentioned above that the single-threaded server is blocked when receiving data and cannot receive new requests, you can actually make the server not block while waiting for data and the problem will be solved.

    • First solution (no blocking while waiting for connections and data)

      public class Server {
      	public static void main(String[] args) throws InterruptedException {
      		ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
      		try {
      			//Java classes set for non-blocking
      			ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
      			serverSocketChannel.bind(new InetSocketAddress(8080));
      			// Set it to non-blocking
      			serverSocketChannel.configureBlocking(false);
      			while(true) {
      				SocketChannel socketChannel = serverSocketChannel.accept();
      				if(socketChannel==null) {
      					// Indicates that there is no connection
      					System.out.println("Waiting for client to request connection...");
      					Thread.sleep(5000);
      				}else {
      					System.out.println("Currently received a client connection request...");
      				}
      				if(socketChannel! =null) {
                          // Set it to non-blocking
      					socketChannel.configureBlocking(false);
      					byteBuffer.flip();// Switch mode write --> read
      					int effective = socketChannel.read(byteBuffer);
      					if(effective! =0) {
      						String content = Charset.forName("utf-8").decode(byteBuffer).toString();
      						System.out.println(content);
      					}else {
      						System.out.println("Currently no client message received"); }}}}catch (IOException e) {
      			// TODO Auto-generated catch blocke.printStackTrace(); }}}Copy the code
    • The results

      As you can see, in this solution, while receiving a client message without blocking, the server starts receiving requests again, and before the user has time to enter the message, the server moves on to receiving requests from other clients. In other words, the server loses the request from the current client.

    • Solution 2 (Caching sockets, polling data ready)

      public class Server {
      	public static void main(String[] args) throws InterruptedException {
      		ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
      		
      		List<SocketChannel> socketList = new ArrayList<SocketChannel>();
      		try {
      			//Java classes set for non-blocking
      			ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
      			serverSocketChannel.bind(new InetSocketAddress(8080));
      			// Set it to non-blocking
      			serverSocketChannel.configureBlocking(false);
      			while(true) {
      				SocketChannel socketChannel = serverSocketChannel.accept();
      				if(socketChannel==null) {
      					// Indicates that there is no connection
      					System.out.println("Waiting for client to request connection...");
      					Thread.sleep(5000);
      				}else {
      					System.out.println("Currently received a client connection request...");
      					socketList.add(socketChannel);
      				}
      				for(SocketChannel socket:socketList) {
      					socket.configureBlocking(false);
      					int effective = socket.read(byteBuffer);
      					if(effective! =0) {
      						byteBuffer.flip();// Switch mode write --> read
      						String content = Charset.forName("UTF-8").decode(byteBuffer).toString();
      						System.out.println("Received message :"+content);
      						byteBuffer.clear();
      					}else {
      						System.out.println("Currently no client message received"); }}}}catch (IOException e) {
      			// TODO Auto-generated catch blocke.printStackTrace(); }}}Copy the code
    • The results

    • Code parsing

      In the solution one, we have adopted a non-blocking mode, but found that once a non-blocking, while they are waiting for the client sends the message will not be blocked, but again to get a new client connection requests directly, it will cause a client connection is lost, and in the solution 2, we will connect the storage in a list in the collection, Poll each time you wait for a client message to see if it is ready, and print the message if it is. As you can see, we did not start the second thread at all. Instead, we used a single thread to handle multiple client connections, which is a perfect solution to the BIO’s inability to handle multiple client requests in single-threaded mode, and solved the problem of connection loss in non-blocking mode.

  • Existing problems (Solution 2)

    As you can see from the results just now, the message is not lost and the program is not blocked. But, on the way of receiving messages there may be some wrong, we have adopted a polling to receive messages, polling every time all connections, whether the message is ready, the test cases just three connection, so don’t see what problem, but we assume that there are 10 million links, or more, with the method of the polling efficiency is very low. On the other hand, out of 10 million connections, we may only have 1 million messages and the remaining 9 million will not send any messages, so those linkers still have to poll every time, which is obviously not appropriate.

  • How to solve it in real NIO

    In real NIO, instead of polling at the Java level, we would delegate the polling to our operating system by making an operating system-level system call (select, or epoll in Linux) to the select function. Actively aware of the socket with data.

On the difference between using SELECT /epoll and polling directly at the application layer

We implemented a before use Java to do multiple clients to connect polling logic, but in the real NIO source code is not really so, NIO USES the operating system at the bottom of the polling system calls the select/epoll (Windows: select, Linux: epoll), So why not just implement it instead of calling the system to do polling?

  • Select underlying logic

    Suppose there are five connections A, B, C, D, and E connecting to the server at the same time. According to our design above, the program will traverse these five connections, poll each connection, and obtain the data readiness of each connection. What is the difference between this program and our own program?

    First of all, the nature of the Java program we write also needs to call system functions when polling each Socket, so polling is called once, which will cause unnecessary context switching overhead.

    Select copies all five requests from the user-mode space to the kernel-mode space to determine whether data is ready for each request, completely avoiding frequent context switching. So the efficiency is much higher than if we write polling directly in the application layer.

    If the select query does not find the request with data, it will always block (yes, select is a blocking function). If one or more requests have data ready, select will set the file descriptor with data, and select will return. Return and iterate to see which request has data.

    Disadvantages of SELECT:

    1. The underlying storage relies on Bitmaps and is capped at 1024 requests.

    2. File descriptors are set, so if the set file descriptor needs to be reused, it needs to be reassigned with a null value.

    3. There is still an overhead in copying fd (file descriptor) from user to kernel mode.

    4. After the SELECT returns, it is iterated again to see which request has data.

  • The poll function’s underlying logic

    Poll works much like SELECT. Let’s look at a structure used inside a poll.

    struct pollfd{
        int fd;
        short events;
        short revents;
    }
    Copy the code

    Poll also copies all requests to the kernel state. Like SELECT, poll is a blocking function. Pollfd sets events or Revents when one or more requests have data, but pollFD sets events or Revents instead of fd itself. So there is no need to reassign the null value the next time it is used. Poll internal storage does not rely on bitmaps, but instead uses a data structure of a PollFD array, which must be larger than 1024. Select 1, 2.

  • epoll

    Epoll is one of the latest multiplex IO multiplexing functions. Here are just some of its features.

    The biggest difference between epoll and the above two functions is that its FD is shared between the user state and the kernel state, so there is no need to make a copy from the user state to the kernel state, which can save system resources. In addition, in SELECT and poll, if the data for a request is ready, they return all the requests, and the program iterates to see which requests have data. However, epoll returns only the requests that have data. This is because epoll first performs a reordering operation when it discovers that a request has data. By putting all FDS with data to the front and returning (N as the number of existing data requests), our upper layer program can not poll all requests, but simply iterate over the first N requests returned by epoll, all of which have data.

The concepts of BIO and NIO in Java

Some articles usually put concepts at the beginning, but THIS time I choose to put concepts at the end, because through the above practice, I believe that you have some understanding of the Java BIO and NIO, this time should be a better understanding of the concept.

Concept of finishing in: blog.csdn.net/guanghuiche…

To understand the concept, let’s take a bank withdrawal as an example:

  • Synchronization: Go to the bank with your bank card to withdraw money (Java handles IO reading and writing by itself when using synchronous IO).
  • Asynchronous: entrust a younger brother to take the bank card to the bank to withdraw money, and then give you (when using asynchronous IO, Java will entrust THE IO read and write to OS processing, need to send the data buffer address and size to OS(bank card and password), OS needs to support asynchronous IO API).
  • Blocking: ATM queues to withdraw money and you have to wait (with blocking IO, Java calls are blocked until the read or write is finished).
  • Non-blocking: You can ask the lobby manager if the queue has arrived. If the lobby manager says it hasn’t arrived, you can’t go. (When using non-blocking IO, if you can’t read and write Java calls, it will return immediately. Read/write continues when the IO event dispatcher informs the read/write, repeating the loop until the read/write is complete.

Java support for BIO, NIO:

  • Java BIO (Blocking I/O) : the server implementation mode is one connection one thread, that is, when the client has a connection request, the server needs to start a thread to process, if the connection does not do anything, it will cause unnecessary thread overhead, of course, can be improved through the thread pool mechanism.
  • Java NIO (non-blocking I/O) : Synchronous non-blocking. The server implements the per-request per-thread mode, that is, all connection requests sent by the client are registered with the multiplexer, and the multiplexer starts a thread to process the connection only when there is an I/O request.

BIO and NIO application scenario analysis:

  • BIO mode is suitable for small and fixed number of connections. This mode requires high server resources, concurrency is limited to the application, and the only choice before JDK1.4, but the program is intuitive, simple and easy to understand.
  • NIO is suitable for architectures with a large number of connections and relatively short (light operation) connections, such as chat servers, where concurrency is limited to applications and programming is complicated. JDK1.4 supports NIO.

conclusion

This article introduces some of the JavaBIO and NIO from the point of view of their own practical understanding, I personally believe that this understanding of BIO and NIO will be more than just look at the concept of a deeper understanding, I also hope that you can go to type, through the results of the program to get their own understanding of JavaBIO and NIO.

Welcome to visit my personal Blog: Object’s Blog