Java NIO provides a different way of IO working than standard IO:
Channels and Buffers: Standard IO operates based on byte streams and character streams, while NIO operates based on channels (Channel) and buffers (Buffer). Data is always read from the channel to the buffer. area, or write from the buffer to the channel.
Asynchronous IO: Java NIO allows you to use IO asynchronously. For example, when a thread reads data from a channel into a buffer, the thread can still do other things. When data is written to the buffer, the thread can continue processing it. Writing to a channel from a buffer is similar.
Selectors: Java NIO introduces the concept of selectors, which are used to listen to events on multiple channels (for example: connection opening, data arrival). Therefore, a single thread can listen to multiple data channels.
Let’s introduce the relevant knowledge of Java NIO in detail.
Java NIO Overview
Java NIO consists of the following core parts:
Channels
Buffers
Selectors
Although there are many other classes and components in Java NIO, in my opinion, Channel, Buffer and Selector constitute the core API. Other components, such as Pipe and FileLock, are just utility classes used with the three core components. Therefore, in this overview I will focus on these three components. Other components are covered in separate chapters.
Channel and Buffer
Basically, all IO in NIO starts from a Channel. Channels are a bit like streams. Data can be read from the Channel to the Buffer, or written from the Buffer to the Channel. Here is an illustration:
There are several types of Channels and Buffers. The following are the implementations of some main Channels in JAVA NIO:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
As you can see, these channels cover UDP and TCP network IO, as well as file IO.
Along with these classes there are some interesting interfaces, but for the sake of simplicity I tried not to mention them in the overview. I will explain them in other chapters of this tutorial where they are relevant.
The following is the key Buffer implementation in Java NIO:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
These Buffers cover the basic data types you can send via IO: byte, short, int, long, float, double and char.
Java NIO also has a Mappyteuffer, which is used to represent memory mapped files. I am not going to explain it in the overview.
Selector
Selector allows a single thread to handle multiple Channels. If your application opens multiple connections (channels), but the traffic of each connection is very low, using Selector can be convenient. For example, in a chat server.
This is an illustration of using a Selector to process 3 Channels in a single thread:
To use a Selector, you must register a Channel with the Selector and then call its select() method. This method will block until a registered channel has an event ready. Once this method returns, the thread can handle these events. Examples of events are new connections coming in, data receiving, etc.
Java NIO vs. IO
(Original address of this part, author: Jakob Jenkov, translator: Guo Lei, proofreader: Fang Tengfei)
After learning about Java NIO and IO API, a question immediately came to mind:
Quote
When should I use IO and when should I use NIO? In this article, I will try to clearly explain the differences between Java NIO and IO, their usage scenarios, and how they affect your code design.
Main differences between Java NIO and IO
The following table summarizes the main differences between Java NIO and IO. I will describe the differences in each part of the table in more detail.
IO NIO
Stream oriented Buffer oriented
Blocking IO Non blocking IO
Selectors
Stream-oriented and buffer-oriented
The first biggest difference between Java NIO and IO is that IO is stream-oriented and NIO is buffer-oriented. Java IO is stream-oriented meaning that one or more bytes are read from the stream at a time, and until all bytes are read, they are not cached anywhere. Additionally, it cannot move data in the stream forward or backward. If you need to move the data read from the stream back and forth, you need to cache it in a buffer first. Java NIO's buffer-oriented approach is slightly different. The data is read into a buffer that it processes later, moving back and forth in the buffer as needed. This increases flexibility in processing. However, you also need to check that the buffer contains all the data you need to process. Also, make sure that as more data is read into the buffer, unprocessed data in the buffer is not overwritten.
Blocking and non-blocking IO
Various streams of Java IO are blocking. This means that when a thread calls read() or write(), the thread is blocked until some data is read, or the data is completely written. The thread cannot do anything else during this period. The non-blocking mode of Java NIO allows a thread to send a request to read data from a certain channel, but it can only get the currently available data. If no data is currently available, nothing will be obtained. Instead of keeping the thread blocked, the thread can continue to do other things until the data becomes readable. The same goes for non-blocking writes. A thread requests to write some data to a channel, but does not need to wait for it to be completely written. The thread can do other things in the meantime. Threads typically use idle time in non-blocking IO to perform IO operations on other channels, so a single thread can now manage multiple input and output channels.
Selectors
Java NIO's selectors allow a single thread to monitor multiple input channels. You can register multiple channels using a selector, and then use a separate thread to "select" channels: these channels already have input that can be processed. Or select a channel that is ready for writing. This selection mechanism makes it easy for a single thread to manage multiple channels.
How NIO and IO impact application design
Whether you choose an IO or NIO toolbox, there are several aspects that may affect your application design:
API calls to NIO or IO classes.
Data processing.
The number of threads used to process data.
API call
Of course, the API calls when using NIO look different than when using IO, but this is not unexpected because instead of just reading from an InputStream byte by byte, the data must first be read into a buffer and then processed.
Data processing
Using pure NIO design compared to IO design, data processing is also affected.
In IO design, we read data byte by byte from InputStream or Reader. Suppose you are processing a line-based text data stream, for example:
Copy the code code as follows:
Name: Anna
Age: 25
Email: [email protected]
Phone: 1234567890
The stream of text lines can be handled like this:
Copy the code code as follows:
InputStream input = … ; // get the InputStream from the client socket
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine= reader.readLine();
String phoneLine= reader.readLine();
Note that the processing status is determined by how long the program has been executing. In other words, once the reader.readLine() method returns, you know for sure that the line of text has been read. This is why readline() blocks until the entire line has been read. You also know that this line contains names; similarly, when the second readline() call returns, you know that this line contains ages, etc. As you can see, this handler only runs when new data is read in, and knows what the data is at each step. Once a running thread has processed some of the data it has read, it will not roll back the data (mostly). The following figure also illustrates this principle:
Reading data from a blocked stream
While a NIO implementation will be different, here is a simple example:
Copy the code code as follows:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
Note the second line, reading bytes from the channel into a ByteBuffer. When this method call returns, you don't know if all the data you need is in the buffer. All you know is that the buffer contains some bytes, which makes processing a bit difficult.
Suppose that after the first read(buffer) call, the data read into the buffer is only half a line, for example, "Name: An", can you process the data? Obviously not, you need to wait until the entire row of data is read into the cache. Before that, any processing of the data is meaningless.
So, how do you know if the buffer contains enough data to process? Well, you don't know. Discovered methods can only view data in the buffer. The result is that you have to check the buffer's data several times before you know that all the data is in the buffer. Not only is this inefficient, but it can also clutter the programming solution. For example:
Copy the code code as follows:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}
The bufferFull() method must keep track of how much data was read into the buffer and return true or false, depending on whether the buffer is full. In other words, if the buffer is ready to be processed, the buffer is full.
The bufferFull() method scans the buffer, but must remain in the same state as before the bufferFull() method was called. If not, the next data read into the buffer may not be read to the correct location. This is impossible, but it is yet another issue to be aware of.
If the buffer is full, it can be processed. If it doesn't work, and it makes sense in your actual case, you might be able to handle some of it. But in many cases this is not the case. The following figure shows "buffer data cycle ready":
Read data from a channel until all data is read into the buffer
Summarize
NIO lets you manage multiple channels (network connections or files) using just a single thread (or a few), but the trade-off is that parsing the data can be more complex than reading it from a blocking stream.
If you need to manage thousands of connections opened simultaneously that send only small amounts of data each time, such as a chat server, a server implementing NIO may be an advantage. Likewise, if you need to maintain many open connections to other computers, such as in a P2P network, it may be an advantage to use a separate thread to manage all your outbound connections. The design scheme of multiple connections in one thread is shown in the figure below:
Single thread manages multiple connections
If you have a small number of connections using very high bandwidth, sending large amounts of data at once, perhaps a typical IO server implementation might be a good fit. The following figure illustrates a typical IO server design:
A typical IO server design:
A connection is handled by a thread
Channel
Java NIO channels are similar to streams, but somewhat different:
Data can be read from the channel and data can be written to the channel. But reading and writing streams are usually one-way.
Channels can be read and written asynchronously.
Data in the channel must first be read from a Buffer, or always written from a Buffer.
As mentioned above, data is read from the channel to the buffer and data is written from the buffer to the channel. As shown below:
Channel implementation
These are the implementations of the most important channels in Java NIO:
FileChannel: Read and write data from files.
DatagramChannel: can read and write data in the network through UDP.
SocketChannel: Can read and write data in the network through TCP.
ServerSocketChannel: Can monitor incoming TCP connections, like a web server. A SocketChannel is created for each new incoming connection.
Basic Channel example
The following is an example of using FileChannel to read data into a Buffer:
Copy the code code as follows:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
Note that the call to buf.flip() first reads data into the Buffer, then reverses the Buffer, and then reads the data from the Buffer. The next section will go into more details about Buffer.
Buffer
Buffer in Java NIO is used to interact with NIO channels. As you know, data is read from the channel into the buffer and written from the buffer into the channel.
A buffer is essentially a block of memory to which data can be written and from which data can then be read. This memory is packaged as a NIO Buffer object and provides a set of methods to conveniently access this memory.
Basic usage of Buffer
Using Buffer to read and write data generally follows the following four steps:
Write data to Buffer
Call flip() method
Read data from Buffer
Call the clear() method or compact() method
When data is written to the buffer, the buffer records how much data was written. Once you want to read data, you need to switch the Buffer from write mode to read mode through the flip() method. In read mode, all data previously written to the buffer can be read.
Once all the data has been read, the buffer needs to be cleared so that it can be written to again. There are two ways to clear the buffer: calling the clear() or compact() method. The clear() method clears the entire buffer. The compact() method will only clear the data that has been read. Any unread data is moved to the beginning of the buffer, and newly written data is placed after the unread data in the buffer.
Here is an example of using Buffer:
Copy the code code as follows:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {
buf.flip();//make buffer ready for read
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // read 1 byte at a time
}
buf.clear(); //make buffer ready for writing
bytesRead = inChannel.read(buf);
}
aFile.close();
Buffer’s capacity, position and limit
A buffer is essentially a block of memory to which data can be written and from which data can then be read. This memory is packaged as a NIO Buffer object and provides a set of methods to conveniently access this memory.
In order to understand how Buffer works, you need to be familiar with its three properties:
capacity
position
limit
The meaning of position and limit depends on whether the Buffer is in read mode or write mode. No matter what mode the Buffer is in, the meaning of capacity is always the same.
Here is an explanation of capacity, position, and limit in read and write mode, with detailed explanations following the illustration.
capacity
As a memory block, Buffer has a fixed size value, also called "capacity". You can only write capacity of byte, long, char and other types into it. Once the Buffer is full, it needs to be emptied (by reading data or clearing data) before writing data can continue.
position
When you write data to the Buffer, position represents the current position. The initial position value is 0. When a byte, long, etc. data is written to the Buffer, the position will move forward to the next Buffer unit where data can be inserted. The maximum position can be capacity 1.
When data is read, it is also read from a specific location. When switching the Buffer from write mode to read mode, position will be reset to 0. When data is read from the Buffer's position, the position moves forward to the next readable position.
limit
In write mode, the limit of the Buffer indicates the maximum amount of data you can write to the Buffer. In write mode, limit is equal to the capacity of the Buffer.
When switching the Buffer to read mode, limit indicates the maximum amount of data you can read. Therefore, when switching the Buffer to read mode, limit will be set to the position value in write mode. In other words, you can read all the data written before (limit is set to the number of written data, this value is position in write mode)
Buffer type
Java NIO has the following Buffer types:
ByteBuffer
MappedByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
As you can see, these Buffer types represent different data types. In other words, the bytes in the buffer can be manipulated through char, short, int, long, float or double types.
MappedByteBuffer is a bit special and will be discussed in its own chapter.
Buffer allocation
To obtain a Buffer object, you must first allocate it. Every Buffer class has an allocate method. Below is an example of allocating a ByteBuffer with 48 bytes of capacity.
Copy the code code as follows:
ByteBuffer buf = ByteBuffer.allocate(48);
This allocates a CharBuffer that can store 1024 characters:
Copy the code code as follows:
CharBuffer buf = CharBuffer.allocate(1024);
Write data to Buffer
There are two ways to write data to Buffer:
Write from Channel to Buffer.
Write to Buffer through Buffer's put() method.
Example of writing from Channel to Buffer
Copy the code code as follows:
int bytesRead = inChannel.read(buf); //read into buffer.
Example of writing Buffer through put method:
Copy the code code as follows:
buf.put(127);
There are many versions of the put method, allowing you to write data to the Buffer in different ways. For example, writing to a specified location, or writing a byte array to a Buffer. For more details on Buffer implementation, refer to JavaDoc.
flip() method
The flip method switches the Buffer from write mode to read mode. Calling the flip() method will set the position back to 0 and set the limit to the value of the previous position.
In other words, position is now used to mark the reading position, and limit represents how many bytes, chars, etc. were written before - how many bytes, chars, etc. can be read now.
Read data from Buffer
There are two ways to read data from Buffer:
Read data from Buffer to Channel.
Use the get() method to read data from the Buffer.
Example of reading data from Buffer to Channel:
Copy the code code as follows:
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);
Example of using the get() method to read data from Buffer
Copy the code code as follows:
byte aByte = buf.get();
There are many versions of the get method, allowing you to read data from the Buffer in different ways. For example, read from a specified position, or read data from a Buffer into a byte array. For more details on Buffer implementation, refer to JavaDoc.
rewind() method
Buffer.rewind() sets position back to 0, so you can reread all data in the Buffer. The limit remains unchanged and still indicates how many elements (byte, char, etc.) can be read from the Buffer.
clear() and compact() methods
Once the data in the Buffer has been read, the Buffer needs to be ready to be written to again. This can be done via the clear() or compact() methods.
If the clear() method is called, position will be set back to 0 and limit will be set to the value of capacity. In other words, the Buffer is cleared. The data in the Buffer is not cleared, but these marks tell us where to start writing data into the Buffer.
If there is some unread data in the Buffer and you call the clear() method, the data will be "forgotten", which means there will no longer be any markers to tell you which data has been read and which has not.
If there is still unread data in the Buffer and the data is needed later, but you want to write some data first, use the compact() method.
The compact() method copies all unread data to the beginning of the Buffer. Then set the position to just behind the last unread element. The limit attribute is still set to capacity like the clear() method. The Buffer is now ready for writing data, but unread data will not be overwritten.
mark() and reset() methods
By calling the Buffer.mark() method, you can mark a specific position in the Buffer. You can later restore to this position by calling the Buffer.reset() method. For example:
Copy the code code as follows:
buffer.mark();
//call buffer.get() a couple of times, eg during parsing.
buffer.reset();//set position back to mark.
equals() and compareTo() methods
You can use equals() and compareTo() methods for two Buffers.
equals()
When the following conditions are met, it means that the two Buffers are equal:
Have the same type (byte, char, int, etc.).
The number of remaining bytes, chars, etc. in the Buffer is equal.
All remaining bytes, chars, etc. in the Buffer are the same.
As you can see, equals only compares part of the Buffer, not every element in it. In fact, it only compares the remaining elements in the Buffer.
compareTo() method
The compareTo() method compares the remaining elements (byte, char, etc.) of two Buffers. If the following conditions are met, one Buffer is considered "less than" the other Buffer:
The first unequal element is smaller than the corresponding element in the other Buffer.
All elements are equal, but the first Buffer is exhausted before the other (the first Buffer has fewer elements than the other).
(Annotation: The remaining elements are the elements from position to limit)
Scatter/Gather
(Original address of this part, author: Jakob Jenkov, translator: Guo Lei)
Java NIO begins to support scatter/gather. Scatter/gather is used to describe the operation of reading from or writing to Channel (Translator's Note: Channel is often translated as channel in Chinese).
Scatter reading from the Channel means writing the read data into multiple buffers during the read operation. Therefore, the Channel "scatters" the data read from the Channel into multiple Buffers.
Gathering and writing to a Channel means writing data from multiple buffers to the same Channel during a write operation. Therefore, the Channel "gathers" the data in multiple Buffers and sends them to the Channel.
Scatter/gather is often used in situations where the transmitted data needs to be processed separately. For example, when transmitting a message consisting of a message header and a message body, you may scatter the message body and message header into different buffers, so that you can conveniently Process message headers and message bodies.
Scattering Reads
Scattering Reads refers to reading data from one channel into multiple buffers. As described in the figure below:
The code example is as follows:
Copy the code code as follows:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
Note that the buffer is first inserted into the array, and then the array is used as an input parameter to channel.read(). The read() method writes the data read from the channel to the buffer in the order of the buffer in the array. When one buffer is filled, the channel writes to another buffer.
Scattering Reads must fill the current buffer before moving to the next buffer, which also means that it is not suitable for dynamic messages (Translator's Note: The message size is not fixed). In other words, if there is a message header and message body, the message header must be completely filled (for example, 128byte) for Scattering Reads to work properly.
Gathering Writes
Gathering Writes means that data is written from multiple buffers to the same channel. As described in the figure below:
The code example is as follows:
Copy the code code as follows:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
The buffers array is the input parameter of the write() method. The write() method will write data to the channel in the order of the buffers in the array. Note that only the data between position and limit will be written. Therefore, if a buffer has a capacity of 128 bytes but only contains 58 bytes of data, then the 58 bytes of data will be written to the channel. Therefore, contrary to Scattering Reads, Gathering Writes can handle dynamic messages better.
Data transfer between channels
(Original address of this part, author: Jakob Jenkov, translator: Guo Lei, proofreader: Zhou Tai)
In Java NIO, if one of the two channels is a FileChannel, then you can directly transfer data from one channel (Translator's Note: channel is often translated as channel in Chinese) to another channel.
transferFrom()
The transferFrom() method of FileChannel can transfer data from the source channel to the FileChannel (Translator's Note: This method is explained in the JDK documentation as transferring bytes from a given readable byte channel to the file of this channel. ). Here's a simple example:
Copy the code code as follows:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(position, count, fromChannel);
The input parameter position of the method indicates starting from position to write data to the target file, and count indicates the maximum number of bytes transferred. If the source channel has less than count bytes of remaining space, the number of bytes transferred is less than the number of bytes requested.
In addition, it should be noted that in the implementation of SoketChannel, SocketChannel will only transmit the data prepared at this moment (which may be less than count bytes). Therefore, the SocketChannel may not transfer all of the requested data (count bytes) into the FileChannel.
transferTo()
The transferTo() method transfers data from FileChannel to other channels. Here's a simple example:
Copy the code code as follows:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
Did you find that this example is particularly similar to the previous one? Except that the FileChannel object calling the method is different, everything else is the same.
The problems mentioned above about SocketChannel also exist in the transferTo() method. SocketChannel will continue to transmit data until the target buffer is filled.
Selector
(Link to the original text of this section, author: Jakob Jenkov, translator: Langjiv, proofreader: Ding Yi)
Selector is a component in Java NIO that can detect one or more NIO channels and know whether the channel is ready for events such as read and write. In this way, a single thread can manage multiple channels and thus multiple network connections.
(1)Why use Selector?
The advantage of using only a single thread to handle multiple Channels is that fewer threads are needed to handle the channels. In fact, it is possible to use only one thread to handle all channels. For the operating system, context switching between threads is very expensive, and each thread occupies some system resources (such as memory). Therefore, the fewer threads used, the better.
However, keep in mind that modern operating systems and CPUs are getting better and better at multitasking, so the overhead of multithreading becomes smaller and smaller over time. In fact, if a CPU has multiple cores, not using multitasking may be a waste of CPU power. Anyway, the discussion of that design should be in a different article. Here, it is enough to know that you can handle multiple channels using Selector.
The following is an example diagram of a single thread using a Selector to process three channels:
(2)Creation of Selector
Create a Selector by calling the Selector.open() method, as follows:
Copy the code code as follows:
Selector selector = Selector.open();
(3) Register the channel with Selector
In order to use Channel and Selector together, the channel must be registered with the selector. This is achieved through the SelectableChannel.register() method, as follows:
Copy the code code as follows:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,
Selectionkey.OP_READ);
When used with a Selector, the Channel must be in non-blocking mode. This means that you cannot use FileChannel with a Selector because FileChannel cannot be switched to non-blocking mode. Socket channels are fine.
Note the second parameter of the register() method. This is an "interest collection", which means what events you are interested in when listening to the Channel through the Selector. There are four different types of events that can be listened to:
Connect
Accept
Read
Write
A channel triggering an event means that the event is ready. Therefore, a channel that successfully connects to another server is called "connection ready". A server socket channel is said to be "ready to receive" when it is ready to receive incoming connections. A channel that has data to read is said to be "read-ready". A channel waiting to write data can be said to be "write-ready".
These four events are represented by the four constants of SelectionKey:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
If you are interested in more than one event, you can use the bitwise OR operator to connect the constants, as follows:
Copy the code code as follows:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
Interest collections will be mentioned below.
(4)SelectionKey
In the previous section, when registering a Channel with the Selector, the register() method returns a SelectionKey object. This object contains some properties that may be of interest to you:
interest collection
ready collection
Channel
Selector
Additional objects (optional)
Below I describe these properties.
interest collection
As described in the Registering a Channel with a Selector section, the interest collection is a collection of interesting events that you select. You can read and write the interest collection through SelectionKey, like this:
Copy the code code as follows:
int interestSet = selectionKey.interess();
boolean isInterestedInAccept= (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
It can be seen that by using "bit AND" to operate the interest collection and the given SelectionKey constant, you can determine whether a certain event is in the interest collection.
ready collection
The ready set is the set of operations for which the channel is ready. After a selection (Selection), you will first access the ready set. Selection will be explained in the next section. The ready collection can be accessed like this:
int readySet = selectionKey.readyOps();
You can use the same method as detecting the interest collection to detect what events or operations are ready in the channel. However, the following four methods are also available, all of which return a Boolean type:
Copy the code code as follows:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel+Selector
Accessing Channel and Selector from SelectionKey is simple. as follows:
Copy the code code as follows:
Channelchannel= selectionKey.channel();
Selector selector = selectionKey.selector();
additional objects
An object or more information can be attached to the SelectionKey to easily identify a given channel. For example, you can attach a Buffer for use with a channel, or an object that contains aggregated data. How to use it:
Copy the code code as follows:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
You can also attach objects when registering the Channel with the Selector using the register() method. like:
Copy the code code as follows:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
(5) Select the channel through Selector
Once one or more channels are registered with a Selector, several overloaded select() methods can be called. These methods return those channels that are ready for the event you are interested in (such as connect, accept, read, or write). In other words, if you are interested in "read-ready" channels, the select() method will return those channels for which read events are ready.
Here is the select() method:
int select()
int select(long timeout)
int selectNow()
select() blocks until at least one channel is ready for the event you registered.
select(long timeout) is the same as select(), except that it will block for up to timeout milliseconds (parameter).
selectNow() does not block and returns immediately no matter what channel is ready (Translator's Note: This method performs a non-blocking selection operation. If no channel becomes selectable since the previous selection operation, this method directly returns zero .).
The int value returned by the select() method indicates how many channels are ready. That is, how many channels have become ready since the last call to the select() method. If the select() method is called, 1 is returned because one channel becomes ready. If the select() method is called again, if another channel is ready, it will return 1 again. If no operations are done on the first ready channel, there are now two ready channels, but between each select() method call, only one channel is ready.
selectedKeys()
Once the select() method is called and the return value indicates that one or more channels are ready, the ready channels in the "selected key set" can then be accessed by calling the selector's selectedKeys() method. As shown below:
Copy the code code as follows:
Set selectedKeys = selector.selectedKeys();
When registering a Channel like a Selector, the Channel.register() method returns a SelectionKey object. This object represents the channel registered to the Selector. These objects can be accessed through SelectionKey's selectedKeySet() method.
Ready channels can be accessed by traversing this selected set of keys. as follows:
Copy the code code as follows:
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.<tuihighlight><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;">remove</a ></tuihighlight>();
}
This loop iterates through each key in the selected key set and detects the ready event for the channel corresponding to each key.
Note the keyIterator.remove() call at the end of each iteration. The Selector does not remove SelectionKey instances from the selected key set itself. Must be removed yourself when the channel is processed. The next time the channel becomes ready, the Selector will put it into the selected key set again.
The channel returned by the SelectionKey.channel() method needs to be converted into the type you want to process, such as ServerSocketChannel or SocketChannel, etc.
(6)wakeUp()
A thread is blocked after calling the select() method. Even if no channel is ready, there is a way to return it from the select() method. Just let other threads call the Selector.wakeup() method on the object where the first thread called the select() method. The thread blocked on the select() method will return immediately.
If another thread calls the wakeup() method, but no thread is currently blocked on the select() method, the next thread that calls the select() method will "wake up" immediately.
(7)close()
Calling its close() method after using the Selector will close the Selector and invalidate all SelectionKey instances registered to the Selector. The channel itself does not close.
(8) Complete example
Here is a complete example, open a Selector, register a channel to the Selector (the initialization process of the channel is omitted), and then continuously monitor whether the Selector's four events (accept, connect, read, write) are ready.
Copy the code code as follows:
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.select();
if(readyChannels == 0) continue;
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.<tuihighlight><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;">remove</a ></tuihighlight>();
}
}
file channel
(Link to the original text of this section, author: Jakob Jenkov, translator: Zhou Tai, proofreader: Ding Yi)
FileChannel in Java NIO is a channel connected to a file. Files can be read and written through file channels.
FileChannel cannot be set to non-blocking mode, it always runs in blocking mode.
OpenFileChannel
Before using FileChannel, it must be opened. However, we cannot open a FileChannel directly. We need to obtain a FileChannel instance by using an InputStream, OutputStream or RandomAccessFile. Here is an example of opening a FileChannel via RandomAccessFile:
Copy the code code as follows:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
Read data from FileChannel
Call one of the multiple read() methods to read data from the FileChannel. like:
Copy the code code as follows:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
First, allocate a Buffer. Data read from FileChannel will be read into Buffer.
Then, call the FileChannel.read() method. This method reads data from FileChannel into Buffer. The int value returned by the read() method indicates how many bytes were read into the Buffer. If it returns -1, it means the end of the file has been reached.
Write data to FileChannel
Use the FileChannel.write() method to write data to FileChannel. The parameter of this method is a Buffer. like:
Copy the code code as follows:
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
Note that FileChannel.write() is called in a while loop. Because there is no guarantee how many bytes the write() method can write to the FileChannel at one time, the write() method needs to be called repeatedly until there are no bytes in the Buffer that have not been written to the channel.
CloseFileChannel
The FileChannel must be closed when you are finished with it. like:
Copy the code code as follows:
channel.close();
FileChannel position method
Sometimes it may be necessary to read/write data at a specific location in the FileChannel. You can get the current position of FileChannel by calling the position() method.
You can also set the current position of FileChannel by calling the position(long pos) method.
Here are two examples:
Copy the code code as follows:
long pos = channel.position();
channel.position(pos +123);
If you set the position after the end of file and then try to read data from the file channel, the read method will return -1 - the end of file flag.
If you set the position after the end of file and then write data to the channel, the file will be expanded to the current position and the data will be written. This can lead to "file holes", gaps between the data written in the physical files on the disk.
FileChannel size method
The size() method of a FileChannel instance will return the size of the file associated with the instance. like:
Copy the code code as follows:
long fileSize = channel.size();
FileChannel's truncate method
You can use the FileChannel.truncate() method to intercept a file. When intercepting a file, the part after the specified length of the file will be deleted. like:
Copy the code code as follows:
channel.truncate(1024);
This example intercepts the first 1024 bytes of the file.
Force method of FileChannel
The FileChannel.force() method forces data in the channel that has not yet been written to disk to disk. For performance reasons, the operating system caches data in memory, so there is no guarantee that data written to FileChannel will be written to disk immediately. To ensure this, the force() method needs to be called.
The force() method has a boolean parameter that indicates whether to write file metadata (permission information, etc.) to the disk at the same time.
The following example forces both file data and metadata to disk:
Copy the code code as follows:
channel.force(true);
Socket channel
(Link to the original text of this section, author: Jakob Jenkov, translator: Zheng Yuting, proofreader: Ding Yi)
SocketChannel in Java NIO is a channel connected to a TCP network socket. SocketChannel can be created in the following 2 ways:
Open a SocketChannel and connect to a server on the Internet.
When a new connection arrives at ServerSocketChannel, a SocketChannel is created.
Open SocketChannel
The following is how to open SocketChannel:
Copy the code code as follows:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
Close SocketChannel
When you are done with the SocketChannel, call SocketChannel.close() to close the SocketChannel:
Copy the code code as follows:
socketChannel.close();
Read data from SocketChannel
To read data from a SocketChannel, call one of the read() methods. Here are examples:
Copy the code code as follows:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
First, allocate a Buffer. The data read from SocketChannel will be placed in this Buffer.
Then, call SocketChannel.read(). This method reads data from SocketChannel into Buffer. The int value returned by the read() method indicates how many bytes were read into the Buffer. If -1 is returned, it means that the end of the stream has been read (the connection was closed).
Write to SocketChannel
Writing data to SocketChannel uses the SocketChannel.write() method, which takes a Buffer as a parameter. Examples are as follows:
Copy the code code as follows:
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
Note that the SocketChannel.write() method is called in a while loop. The Write() method cannot guarantee how many bytes can be written to the SocketChannel. So, we call write() repeatedly until the Buffer has no bytes left to write.
non-blocking mode
You can set SocketChannel to non-blocking mode. After setting, you can call connect(), read() and write() in asynchronous mode.
connect()
If SocketChannel is in non-blocking mode and connect() is called at this time, the method may return before the connection is established. To determine whether the connection is established, you can call the finishConnect() method. Like this:
Copy the code code as follows:
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
while(! socketChannel.finishConnect() ){
//wait, or do something else...
}
write()
In non-blocking mode, the write() method may return before writing anything. So write() needs to be called in the loop. There have been examples before, so I won’t go into details here.
read()
In non-blocking mode, the read() method may return before any data has been read. So you need to pay attention to its int return value, which will tell you how many bytes were read.
Non-blocking mode and selectors
Non-blocking mode works better with selectors. By registering one or more SocketChannels with the Selector, you can ask the selector which channel is ready for reading, writing, etc. The combination of Selector and SocketChannel will be discussed in detail later.
ServerSocket channel
(Link to the original text of this section, author: Jakob Jenkov, translator: Zheng Yuting, proofreader: Ding Yi)
ServerSocketChannel in Java NIO is a channel that can listen to new incoming TCP connections, just like ServerSocket in standard IO. The ServerSocketChannel class is in the java.nio.channels package.
Here is an example:
Copy the code code as follows:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//do something with socketChannel...
}
Open ServerSocketChannel
Open the ServerSocketChannel by calling the ServerSocketChannel.open() method. For example:
Copy the code code as follows:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Close ServerSocketChannel
Close ServerSocketChannel by calling ServerSocketChannel.close() method. For example:
Copy the code code as follows:
serverSocketChannel.close();
Listen for new incoming connections
Listen for new incoming connections through the ServerSocketChannel.accept() method. When the accept() method returns, it returns a SocketChannel containing the new incoming connection. Therefore, the accept() method will block until a new connection arrives.
Usually instead of just listening to one connection, the accept() method is called in the while loop. As in the following example:
Copy the code code as follows:
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//do something with socketChannel...
}
Of course, you can also use other exit criteria besides true in the while loop.
non-blocking mode
ServerSocketChannel can be set to non-blocking mode. In non-blocking mode, the accept() method will return immediately. If there is no new incoming connection, the return value will be null. Therefore, you need to check whether the returned SocketChannel is null. like:
Copy the code code as follows:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
if(socketChannel != null){
//do something with socketChannel...
}
}
Datagram channel
(Link to the original text of this section, author: Jakob Jenkov, translator: Zheng Yuting, proofreader: Ding Yi)
DatagramChannel in Java NIO is a channel that can send and receive UDP packets. Because UDP is a connectionless network protocol, it cannot be read and written like other channels. It sends and receives data packets.
OpenDatagramChannel
Here is how DatagramChannel is opened:
Copy the code code as follows:
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
The DatagramChannel opened by this example can receive packets on UDP port 9999.
receive data
Receive data from DatagramChannel through the receive() method, such as:
Copy the code code as follows:
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
The receive() method will copy the received data packet content to the specified Buffer. If the Buffer cannot accommodate the received data, the excess data will be discarded.
Send data
Send data from DatagramChannel through the send() method, such as:
Copy the code code as follows:
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));
This example sends a string of characters to UDP port 80 of the "jenkov.com" server. Because the server is not monitoring this port, nothing will happen. It also won't notify you whether the outgoing packet has been received, because UDP does not have any guarantees in terms of data delivery.
Connect to a specific address
A DatagramChannel can be "connected" to a specific address in the network. Since UDP is connectionless, connecting to a specific address does not create a real connection like a TCP channel. Instead, the DatagramChannel is locked so that it can only send and receive data from a specific address.
Here is an example:
Copy the code code as follows:
channel.connect(new InetSocketAddress("jenkov.com", 80));
Once connected, you can also use the read() and write() methods just like you would with a traditional channel. There are just no guarantees regarding data transfer. Here are a few examples:
Copy the code code as follows:
int bytesRead = channel.read(buf);
int bytesWritten = channel.write(but);
Pipe
(Link to the original text of this section, author: Jakob Jenkov, translator: Huang Zhong, proofreader: Ding Yi)
A Java NIO pipe is a one-way data connection between 2 threads. Pipe has a source channel and a sink channel. Data will be written to the sink channel and read from the source channel.
Here is an illustration of the Pipe principle:
Create pipeline
Open the pipe through the Pipe.open() method. For example:
Copy the code code as follows:
Pipe pipe = Pipe.open();
Write data to the pipe
To write data to the pipe, you need to access the sink channel. Like this:
Copy the code code as follows:
Pipe.SinkChannel sinkChannel = pipe.sink();
Write data to SinkChannel by calling the write() method of SinkChannel, like this:
Copy the code code as follows:
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
<b>sinkChannel.write(buf);</b>
}
[code]
Read data from pipe
To read data from a pipe, you need to access the source channel, like this:
[code]
Pipe.SourceChannel sourceChannel = pipe.source();
Call the read() method of the source channel to read the data, like this:
Copy the code code as follows:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
The int value returned by the read() method will tell us how many bytes were read into the buffer.