Recently, I have learned the knowledge related to Java network programming and Netty. I have learned that Netty is a NIO mode network framework, but it provides different channels to support different modes of network communication processing, including synchronous, asynchronous, blocking and non-blocking. Learning starts with the basics, so let’s take a look at the basic concepts and java-native NIO. Here, I will learn the knowledge summed up recently, for everyone to understand.

In order to save your time, the main contents of this article are as follows:

  • The concept of asynchrony, blocking
  • Operating system I/O type
  • The underlying implementation of Java NIO

Asynchronous, synchronous, blocking, non-blocking

Synchronization and asynchronism focus on message communication mechanism. The so-called synchronization is that after the caller calls, the call will not return until the result is obtained, but once the call returns, the return value is obtained. Synchronization means that the caller actively waits for the call result; Asynchronous, on the other hand, returns directly after the call, so there may be no return value, when there is a return value, by the called state, notification to notify the caller. Asynchrony is when the called notifies the caller that the call is ready *. * Therefore, there is a difference in the message communication mechanism between the caller to check whether the call result is ready and the caller to notify the caller that the call result is ready

Blocking and non-blocking are concerned with the state of the program while it waits for the result of the call (message, return value). A blocked call means that the current thread is suspended until the result of the call is returned, and the calling thread will not resume execution until it gets the result. A non-blocking call means that the calling thread is not suspended or can do something else until the structure is not immediately available.

When the two sets of concepts are combined, there are four cases: synchronous blocking, synchronous non-blocking, asynchronous blocking, and asynchronous non-blocking. Let’s give an example of the four cases of appeal.

For example, if you want to download a 1 gigabyte file from the Internet, after pressing the download button, if you wait by the computer, waiting for the download to end, this is synchronous blocking. If you don’t need to be at your computer all the time, you can read a book for a while, but you still check the download progress every once in a while, which is synchronous non-blocking; If you’re sitting next to your computer all the time, but the downloader plays music to remind you when the download is over, that’s asynchronous blocking. But if you’re not at your computer, reading a book, and the music comes on when the download is done, then this is asynchronous non-blocking.

Unix I/O type

With these two sets of concepts in mind, let’s take a look at the five I/O models available under Unix:

  • Blocking I/O (bloking IO)
  • Nonblocking I/O(NONblocking IO)
  • Multiplexing I/O multiplexing
  • Signal Driven I/O
  • Asynchronous I/O (asynchronous I/O)

The first four are synchronous, but the last is asynchronous I/O. Note that ***Java NIO relies on multiplexing I/O of Unix systems, which is synchronous I/O for I/O operations, but asynchronous network calls for the programming model ***. Let’s introduce the different I/O types using system read calls.

When a read occurs, it goes through two phases:

  • Procedure 1 Wait for data preparation
  • 2 Copy data from kernel memory space to process memory space

Different I/O types have different behaviors in these two phases. But because this content is more, and most of the knowledge is descriptive, so here we only give a few pictures to explain, interested students can go to the specific understanding.

The underlying implementation of Java NIO

We all know that Netty provides Native Socket Transport via JNI, so why would Netty provide its own Native version of NIO? It is clear that the Java NIO base is also based on epoll calls (the latest version). Now, let’s not say this, but let’s think about the possible scenarios. The following source code comes from openJDK-8U40-b25.

The open method

If we follow the Selector. Open () method class by class, it’s easy to see that the Selector is initialized by DefaultSelectorProvider which generates different selectorProviders depending on the operating system platform, On Linux, it generates an instance of EPollSelectorProvider, which in turn generates EPollSelectorImpl as the final Selector implementation.

class EPollSelectorImpl extends SelectorImpl { ..... // The poll object EPollArrayWrapper pollWrapper; . EPollSelectorImpl(SelectorProvider sp) throws IOException { ..... pollWrapper = new EPollArrayWrapper(); pollWrapper.initInterrupt(fd0, fd1); . }... }Copy the code

EpollArrayWapper encapsulates Linux epoll-related system calls into native methods for EpollSelectorImpl use.

    private native int epollCreate();
    private native void epollCtl(int epfd, int opcode, int fd, int events);
    private native int epollWait(long pollAddress, int numfds, long timeout,
                                 int epfd) throws IOException;
Copy the code

The above three native methods correspond to the three system calls related to epoll in Linux

// Create an epoll handle. Size is the maximum number of listeners. int epoll_create(int size); Epoll_ctl (int epfd, int op, int fd, struct epoll_event *event); epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // Wait for an event to occur, similar to a SELECT call, Int epoll_wait(int epfd, struct epoll_event * events, int maxEvents, int timeout);Copy the code

So, we see that the epollCreate method is called in the constructor of EpollArrayWapper, creating an epoll handle. And then we’re done creating the Selector object.

The register method

Similar to Open, the ServerSocketChannel register function calls the Register method of the SelectorImpl class, which is the parent of EPollSelectorImpl.

protected final SelectionKey register(AbstractSelectableChannel ch,
                                      int ops,
                                      Object attachment)
{
    if(! (ch instanceof SelChImpl)) throw new IllegalSelectorException(); SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this); //attach the object the user wants to store. Synchronized (publicKeys) {implRegister(k); } // Set the option of concern k.interestops (ops);return k;
}
Copy the code

