1. BIO basics
BIO is blocking I/O, which is the most basic IO method provided by Java. In network communication, it is necessary to establish a two-way link between the client and the server through Socket to achieve communication. The main steps are as follows:
1. The server monitors whether there is a connection request on a port.
2. The client sends a link request to the server.
3. The server returns an Accept message to the client, and the connection is successful at this time.
4. The client and server communicate with each other through Send(), Write() and other methods.
5. Close the link.
A mind map of BIO (image source: https://my.oschina.net/langxSpirit/blog/830620 )
Java provides two classes Socket and ServerSocket respectively, which are used to represent the client and server of the bidirectional link. Based on these two classes, a simple network communication example is as follows:
1. Client
package com.wang.test.net.io.bio; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; /** * Client */ public class Client { public static void main(String[] args) throws Exception { try ( //Send a request to port 8080 of the machine Socket socket = new Socket("127.0.0.1", 8080); //Construct a BufferedReader object from standard input BufferedReader clientInput = new BufferedReader(new InputStreamReader(System.in)); //Get the output stream through Socket, construct the PrintWriter object PrintWriter writer = new PrintWriter(socket.getOutputStream()); //Get the input stream through Socket and construct a BufferedReader object BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); ){ //read input information String input = clientInput.readLine(); while ( !input.equals("exit") ){ //Send the input information to the server writer.print(input); //Refresh the output stream so that the server can receive the request information immediately writer.flush(); //Read the information returned by the server System.out.println("The corresponding server side is: " + reader.readLine()); //read the next input input = clientInput.readLine(); } } catch ( Exception e ) { System.out.println(e); } } }
2. Server
package com.wang.test.net.io.bio; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; /** * Service-Terminal */ public class Server { public static void main(String[] args) { try( //Create ServerSocket listening port 8080 ServerSocket server = new ServerSocket(8080); // wait for client request Socket socket = server.accept(); //Construct a BufferedReader object from standard input BufferedReader serverInput = new BufferedReader(new InputStreamReader(System.in)); //Get the output stream through the Socket object, BufferedReader BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); //Get the output stream through the Socket object and construct the PrintWrite object PrintWriter writer = new PrintWriter(socket.getOutputStream()); ){ //read client request System.out.println("Client request:" + reader.readLine()); // input server corresponding String input = serverInput.readLine(); //If the input is "exit", exit while (!input.equals("exit")){ writer.print(input); // output the string to the client writer.flush(); //read client request System.out.println("Client request:" + reader.readLine()); // input server corresponding input = serverInput.readLine(); } } catch ( Exception e ){ System.out.println(e); } } }
2. JIoEndpoint
In the implementation of Tomcat server, Tomcat's IO monitoring is completed by Endpoint. Specifically, BIO is JIoEndpoint. The startup process of JIoEndpoint is as follows:
1. Create a ServerSocket instance according to the IP address (in the case of multiple IPs) and port; part of the code of the bind() method of the JIoEndpoint class
if (serverSocket == null) { //Create a ServerSocket instance based on the IP address (in the case of multiple IPs) and port try { if (getAddress() == null) { serverSocket = serverSocketFactory.createSocket(getPort(), getBacklog ()); } else { serverSocket = serverSocketFactory.createSocket(getPort(), getBacklog(), getAddress()); } } catch (BindException orig) { String msg; if (getAddress() == null) msg = orig.getMessage() + " <null>:" + getPort(); else msg = orig.getMessage() + " " + getAddress().toString() + ":" + getPort(); BindException be = new BindException(msg); be.initCause(orig); throw be; } }
2. If the Connector is not configured with a shared thread pool, create a request processing thread pool; part of the code of the startInternal() method of the JIoEndpoint class
// Create worker collection if (getExecutor() == null) { //If the Connector is not configured with a shared thread pool createExecutor(); //Create a request processing thread pool }
The createExecutor() method is implemented in the JIoEndpoint parent abstract class
public void createExecutor() { internalExecutor = true; TaskQueue taskqueue = new TaskQueue(); TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority()); executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf); taskqueue.setParent( (ThreadPoolExecutor) executor); }
3. Create and start the Accept thread according to the number of AcceptThreadCount configuration, part of the code of the startInternal() method of the JIoEndpoint class
startAcceptorThreads(); //According to the number of AcceptorThreadCount configuration, create and start Acceptor threads
The startAcceptThread method is implemented in the parent abstract class:
/** * Create and start Acceptor threads according to the number configured by AcceptorThreadCount */ protected final void startAcceptorThreads() { int count = getAcceptorThreadCount(); acceptors = new Acceptor[count]; for (int i = 0; i < count; i++) { acceptors[i] = createAcceptor(); String threadName = getName() + "-Acceptor-" + i; acceptors[i].setThreadName(threadName); Thread t = new Thread(acceptors[i], threadName); //The thread is started separately and is not placed in the thread pool, so it will not affect concurrent processing of requests t.setPriority(getAcceptorThreadPriority()); t.setDaemon(getDaemon()); t.start(); } }
In this method, these threads are started independently, so it does not affect the number of concurrent requests. createAcceptor obtains the inner class Accept in the JIoEndpoint class, which inherits the inner class Accept in the JIoEndpoint parent abstract class. Acceptor implements the Runnable interface and is responsible for polling to receive client requests. Inner class Acceptor in JIoEndpoint class
/** * Responsible for polling to receive client requests, and also to detect EndPoint status and the maximum number of connections * The background thread that listens for incoming TCP/IP connections and * hands them off to an appropriate processor. */ protected class Acceptor extends AbstractEndpoint.Acceptor { @Override public void run() { int errorDelay = 0; // Loop until we receive a shutdown command while (running) { // Loop if endpoint is paused while (paused && running) { state = AcceptorState.PAUSED; try { Thread.sleep(50); } catch (InterruptedException e) { // Ignore } } if (!running) { break; } state = AcceptorState.RUNNING; try { //if we have reached max connections, wait countUpOrAwaitConnection(); Socket socket = null; try { // Accept the next incoming connection from the server // socket socket = serverSocketFactory.acceptSocket(serverSocket); } catch (IOException ioe) { countDownConnection(); // Introduce delay if necessary errorDelay = handleExceptionWithDelay(errorDelay); // re-throw throw ioe; } // Successful accept, reset the error delay errorDelay = 0; // Configure the socket if (running && !paused && setSocketOptions(socket)) { //When a client request is received // Hand this socket off to an appropriate processor if (!processSocket(socket)) { // countDownConnection(); // Close socket right away closeSocket(socket); } } else { countDownConnection(); // Close socket right away closeSocket(socket); } } catch (IOException x) { if (running) { log.error(sm.getString("endpoint.accept.fail"), x); } } catch (NullPointerException npe) { if (running) { log.error(sm.getString("endpoint.accept.fail"), npe); } } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString("endpoint.accept.fail"), t); } } state = AcceptorState.ENDED; } }
4. After receiving the client request, create a SocketProcessor object and submit it to the thread pool for processing.
5. SocketProcessor does not directly process Socket, but passes it to a specific protocol processing class. The HTTP protocol in BIO mode uses HTTP11Processor.
Inner class SocketProcessor in JIoEndpoint class
/** * This class is the equivalent of the Worker, but will simply use in an * external Executor thread pool. * SocketProcessor does not directly process Socket, but passes it to a specific protocol processing class, such as Http11Processor for processing HTTP protocol in BIO mode */ protected class SocketProcessor implements Runnable { protected SocketWrapper<Socket> socket = null; protected SocketStatus status = null; public SocketProcessor(SocketWrapper<Socket> socket) { if (socket==null) throw new NullPointerException(); this.socket = socket; } public SocketProcessor(SocketWrapper<Socket> socket, SocketStatus status) { this(socket); this.status = status; } @Override public void run() { boolean launch = false; synchronized (socket) { try { SocketState state = SocketState.OPEN; handler.beforeHandshake(socket); try { // SSL handshake serverSocketFactory.handshake(socket.getSocket()); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); if (log.isDebugEnabled()) { log.debug(sm.getString("endpoint.err.handshake"), t); } // Tell to close the socket state = SocketState.CLOSED; } if ((state != SocketState.CLOSED)) { if (status == null) { state = handler.process(socket, SocketStatus.OPEN_READ); } else { state = handler.process(socket,status); } } if (state == SocketState.CLOSED) { // Close socket if (log.isTraceEnabled()) { log.trace("Closing socket:"+socket); } countDownConnection(); try { socket.getSocket().close(); } catch (IOException e) { // Ignore } } else if (state == SocketState.OPEN || state == SocketState.UPGRADING || state == SocketState.UPGRADED){ socket.setKeptAlive(true); socket.access(); launch = true; } else if (state == SocketState.LONG) { socket.access(); waitingRequests.add(socket); } } finally { if (launch) { try { getExecutor().execute(new SocketProcessor(socket, SocketStatus.OPEN_READ)); } catch (RejectedExecutionException x) { log.warn("Socket reprocessing request was rejected for:"+socket,x); try { //unable to handle connection at this time handler.process(socket, SocketStatus.DISCONNECT); } finally { countDownConnection(); } } catch (NullPointerException npe) { if (running) { log.error(sm.getString("endpoint.launch.fail"), name); } } } } } socket = null; // Finish up this request } }
6. In addition, JIoEndpoint also constructs a separate thread to detect timeout requests. Part of the code of the startInternal method in the JIoEndpoint class
// Start async timeout thread is used to detect timeout requests setAsyncTimeout(new AsyncTimeout()); Thread timeoutThread = new Thread(getAsyncTimeout(), getName() + "-AsyncTimeout"); timeoutThread.setPriority(threadPriority); timeoutThread.setDaemon(true); timeoutThread.start();