NIO学习
NIO学习
Why NIO
Blocking IO + ThreadPool ?
for protocols with long-lived connections, the size of the thread pool still limits the number of clients that can be handled simultaneously. Increasing the thread pool size increases the thread-handling overhead without improving performance, because most of the time clients are idle.
Shared State among different thread: synchronization - complexity, scheduling and context-switching overhead
That's why me programmers prefer to stick with a single-threaded approach, in which the server has only one thread, which deals with all clients—not sequentially, but all at once.
Components
Channel
A Channel
instance represents a “pollable” I/O target such as a socket (or a file, or a device). Channels can register an instance of class Selector
. The select()
method of Selector
allows you to ask “Among the set of channels, which ones are currently ready to be serviced (i.e., accepted, read, or written)?”
Factory Method
SocketChannel clntChan = SocketChannel.open(); |
Channel Send data to buffer Or Receive data From buffer. (Read into / Write from)
Part of the power of NIO comes from the fact that channels can be made nonblocking.
clntChan.configureBlocking(false);
clntChan.configureBlocking(false); |
Buffer
Just as selectors and channels give greater control and predictability of the overhead involved with handling many clients at once, Buffer
enables more efficient, predictable I/O than is possible with the Stream
abstraction
The Buffer
abstraction represents a finite-capacity container for data—essentially, an array with associated pointers indicating where to put data in, and where to read data out.
The ByteBuffer
is the most flexible one.
ByteBuffer buffer = ByteBuffer.allocate(CAPACITY); |
ByteBuffer buffer = ByteBuffer.wrap(byteArray); |
Read Into and Write From buffer.
Index | Description | Accessor/Mutator/Usage |
---|---|---|
capacity | Number of elements in buffer (Immutable) | int capacity() |
position | Next element to read/write | int position() |
(numbered from 0) | Buffer position (int newPosition ) |
|
limit | First unreadable/unwritable element | int limit() |
Buffer limit (int newLimit ) |
||
mark | User-chosen prev. value of position, or 0 | Buffer mark() |
Buffer reset() |
The distance between the position and limit tells us the number of bytes available for getting/putting.
0 *≤ mark ≤ position ≤ limit ≤ capacity*
hasRemaining()
returns TRUE if at least one element is available, and remaining()
returns the number of elements available.
The mark value “remembers” a position so you can come back to it later; the reset()
method returns the position to the value it had when mark()
was last called (unless doing so would violate the above invariant).
Storing and Retrieve Data
put: place data into buffer, read implicitly calls put
get: retrieve data from buffer, write implicitly calls get
Each call to a relative put()
or get()
advances the value of position by the length of the particular parameter type: 2 for short
, 4 for int
, etc.
Preparing Buffer
flip
: prepare buffer for write/get
first channel read , then want to use this buffer for write : set position to 0, limit to previous position
clear
: prepare buffer for read/put
Setting position to 0, limit to capacity
rewind
: prepare buffer for rewrite
sets position to zero and invalidates the mark. It’s similar to flip()
except limit remains unchanged.
// Start with buffer ready for writing |
compact
The compact()
operation copies the elements between position and limit to the start of the buffer, to make room for subsequent put()
/read()
calls.
By calling compact()
after the write()
but before the read()
that will add more data, we move all the “left over” data to the start of the buffer, freeing up the maximum space for new data.
Selector
The Selector
class allows us to avoid the wasteful “busy waiting” approach we saw in the nonblocking client.
An instance of Selector
can simultaneously check (and wait, if desired) for I/O opportunities on a set of channels, a selector is a multiplexor because a single selector can manage I/O on multiple channels.
To use a selector, create it (using the static factory method open()
) and register it with the channels that you wish to monitor (note that this is done via a method of the channel, not the selector). Finally, call the selector’s select()
method, which blocks until one or more channels are ready for I/O or a timeout expires.
When select()
returns, it tells you the number of channels ready for I/O. Then check for ready I/O on several channels by calling select()
each selector has an associated set of channels which it monitors for specific I/O “operations of interest” to that channel.
As we have seen, each selector has an associated set of channels which it monitors for specific I/O “operations of interest” to that channel. The association between a Selector
and a Channel
is represented by an instance of SelectionKey
.
A channel is registered with a selector by calling the channel’s register()
method.
selecting and identifying ready channels
After selection, we need to know which channels have ready I/O of interest. Each selector maintains a selected-key set containing the keys from the key set whose associated channels have impending I/O of interest.
int select() |
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator(); |
To summarize, here are the steps in using a Selector
:
- Create a selector instance.
- Register it with various channels, specifying I/O operations of interest for each channel.
- Repeatedly:
- Call one of the select methods.
- Get the list of selected keys.
- For each key in the selected-keys set,
- Fetch the channel and (if applicable) attachment from the key
- Determine which operations are ready and perform them. If an accept operation, set the accepted channel to nonblocking and register it with the selector
- Modify the key’s operation interest set if needed
- Remove the key from the selected-keys set
https://zhuanlan.zhihu.com/p/111816019