java Learning - NIO (III) Channel

Keywords: socket Java ascii

Channel is the second major innovation of java.nio. They are neither extensions nor enhancements, but new and excellent Java I/O examples that provide direct connections to I/O services. Channel is used to efficiently transfer data between byte buffers and entities (usually a file or socket) on the other side of the channel.

Introduction to channel

Channels are conduits for accessing I/O services. I/O can be divided into two broad categories: File I/O and Stream I/O. It's not surprising that there are two types of channels, file and socket. We see a FileChannel class and three socket channel classes in the api: Socket Channel, Server Socket Channel and Datagram Channel.

Channels can be created in many ways. Socket channels have factory methods to create new socket channels directly. But a FileChannel object can only be obtained by calling getChannel() on an open Random Access File, FileInputStream, or FileOutputStream object. You can't create a FileChannel object directly.

Let's first look at the use of FileChannel:

   // Create a file output byte stream
   FileOutputStream fos = new FileOutputStream("data.txt");
   //Get the file channel
   FileChannel fc = fos.getChannel();
   //Write ByteBuffer to the channel
   fc.write(ByteBuffer.wrap("Some text ".getBytes()));
   //Closed flow
   fos.close();

   //Random Access Files
   RandomAccessFile raf = new RandomAccessFile("data.txt", "rw");
   //Get the file channel
   fc = raf.getChannel();
   //Set the file location of the channel to the end
   fc.position(fc.size()); 
   //Write ByteBuffer to the channel
   fc.write(ByteBuffer.wrap("Some more".getBytes()));
   //Close
   raf.close();

   //Create File Input Stream
   FileInputStream fs = new FileInputStream("data.txt");
   //Get the file channel
   fc = fs.getChannel();
   //Allocation of ByteBuffer Space Size
   ByteBuffer buff = ByteBuffer.allocate(BSIZE);
   //Reading ByteBuffer from Channels
   fc.read(buff);
   //Call this method to prepare for a series of channel writes or relative acquisitions
   buff.flip();
   //Read bytes from ByteBuffer in turn and print them
   while (buff.hasRemaining()){
       System.out.print((char) buff.get());
   }
   fs.close();

Take another look at Socket Channel:

 SocketChannel sc = SocketChannel.open( );
 sc.connect (new InetSocketAddress ("somehost", someport)); 
 ServerSocketChannel ssc = ServerSocketChannel.open( ); 
 ssc.socket( ).bind (new InetSocketAddress (somelocalport)); 
 DatagramChannel dc = DatagramChannel.open( );

SocketChannel can be set to non-blocking mode. Once set, connect(), read() and write() can be invoked in asynchronous mode. If SocketChannel calls connect() in non-blocking mode, the method may return before the connection is established. To determine whether a connection is established, you can call the finishConnect() method. Like this:

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

while(! socketChannel.finishConnect() ){
    //wait, or do something else...
}

Server-side usage often takes into account non-blocking socket channels, because they make it easier to manage many socket channels at the same time. However, it is also beneficial to use one or more non-blocking socket channels on the client side. For example, with non-blocking socket channels, GUI programs can focus on user requests and maintain sessions with one or more servers at the same time. Non-blocking mode is useful in many programs.

Call the finishConnect() method to complete the connection process, which can be safely invoked at any time. If the finishConnect() method is called on a non-blocking SocketChannel object, one of the following scenarios may occur:

  • The connect() method has not yet been called. Then a NoConnectionPendingException exception will be generated.
  • The connection establishment process is in progress and is not yet complete. Then nothing happens, and the finishConnect() method immediately returns the false value.
  • After calling the connect() method in non-blocking mode, SocketChannel is switched back to blocking mode. Then, if necessary, the calling thread blocks until the connection is established, and the finishConnect() method then returns the true value. After the first call to connect() or the last call to finishConnect(), the connection establishment process has been completed. Then the internal state of the SocketChannel object will be updated to the connected state, the finishConnect() method will return the true value, and then the SocketChannel object can be used to transfer data.
  • The connection has been established. Then nothing happens, and the finishConnect() method returns the true value.

