Basic concepts
• Buffer operation
Buffers and operations are the basis of all I/O, and the process performs I/O operations, which boils down to making a request to the operating system to either drain (write) The data in the buffer or fill the buffer (read). Such as
• Kernel space, user space
The process of moving data from disk to user process is briefly described, involving both kernel space and user space. What is the difference between these two spaces?
User space is the region of a regular process, such as a JVM, where user space is non-privileged and does not have direct access to hardware devices. Kernel space is the area of the operating system, it must be privileged, such as to communicate with the device controller, control the user area of the process running state. When the process performs an I/O operation, it executes a system call that puts control to the kernel.
• Virtual Memory
• Memory page scheduling
5 Types of I/O models
Speaking of I/O model, there is a wrong concept on the network, asynchronous non-blocking/blocking model, in fact, async is not blocking the asynchronous model is the asynchronous model. Let's take a look at the 5 I/O models presented by Richard Stevens in its UNIX network programming volume 1.
• Block-Type I/O
• Non-blocking I/O
I/O multiplexing (Java NiO is this model)
• Signal-driven I/O
• Asynchronous I/O
Defined by POSIX terminology, synchronous I/O operations cause the request process to block until the I/O operation completes, and asynchronous I/O operations do not cause the request process to block. The first 4 of the 5 models belong to the synchronous I/O model.
Why NIO?
Before you start talking about NIO, it is necessary to understand why NIO is a problem, compared to the advantages of traditional stream I/O, what it can be used to do, and so on.
Traditional stream I/O is byte-based and all I/O is treated as a single byte move, and NIO is block-based, as you might guess, NiO's performance is definitely better than flow I/O. That's right! Its performance is improved by its use of a structure that is closer to how the operating system performs I/O: Channels and buffers. We can think of it as a coal mine, the channel is a mineral that contains coal (data), and the buffer is a truck that is sent to the mine. The truck was full of coal and we got coal from the truck. In other words, we do not interact directly with the channel; we just interact with the buffer and dispatch the buffer to the channel. The channel either obtains data from the buffer or sends data to the buffer. (This metaphor is derived from the idea of Java programming)
The main applications of NIO are in high-performance, high-capacity service-side applications, typically with Apache Mina based on it.
Buffer
A buffer is essentially an array, but it is not just an array, but it also provides structured access to the data and can also track the read/write process of the system. Why do you say that? Here's a look at the details of the buffer.
Before we talk about buffer details, let's take a look at the buffer "genealogy":
• Internal details
The buffer object has four basic properties:
O Capacity Capacity: Maximum number of data elements that the buffer can hold, set when the buffer is created, cannot be changed
o Upper bound limit: The index of the first element of a buffer that cannot be read or written
o Position Position: Index of the next element to be read or written
O Mark Mark: Memo location, call Mark () to set mark=position, call Reset () to set Position=mark
These four attributes always follow such a relationship: 0<=mark<=position<=limit<=capacity. is a newly created buffer logical view with a capacity of 10:
Buffer.put ((byte) ' H '). Put ((byte) ' E '). put ((byte) ' L '). Put ((byte) ' O ');
Five times the put buffer is called:
Buffer.put (0, (byte) ' M '). Put ((byte) ' W ');
Calling an absolute version of put does not affect position:
Now that the buffer is full, we must empty it. We want to pass this buffer to a channel so that the content can be completely written out, but now executing get () will undoubtedly take out undefined data. We have to set the posistion to 0, and then the channel will start reading from the correct position, but what is read? This is why limit is introduced, which indicates the end of the buffer's valid content. This operation is called rollover in the buffer: Buffer.flip ().
The rewind operation is similar to flip, but does not affect the limit.
The process of copy data from the input channel to the output channel should look like this:
while (true) { buffer.clear(); // 重设缓冲区以便接收更多字节 int r = fcin.read( buffer ); if (r==-1) { break; } buffer.flip(); // 准备读取缓冲区数据到通道 fcout.write( buffer );}
• Create buffers
Generally, the new allocation of a buffer is through the allocate method. If you want to provide your own array of backup memory for buffers, call the Wrap method.
The buffers created in both ways are indirect, and the indirect buffers use a backup array (The associated methods are HasArray (), Array (), Arrayoffset ()).
• Copy Buffers
The duplicate method creates a buffer similar to the original buffer, with two buffers that share data elements, but they have their own position, limit, and mark, such as:
Another method, slice, is similar to duplicate, but the slice method creates a new buffer starting at the current position of the original buffer, and the capacity is the number of remaining elements of the original buffer (limit-position), see.
• Byte buffers
o byte order
Why is there a byte order? For example, there are 1 int type digital 0X036FC5D9, it accounts for 4 bytes, then in memory, it is possible that the highest byte 03 bit the low address (big endian byte order), it is possible that the lowest byte D9 is located in the low address (small-endian byte order).
The network byte order is specified in the IP protocol, so we must first convert between the local host byte order and the common network byte order. In Java.nio, the byte order is encapsulated by the Byteorder class.
The default byte order in Bytebuffer is Bytebuffer.big_endian, but why does byte need a byte order? Bytebuffer, like other basic data types, has a number of handy ways to get and hold buffer content, which encodes or decodes bytes depending on the Bytebuffer current byte order.
O Direct Buffer
A direct buffer is created by calling the Bytebuffer.allocatedirect method. Usually a direct buffer is the best choice for I/O operations, because it avoids some copying processes, but may also cost more than an indirect buffer, and its memory is allocated by calling code on the local operating system.
o View Buffers
The view buffer and the buffer copy are very similar, except that the data types are different, so the byte correspondence is slightly different. For example, Bytebuffer.ascharbuffer, the converted buffer gets the elements from the get operation corresponding to the 2 bytes in the backup store.
o How to access unsigned integers?
There is no direct support for unsigned values in Java, and each unsigned value read from the buffer is raised to the next data type larger than it.
public static short getUnsignedByte(ByteBuffer bb) { return ((short) (bb.get() & 0xff)); } public static void putUnsignedByte(ByteBuffer bb, int value) { bb.put((byte) (value & 0xff));}
Channel
Channels are used to transfer data efficiently between the buffer and the entity (file, socket) that is located on the other side of the channel. The "genealogy" of the channel is slightly more complex relative to the buffer:
• Using channels
Open channel is relatively simple, except FileChannel, all open method.
We know that the channel interacts with the buffer, gets the data from the buffer, or transmits it to the buffer. As you can see from the class inheritance hierarchy, the channels are generally bidirectional (except FileChannel).
Here's a look at the code for data transfer between channels:
private static void channelCopy(ReadableByteChannel src, WritableByteChannel dest) throws IOException { ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024); while (src.read(buffer) != -1) { // Prepare the buffer to be drained buffer.flip(); // Write to the channel; may block dest.write(buffer); // If partial transfer, shift remainder down // If buffer is empty, same as doing clear( ) buffer.compact(); } // EOF will leave buffer in fill state buffer.flip(); // Make sure that the buffer is fully drained while (buffer.hasRemaining()) { dest.write(buffer); }}
• Close the Channel
The channel cannot be reused, which is different from the buffer; When the channel is closed, the channel will no longer connect to anything, and any read or write operation will cause closedchannelexception.
Calling the close () method of the channel may cause the thread to block temporarily, even if the channel is in non-blocking mode. If the channel implements the Interruptiblechannel interface, then blocking a thread on that channel is interrupted, the channel is closed, and the blocked thread throws a Closedbyinterruptexception exception. When a channel is closed, all threads that hibernate on that channel are awakened and receive an asynchronouscloseexception exception.
• Divergence, aggregation
Divergence, aggregation, also known as vector I/O, is a simple and powerful concept, which refers to implementing a simple I/O operation on multiple buffers. It reduces or avoids copies of buffers and system calls, and it should use direct buffers to get maximum performance benefits from local I/O.
• File Channels
Socket Channel
There are three socket channels, Serversocketchannel, Socketchannel and Datagramchannel, respectively, and they correspond to the socket object ServerSocket in the java.net package, When the socket and Datagramsocket;socket channels are instantiated, a peer socket object is created.
The socket channel can run non-blocking mode and is selectable, non-blocking I/O is closely linked to selectivity, which is why the managed blocking API is defined in Selectablechannel. Setting non-blocking is very simple, as long as you call the Configureblocking (False) method. If you need to change the blocking mode halfway, you must first obtain the lock of the object returned by the Blockinglock () method.
o Serversocketchannel
The Serversocketchannel is a channel-based socket listener. But it does not have the bind () method, so it needs to take out the peer socket object and use it to bind to a port to start listening for connections. In nonblocking mode, the Accept () method returns null immediately when no incoming connection is waiting. It is this ability to check connectivity without blocking to achieve scalability and reduce complexity, and selectivity is thus implemented.
ByteBuffer buffer = ByteBuffer.wrap("Hello World".getBytes()); ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind(new InetSocketAddress(12345)); ssc.configureBlocking(false); for (;;) { System.out.println("Waiting for connections"); SocketChannel sc = ssc.accept(); if (sc == null) TimeUnit.SECONDS.sleep(2000); else { System.out.println("Incoming connection from:" + sc.socket().getRemoteSocketAddress()); buffer.rewind(); sc.write(buffer); sc.close(); } }
o Socketchannel
As opposed to Serversocketchannel, it acts as a client, initiates a connection to the listener server, and begins to receive data after the connection is successful.
It is important to note that the open () method that calls it is only opened but not connected, the connection needs to be made immediately after the Connect () method is called, or the open (socketaddress remote) method can be called in two steps.
You will find that the Connect () method does not provide the Timout parameter, and as an alternative, you can use the isconnected (), isconnectpending (), or Finishconnect () method to check the status of the connection.
o Datagramchannel
Unlike the previous two channel objects, it is non-connected and can be either a server or a client.
Selector Selector
Selectors provide the ability to choose which tasks are already ready, which makes multivariate I/O possible. Ready selection and multivariate execution enable single-threaded multiple I/O channels to be managed efficiently at the same time. The selector is the main play in NiO, the core of I/O multiplexing, let's look at this magical stuff.
• Basic Concepts
Let's take a look at the diagram of the selector related classes:
As can be seen from the diagram, the Selector class selector does not have a direct relationship to the channel, but is contacted by the object Selectionkey called the selection key. The Select key represents a registration relationship between the channel and the selector, and the channel () and selector () methods return the registered channels and selectors, respectively. As can be seen from the class diagram, a channel can be registered to multiple selectors, and the Register method registers () is placed in the channel class, and I feel fit in the selector class.
The non-blocking feature is very closely related to multivariate execution, and if you register a channel in blocking mode, the system throws a Illegalblockingmodeexception exception.
So, after the channel is registered to the selector, how does the selector implement the ready choice? The real ready operation is done by the operating system, which processes the I/O requests and notifies the individual threads that their data is ready, while the Selector class provides this abstraction.
Selecting the key as the registration relationship between the channel and the selector requires maintaining the channel operation Interestops () that the registration relationship is concerned with, and the Operation Readyops () that the channel is already prepared for, and the return values of both methods are bit masks, and the Ready collection is a subset of the interest collection. There are 4 selectable actions defined in the Select Key class: Read, write, connect, and accept. In the class diagram you can see that each selectable channel has an abstract method of Validops (), each of which has a different set of valid selectable operations, such as Serversocketchannel's set of valid operations is accept, The set of valid operations for Socketchannel is read, write, and connect.
Back to look at the registration method, the second parameter is a bit mask, this parameter is the above-mentioned registration relationship is concerned about the operation of the channel. During the selection process, the channel operations that you care about can be modified by method Interestops (int operations) without affecting the selection process (which takes effect in the next selection process).
• Using selectors
O Selection Process
As can be seen in the class diagram, the Selector class maintains a collection of two keys: the collection of registered keys keys () and the collection of selected keys Selectedkeys (), the collection of selected keys is a subset of the registered Key's collection. Each member of the selected key in the collection is judged to be ready (at least one action in the set of operations concerned) by the selector (in the previous selection operation). In addition, the selector internally maintains a set of canceled keys that contain the keys that the Cancel () method has been called.
The core of the selector class is the selection process, which is basically a wrapper for system calls such as SELECT (), poll (). So, what is the specific details or steps of the selection process?
When select () of the selector class is called, the following steps are executed:
1. The collection of keys that have been canceled is checked. If not NULL, the keys in the collection are removed from the other two collections, and the associated channel is unregistered. When this step is finished, the set of canceled keys will be empty.
2. The interest collection of keys in the collection of registered keys is checked. After this step is executed, changes to the Interset collection do not affect the remaining inspection process. Once the readiness conditions are determined, the operating system will query to determine the true readiness state of the operations that each channel cares about. This may block for some time, and eventually the ready state of each channel will be determined. Those channels that are not ready will not perform any action, and for those operating systems that indicate at least one of the operations in the interest collection, one of the following two operations will be performed:
A. If the key of the channel is not already in the selected Key's collection, then the Ready collection of the key is emptied, and then the bit mask that indicates that the operating system discovers that the current channel has been prepared will be set.
B. If the channel's key is already in the selected Key's collection, the Ready collection of keys is updated by the bit mask that represents the operation that the operating system discovers the current channel is prepared for, and all previous operations that are no longer in the ready state will not be purged.
3. Step 2 may take a long time, especially if the calling thread is dormant. Also, the key associated with the selector may be canceled. When step 2 finishes, Step 1 will be re-executed to complete any one of the channels that have been canceled during the selection process and the key has been unregistered.
The value returned by the 4.select operation is the number of keys that the Ready collection has been modified in step 2, not the total number of channels in the selected Key's collection. The return value is not the total number of channels that have been prepared, but the number of channels that have entered the ready state since the last select call. Channels that are ready in the previous call and that are still ready in this call are not counted.
o Stop selection process
The selector class provides the method wakeup (), which allows the thread to gracefully exit from the blocked select () method, which returns the first selection operation on the selector that has not yet returned.
Call the Close () method of the Selector class, then any thread that blocks in the selection process will be awakened, the channel associated with the selector will be unregistered, and the key will be canceled.
In addition, the selector class can catch interruptedexception exceptions and call the Wakeup () method.
o Concurrency
• Scalability of the selection process
It might be a good idea to use one thread to serve multiple channels in a single CPU, but for a multi-CPU system, single threading is inherently worse than multithreading.
A good multithreading strategy is to use a selector (or multiple selectors, as appropriate) for all channels, and delegate the services of the ready channel to other threads. Use a thread to monitor the ready state of the channel and use a worker thread pool to process the received data. Speaking so much, let's look at a simple server code written in NiO:
private void run (int port) throws IOException {//Allocate buffer Bytebuffer Echobuffer = bytebuffer.allocate (1024) ; Create a new selector selector selector = Selector.open (); Open a listener the port, and register with the selector serversocketchannel SSC = Serversocketchannel.open (); Ssc.configureblocking (FALSE); ServerSocket ss = Ssc.socket (); Inetsocketaddress address = new inetsocketaddress (port); Ss.bind (address); Selectionkey key = Ssc.register (selector, selectionkey.op_accept); System.out.println ("Going to listen on" + port); for (;;) {int num = Selector.select (); Set Selectedkeys = Selector.selectedkeys (); Iterator it = Selectedkeys.iterator (); while (It.hasnext ()) {Selectionkey Selectionkey = (selectionkey) it.next (); if ((Selectionkey.readyops () & selectionkey.op_accept) = = selectionkey.op_accept) { Accept The New Connection Serversocketchannel Serversocketchannel = (serversocketchannel) selectionkey.channel (); Socketchannel sc = serversocketchannel.accept (); Sc.configureblocking (FALSE); ADD the new connection to the selector selectionkey NewKey = sc.register (selector, selectionkey.op_read) ; It.remove (); SYSTEM.OUT.PRINTLN ("Got connection from" + SC); } else if ((Selectionkey.readyops () & selectionkey.op_read) = = Selectionkey.op_read) { Read the data Socketchannel sc = (socketchannel) selectionkey.channel (); Echo data int bytesechoed = 0; while (true) {echobuffer.clear (); int r = sc.read (Echobuffer); if (r <= 0) {break; } echobuffer.flip (); Sc.write (echobufFER); Bytesechoed + = r; } System.out.println ("echoed" + bytesechoed + "from" + SC); It.remove (); } } }}
I/O multiplexing mode
There are two classic modes of I/O Multiplexing: Reactor based on synchronous I/O and proactor based on asynchronous I/O.
reactor
O an event handler claims that it is interested in reading events on a socket;
o The event separator is waiting for the event to occur;
o When the event has occurred, the event splitter is awakened, which is responsible for notifying the previous event handler;
o The event handler receives the message and then goes to the socket to read the data. If needed, it again claims to be interested in the read event on the socket, repeating the above steps;
Proactor
o The event handler sends a write operation directly (of course, the operating system must support this asynchronous operation). At this point, the event handler simply does not care about reading the event, it just sends the request, it is the completion of this write operation event. This handler is very drag, send a command, regardless of the specific things, just waiting for someone else (System) to help him fix the time to give him back a message.
o The event separator is waiting for the completion of this reading event (compared with reactor);
o When the event separator silently waiting to complete the matter, the operating system has started to work, it reads data from the target, into the user-provided buffer, and finally notify the event separator, this thing I finished;
O Event handlers prior to notification: the things you commanded were settled;
O Event handlers will now find that the data they want to read has been placed in the buffer he has provided, and what to do with it. If necessary, the event handler initiates another write operation as before, as in the previous steps.
Asynchronous Proactor is good, but it is limited to the operating system (to support asynchronous operations), in order to develop a truly independent platform of the common interface, we can use the reactor simulation to achieve proactor.
Proactor (Analog)
o Wait events (Proactor's work)
o Read the data (see, here it becomes to let proactor do this thing)
o Send the data ready message to the user handler function, i.e. the event handler (Proactor to do)
o Process data (user code to do)
Summarize
This paper introduces some basic concepts of I/O and 5 I/O models, NIO is the I/O multiplexing model in 5 models, and then goes into the topic Java NIO, respectively, about the three most important concepts in NIO: buffer, channel, selector; we also understand how NIO implements the I/O multiplexing model. Finally, two modes in I/O multiplexing mode are discussed: Reactor and Proactor, and how to simulate proactor with reactor.
Resources
O ' Reilly Java NIO
Richard Stevens "UNIX Network Programming Volume 1: Socket Networking API"
Comparison of two high-performance I/O design patterns (reactor/proactor)
Understanding Network I/O
Understanding Disk I/O-when should is worried?
Understanding Java NIO