The corresponding EpollSelectorImpl method is implemented as follows: it calls the add method of EPollArrayWrapper, records the fd value of the Channel, and adds ski to the keys variable. In EPollArrayWrapper there is a byte array eventLow that records the FD values of all channels.

    protected void implRegister(SelectionKeyImpl ski) {
        if(closed) throw new ClosedSelectorException(); SelChImpl ch = ski.channel; Int fd = integer.valueof (ch.getfdVal ()); int fd = integer.valueof (ch.getfdVal ()) fdToKey.put(fd, ski); // Call pollWrapper's add method to add the channel's fd to the monitor list. PollWrapper. Add (fd); // Save to HashSet, keys is SelectorImpl member keys.add(ski); }Copy the code

We can see that calling the Register method does not involve calling the native method epollCtl in EpollArrayWrapper because they defer the call to the Select method.

The Select method

Like the Register method, the Select method in SelectorImpl eventually calls the doSelect method of its subclass EpollSelectorImpl

protected int doSelect(long timeout) throws IOException { ..... try { .... PollWrapper. Poll (timeout); pollctl (native); pollWrapper (timeout); } finally { .... }... Int numKeysUpdated = updateSelectedKeys(); .return numKeysUpdated;
}
Copy the code

From the above code, you can see that EPollSelectorImpl calls EPollArrayWapper’s poll method first, and then updates SelectedKeys. The poll method first calls epollCtl to register the FD and interested event type of the Channel previously saved in the Register method, and then the epollWait method waits for the interested event to be generated, causing the thread to block.

int poll(long timeout) throws IOException { updateRegistrations(); //// Calls epollCtl to update the event type of interest ////. .return updated;
}
Copy the code

The epollWait function returns after the event of interest has occurred (or after the waiting time exceeds a preset maximum). The select function resumes from the blocked state.

SelectedKeys method

Let’s start with the selectedKeys method in SelectorImpl.

Private Set<SelectionKey> publicSelectedKeys; private Set<SelectionKey> publicSelectedKeys; public Set<SelectionKey>selectedKeys() {...return publicSelectedKeys;
}
Copy the code

Return publicSelectedKeys (select); return publicSelectedKeys;

PublicSelectedKeys is actually a copy of the selectedKeys variable, and you can find their relationship in the SelectorImpl constructor, so let’s go back to the Select updateSelectedKeys method.

private int updateSelectedKeys() {// Number of keys updated, or events generated int entries = pollWrapper. Updated; int numKeysUpdated = 0;for(int i=0; i<entries; I++) {/ / the corresponding channel fd int nextFD = pollWrapper. GetDescriptor (I); SelectionKeyImpl ski = fdTokey. get(integ.valueof (nextFD));if(ski ! = null) { int rOps = pollWrapper.getEventOps(i); // Update the selectedKey variable and notify the responding channel to handle the responseif (selectedKeys.contains(ski)) {
                if(ski.channel.translateAndSetReadyOps(rOps, ski)) { numKeysUpdated++; }}else {
                ski.channel.translateAndSetReadyOps(rOps, ski);
                if((ski.nioReadyOps() & ski.nioInterestOps()) ! = 0) { selectedKeys.add(ski); numKeysUpdated++; }}}}return numKeysUpdated;
}
Copy the code

Afterword.

You see the details here and you’ve already seen the underlying implementation of NIO. I want to address two issues here.

First, why did Netty itself reimplement a native NIO underlying method? Listen to what the founder of Netty said. This is because the Java version uses Epoll in level-triggered mode, while Netty wants to use edge-triggered mode, and the Java version does not expose some of the epoll configuration items, such as TCP_CORK and SO_REUSEPORT.

The second is to see so much source code, spend so much time what is the purpose? I feel if from a non-utilitarian point of view, then it is pure hope to understand more, sometimes after reading the source code or understanding the underlying principle, will use a sense of enlightenment, such as the principle of AQS. If you look at it from a purposeful point of view, if you know the underlying principles, you have more control, and if something goes wrong, you can find it faster and fix it. In addition, you can use the source code as a template to build your own wheels based on the actual situation and implement a version that better meets your current needs.

If I have time later, I hope to have a good understanding of the implementation principle of epoll at the operating system level.