Search code examples
javanioreadlinewebsocketbytebuffer

How do I limit reading a line at a time from a SocketChannel InputStream using Java NIO


I am trying to write a Websockets client and server. Initially the connection is HTTP and the Websockets handshake uses HTTP headers to indicate that an upgrade to a new protocol is necessary on the connection.

I want to read in the set of HTTP headers from the SocketChannel and, if an upgrade is indicated, switch over to a different library for handling Websockets and from that point on handle the SocketChannel streams completely differently, as a set of frames rather than lines delimited with \r\n.

I know I can read an arbitrary number of bytes into a ByteBuffer, but a Websockets frame may have been sent with the handshake and I don't want to be passing off half-consumed buffers between these sections of code. What I want is to read from the socket only the data up to and including sequence "\r\n\r\n". Any data beyond that I want to leave in the SocketChannel object input stream.

What is the recommended way to do this? Get the input stream from the SocketChannel and wrap it in a buffered reader? Would this interact properly with NIO, particularly non-blocking uses? Could I drop the buffered reader from the input stream once the blank line was detected and still have all the frame data available when the channel is passed to the Websockets code?

Or perhaps I need to read byte-by-byte (or 4 byte chunks with smaller buffers if some of the target "\r\n\r\n" characters appear at the end of the chunk) and build up my header strings that way.

Or maybe some combination of manipulating mark, limit and position would allow the input stream to get back data it had previously read into the ByteBuffer provided the buffer was allocated directly.

Any advice would be greatly appreciated.


Solution

  • I would recommend using something like Apache Mina or Grizzly. Both allow you to encapsulated the protocol aspect of your problem so you only have to handle consumable data.

    However if you want a quick and dirty way: The basic idea though is yeah you need to read the data as it comes in. If it is not readily usable, I usually create some appendable structure (StringBuilder for something simple) to the SelectionKey that is in the selector. After each read I would append the data to the builder, and if you detect a usable header, slice it out of the buffer and pass it up stream (preferably on a worker thread). Keep doing this and whatever is upstream should be able to react accordingly. Hope that helps.

    So usually you have a structure like this:

    ByteBuffer reUsableBuffer = ByteBuffer.allocateDirect(5120);
    Selector selector = Selector.open();
    ServerSocketChannel channel = .. // wherever you get it from 
    channel.register(selector, SelectionKey.OP_ACCEPT);
    Executor executor = Executors.newThreadPoolExecutor();
    while(selector.isOpen()) { 
     int numKey = selector.select();
     for (SelectionKey key: selector.selectedKeys()) {
        if (key.isAcceptable()) {
                 /// Sort of included for completeness but you get the idea
               ServerSocketChannel server = (ServerSocketChannel)key.channel();
               SocketChannel channel = server.accept();
               channel.register(selector, SelectionKey.OP_READ | Selection.OP_WRITE, new StringBuilder());
        }    if (key.isReadable()) {
              // READ the data
              reUsableBuffer.clear();
              // You have to keep track of previous state.
              // NIO makes no guarantees of anything
              StringBuilder builder = key.attachment();
              SocketChannel socketChannel = (SocketChannel)key.channel();
              int readCount = socketChannel.read(reUsableBuffer);
              if (readCount > 0) {
                 reUsableBuffer.flip();
                 byte[] subStringBytes = new byte[readCount];
                 reUsableBuffer.read(subStringBytes);
                 // Assuming ASCII (bad assumption but simplifies the example)
                 builder.append(new String(substringBytes));
    
                 Command[] commands = removeCommands(builder);
                 // Deal with your commands in some async manor defined by you
                 executor.execute(new Task(commands));
              }
            }
            selector.selectedKeys().clear(); } ....
    
        }   
    
    //
    // Parse out the commands and return them, also remove traces of them in the
    // the builder, such that for a string, "COMMAND, COMMAND, COM"
    // an array of 2 should be returned with a left over buffer of "COM"
    public Command[] parseCommands(StringBuilder s) { ... }