Socket channels are thread-safe. Concurrent access does not require special measures to protect multiple threads that initiate access, but only one read operation and one write operation are in progress at any time. Keep in mind that sockets are stream-oriented rather than package-oriented. They guarantee that the bytes sent will arrive sequentially but cannot promise to maintain the byte grouping. A sender may write 20 bytes to a socket and the receiver receives only three bytes when it calls the read() method. The remaining 17 bytes are still in transit. For this reason, sharing the same side of a stream socket among multiple uncooperative threads is by no means a good design option.

Finally, take a look at Datagram Channel:

The last socket channel is Datagram Channel. Just as Socket Channel corresponds to Socket, Server Socket Channel corresponds to Server Socket, each Datagram Channel object also has an associated Datagram Socket object. However, the original naming model does not apply here: "Datagram Socket Channel" seems a bit clumsy, so the use of a concise "Datagram Channel" name.

Just as Socket Channel simulates connection-oriented flow protocols (such as TCP/IP), Datagram Channel simulates packet-oriented Connectionless Protocols (such as UDP/IP):

The pattern for creating Datagram Channel is the same as for creating other socket channels: calling static open() methods to create a new instance. The new Datagram Channel will have a peer Datagram Socket object that can be obtained by calling the socket() method. The DatagramChannel object can act as either a server (listener) or a client (sender). If you want the newly created channel to be responsible for listening, then the channel must first be bound to a port or address/port combination. Binding a Datagram Channel is no different from binding a regular Datagram Socket, which is implemented by delegating an API on a peer-to-peer socket object:

 DatagramChannel channel = DatagramChannel.open( );
 DatagramSocket socket = channel.socket( ); 
 socket.bind (new InetSocketAddress (portNumber));

Datagram Channel is connectionless. Each datagram is a self-contained entity with its own destination address and data payload independent of other datagrams. Unlike stream-oriented socket s, Datagram Channel can send separate datagrams to different destination addresses. Similarly, DatagramChannel objects can receive packets from any address. Each incoming datagram contains information about where it comes from (source address).

An unbounded Datagram Channel can still receive packets. When an underlying socket is created, a dynamically generated port number is assigned to it. Binding behavior requires that the port associated with the channel be set to a specific value (this process may involve security checks or other validation). Whether or not the channel is bound, all packets sent contain the source address (with port number) of the Datagram Channel. Unbound Datagram Channel can receive and receive packets sent to its port, usually in response to a packet sent before the channel. Binded channels receive packets sent to the well-known ports they bind. The actual sending or receiving of data is realized by sending () and receiving ().

Note: If the ByteBuffer you provide does not have enough space to store the packets you are receiving, unfilled bytes will be quietly discarded.

Scatter/Gather

Channels provide an important new function called Scatter/Gather (sometimes referred to as vector I/O). It refers to the implementation of a simple I/O operation on multiple buffers. For a write operation, data is extracted sequentially (called gather) from several buffers and sent along the channel. Buffers themselves don't need the ability to gather (usually they don't). The effect of the gather process is like that the contents of all buffers are linked together and stored in a large buffer before sending data. For read operations, the data read from the channel is sequentially distributed (called scatter) to multiple buffers, filling each buffer until the data in the channel or the maximum space of the buffer is consumed.

scatter / gather is often used in situations where the transmitted data needs to be processed separately, such as transmitting a message consisting of a header and a message body. You may disperse the message body and the header into different buffer s so that you can easily handle the header and the message body.

Scattering Reads refers to data read from one channel to multiple buffer s. Describe as follows:

The code example is as follows:

ByteBuffer header = ByteBuffer.allocateDirect (10); 
ByteBuffer body = ByteBuffer.allocateDirect (80); 
ByteBuffer [] buffers = { header, body }; 
int bytesRead = channel.read (buffers);

Gathering Writes refers to data written from multiple buffer s to the same channel. Describe as follows:

The code example is as follows:

 ByteBuffer header = ByteBuffer.allocateDirect (10); 
 ByteBuffer body = ByteBuffer.allocateDirect (80); 
 ByteBuffer [] buffers = { header, body }; 
 channel.write(bufferArray);

Scatter/Gather can be an extremely powerful tool when used properly. It allows you to delegate to the operating system the hard work of storing read data separately into multiple bucket s or merging different data blocks into a whole. This is a huge achievement, because the operating system has been highly optimized to do this kind of work. It saves you the work of moving data back and forth, avoids buffer copy and reduces the amount of code you need to write and debug. Since you basically combine data by providing data container references, building multiple buffer array references according to different combinations allows various data blocks to be combined in different ways. The following examples illustrate this well:

public class GatheringTest {
    private static final String DEMOGRAPHIC = "output.txt";
    public static void main (String [] argv) throws Exception {
        int reps = 10;
        if (argv.length > 0) {
            reps = Integer.parseInt(argv[0]);
        }
        FileOutputStream fos = new FileOutputStream(DEMOGRAPHIC);
        GatheringByteChannel gatherChannel = fos.getChannel();

        ByteBuffer[] bs = utterBS(reps);

        while (gatherChannel.write(bs) > 0) {
            // Do not operate, let the channel output data to the file to finish writing
        }
        System.out.println("Mindshare paradigms synergized to " + DEMOGRAPHIC);
        fos.close();
    }
    private static String [] col1 = { "Aggregate", "Enable", "Leverage",
                                      "Facilitate", "Synergize", "Repurpose",
                                      "Strategize", "Reinvent", "Harness"
                                    };

    private static String [] col2 = { "cross-platform", "best-of-breed", "frictionless",
                                      "ubiquitous", "extensible", "compelling",
                                      "mission-critical", "collaborative", "integrated"
                                    };

    private static String [] col3 = { "methodologies", "infomediaries", "platforms", "schemas", "mindshare", "paradigms", "functionalities", "web services", "infrastructures" };

    private static String newline = System.getProperty ("line.separator");


    private static ByteBuffer [] utterBS (int howMany) throws Exception {
        List list = new LinkedList();
        for (int i = 0; i < howMany; i++) {
            list.add(pickRandom(col1, " "));
            list.add(pickRandom(col2, " "));
            list.add(pickRandom(col3, newline));
        }
        ByteBuffer[] bufs = new ByteBuffer[list.size()];
        list.toArray(bufs);
        return (bufs);
    }
    private static Random rand = new Random( );


    /**
     * Random Generation Characters
     * @param strings
     * @param suffix
     * @return
     * @throws Exception
     */
    private static ByteBuffer pickRandom (String [] strings, String suffix) throws Exception {
        String string = strings [rand.nextInt (strings.length)];
        int total = string.length() + suffix.length( );
        ByteBuffer buf = ByteBuffer.allocate (total);
        buf.put (string.getBytes ("US-ASCII"));
        buf.put (suffix.getBytes ("US-ASCII"));
        buf.flip( );
        return (buf);
    }
}

Output is:

 Reinvent integrated web services
 Aggregate best-of-breed platforms
 Harness frictionless platforms
 Repurpose extensible paradigms
 Facilitate ubiquitous methodologies
 Repurpose integrated methodologies
 Facilitate mission-critical paradigms
 Synergize compelling methodologies
 Reinvent compelling functionalities
 Facilitate extensible platforms

Although this output doesn't make sense, gather does make it easy for us to export it.

Pipe

The java.nio.channels package contains a class called Pipe. Broadly speaking, a pipeline is a conduit used to transmit data unilaterally between two entities.
The Java NIO pipeline is a one-way data connection between two threads. Pipe has a source channel and a sink channel. The data is written to the sink channel and read from the source channel. The Pipe class creates a pair of Channel objects that provide loopback mechanisms. The distal ends of the two channels are connected so that any data written on the InkChannel object can appear on the SourceChannel object.

Let's create a Pipe and write data to Pipe:

//Through Pipe.open()Method Open Pipeline
Pipe pipe = Pipe.open();

//To write data to the pipeline, you need to access the sink channel
Pipe.SinkChannel sinkChannel = pipe.sink();

//Write data to SinkChannel by calling the write() method of SinkChannel
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()) {
    sinkChannel.write(buf);
}

Look at how to read data from the pipeline:

To read the data from the pipeline, you need to access the source channel:

Pipe.SourceChannel sourceChannel = pipe.source();

Call the read() method of the source channel to read the data:

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);

The int value returned by the read() method tells us how many bytes have been read into the buffer.

So far we have finished the simple usage of channels. If you want to use them, you have to practice and simulate them more, so that you can know when to use them and how to use them. In the next section, we will talk about selector-Selectors.

Posted by edsmith on Sat, 20 Apr 2019 17:33:33 -0700