NIO SSL Server

SSL with plain old IO is downright simple, yet with NIO it’s a whole different story.

This class uses SSLEngine to allow running multiple SSL connections on a single I/O thread.
The SSL handshake itself is performed by N worker threads to prevent them blocking all I/O.


import java.nio.ByteBuffer;
import java.util.concurrent.Executor;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLException;

public abstract class NonBlockingSSL implements Runnable
{
   final ByteBuffer wrapSrc, unwrapSrc;
   final ByteBuffer wrapDst, unwrapDst;

   final SSLEngine  engine;
   final Executor   ioWorker, taskWorkers;

   public NonBlockingSSL(SSLEngine engine, int bufferSize, Executor ioWorker, Executor taskWorkers)
   {
      this.wrapSrc = ByteBuffer.allocateDirect(bufferSize);
      this.wrapDst = ByteBuffer.allocateDirect(bufferSize);

      this.unwrapSrc = ByteBuffer.allocateDirect(bufferSize);
      this.unwrapDst = ByteBuffer.allocateDirect(bufferSize);

      this.unwrapSrc.limit(0);

      this.engine = engine;
      this.ioWorker = ioWorker;
      this.taskWorkers = taskWorkers;

      this.ioWorker.execute(this);
   }

   public abstract void onInboundData(ByteBuffer decrypted);

   public abstract void onOutboundData(ByteBuffer encrypted);

   public abstract void onHandshakeFailure(Exception cause);

   public abstract void onHandshakeSuccess();

   public abstract void onClosed();

   public void sendLater(final ByteBuffer data)
   {
      this.ioWorker.execute(new Runnable()
      {
         @Override
         public void run()
         {
            wrapSrc.put(data);

            NonBlockingSSL.this.run();
         }
      });
   }

   public void notifyReceived(final ByteBuffer data)
   {
      this.ioWorker.execute(new Runnable()
      {
         @Override
         public void run()
         {
            unwrapSrc.put(data);

            NonBlockingSSL.this.run();
         }
      });
   }

   public void run()
   {
      // executes non-blocking tasks on the IO-Worker

      while (this.step())
      {
         continue;
      }

      // apparently we hit a blocking-task...
   }

   private boolean step()
   {
      switch (engine.getHandshakeStatus())
      {
         case NOT_HANDSHAKING:
            boolean anything = false;
            {
               if (wrapSrc.position() > 0)
                  anything |= this.wrap();
               if (unwrapSrc.position() > 0)
                  anything |= this.unwrap();
            }
            return anything;

         case NEED_WRAP:
            if (!this.wrap())
               return false;
            break;

         case NEED_UNWRAP:
            if (!this.unwrap())
               return false;
            break;

         case NEED_TASK:
            final Runnable sslTask = engine.getDelegatedTask();
            Runnable wrappedTask = new Runnable()
            {
               @Override
               public void run()
               {
                  System.out.println("async SSL task: " + sslTask);
                  long t0 = System.nanoTime();
                  sslTask.run();
                  long t1 = System.nanoTime();
                  System.out.println("async SSL task took: " + (t1 - t0) / 1000000 + "ms");

                  // continue handling I/O
                  ioWorker.execute(NonBlockingSSL.this);
               }
            };
            taskWorkers.execute(wrappedTask);
            return false;

         case FINISHED:
            throw new IllegalStateException("FINISHED");
      }

      return true;
   }

   private boolean wrap()
   {
      SSLEngineResult wrapResult;

      try
      {
         wrapSrc.flip();
         wrapResult = engine.wrap(wrapSrc, wrapDst);
         wrapSrc.compact();
      }
      catch (SSLException exc)
      {
         this.onHandshakeFailure(exc);
         return false;
      }

      switch (wrapResult.getStatus())
      {
         case OK:
            if (wrapDst.position() > 0)
            {
               wrapDst.flip();
               this.onOutboundData(wrapDst);
               wrapDst.compact();
            }
            break;

         case BUFFER_UNDERFLOW:
            // try again later
            break;

         case BUFFER_OVERFLOW:
            throw new IllegalStateException("failed to wrap");

         case CLOSED:
            this.onClosed();
            return false;
      }

      return true;
   }

   private boolean unwrap()
   {
      SSLEngineResult unwrapResult;

      try
      {
         unwrapSrc.flip();
         unwrapResult = engine.unwrap(unwrapSrc, unwrapDst);
         unwrapSrc.compact();
      }
      catch (SSLException exc)
      {
         this.onHandshakeFailure(exc);
         return false;
      }

      switch (unwrapResult.getStatus())
      {
         case OK:
            if (unwrapDst.position() > 0)
            {
               unwrapDst.flip();
               this.onInboundData(unwrapDst);
               unwrapDst.compact();
            }
            break;

         case CLOSED:
            this.onClosed();
            return false;

         case BUFFER_OVERFLOW:
            throw new IllegalStateException("failed to unwrap");

         case BUFFER_UNDERFLOW:
            return false;
      }

      switch (unwrapResult.getHandshakeStatus())
      {
         case FINISHED:
            this.onHandshakeSuccess();
            return false;
      }

      return true;
   }
}

The SSL code is actually independant of NIO, but I added the following code to support NIO.

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.Executor;

import javax.net.ssl.SSLEngine;

public abstract class NioNonBlockingSSL extends NonBlockingSSL
{
   private final SelectionKey key;

   public NioNonBlockingSSL(SelectionKey key, SSLEngine engine, int bufferSize, Executor ioWorker, Executor taskWorkers)
   {
      super(engine, bufferSize, ioWorker, taskWorkers);

      this.key = key;
   }

   private final ByteBuffer big = ByteBuffer.allocateDirect(64 * 1024);

   public boolean processIncomingData()
   {
      big.clear();
      int bytes;
      try
      {
         bytes = ((ReadableByteChannel) this.key.channel()).read(big);
      }
      catch (IOException exc)
      {
         bytes = -1;
      }
      if (bytes == -1)
         return false;
      big.flip();

      ByteBuffer copy = ByteBuffer.allocateDirect(bytes);
      copy.put(big);
      copy.flip();

      this.notifyReceived(copy);
      return true;
   }

   @Override
   public void onOutboundData(ByteBuffer encrypted)
   {
      try
      {
         ((WritableByteChannel) this.key.channel()).write(encrypted);

         if (encrypted.hasRemaining())
         {
            throw new IllegalStateException("failed to bulk-write");
         }
      }
      catch (IOException exc)
      {
         throw new IllegalStateException(exc);
      }
   }
}

Demo, which connects to https://www.paypal.com/ and displays the decrypted HTTP response.

Anybody mentioning URLConnection to do the same thing, will be gently explained that you can similarly run a server with this code.

I realize very few people here actually need a non-blocking SSL server, but heck, give it a whirl!


import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSession;

public class NonBlockingSSLDemo
{
   public static void main(String[] args) throws Exception
   {
      // connect to the webservice
      SelectionKey key;
      {
         InetSocketAddress connectTo = new InetSocketAddress("www.paypal.com", 443);
         Selector selector = Selector.open();
         SocketChannel channel = SocketChannel.open();
         channel.connect(connectTo);
         System.out.println("connected to: " + channel);

         channel.configureBlocking(false);
         channel.socket().setTcpNoDelay(true);

         int ops = SelectionKey.OP_CONNECT | SelectionKey.OP_READ;
         key = channel.register(selector, ops);
      }

      // setup the io/worker threads
      final Executor ioWorker = Executors.newSingleThreadExecutor();
      final Executor taskWorkers = Executors.newFixedThreadPool(4);

      // setup the SSLEngine
      final SSLEngine engine = SSLContext.getDefault().createSSLEngine();
      engine.setUseClientMode(true);
      engine.beginHandshake();
      final int ioBufferSize = 64 * 1024;

      final NioNonBlockingSSL ssl;
      ssl = new NioNonBlockingSSL(key, engine, ioBufferSize, ioWorker, taskWorkers)
      {
         @Override
         public void onHandshakeFailure(Exception cause)
         {
            System.out.println("handshake failure");

            cause.printStackTrace();
         }

         @Override
         public void onHandshakeSuccess()
         {
            System.out.println("handshake success");

            SSLSession session = engine.getSession();

            try
            {
               System.out.println("- local principal: " + session.getLocalPrincipal());
               System.out.println("- remote principal: " + session.getPeerPrincipal());
               System.out.println("- using cipher: " + session.getCipherSuite());
            }
            catch (Exception exc)
            {
               exc.printStackTrace();
            }

            // simple HTTP request to www.paypal.com
            StringBuilder http = new StringBuilder();
            http.append("GET / HTTP/1.0\r\n");
            http.append("Connection: close\r\n");
            http.append("\r\n");

            byte[] data = http.toString().getBytes();
            ByteBuffer send = ByteBuffer.wrap(data);
            this.sendLater(send);
         }

         @Override
         public void onInboundData(ByteBuffer decrypted)
         {
            // this is where the HTTP response ends up

            byte[] dst = new byte[decrypted.remaining()];
            decrypted.get(dst);
            String response = new String(dst);

            System.out.print(response);
            System.out.flush();
         }

         @Override
         public void onClosed()
         {
            System.out.println("<ssl session closed>");
         }
      };

      // simplistic NIO stuff

      while (true)
      {
         key.selector().select();

         Iterator<SelectionKey> keys = key.selector().selectedKeys().iterator();

         while (keys.hasNext())
         {
            keys.next(); // there is only one key, don't bother
            keys.remove();

            ssl.processIncomingData();
         }
      }
   }
}

A thing i’ve been meaning to ask you pro networking guys.
A new URL connection opens a socket to each request to a server right?

Anyway to use the http protocol without having to open always open a new connection? I’m already using Executors and some application heuristics, but there can be 6 connections (threads) at a time for responsiveness.

Browsing through the bytecode (sourcecode seems missing) it seems that sun.net.www.http.HttpClient.New() is able to reuse existing TCP connections.

Yes, HTTP/1.1 supports multiple (sequential) requests/responses.

It’s would be a shame if you have 6 connections to a slow/overloaded server, while you were interested in many files from many servers. I suggest you change it, to (max) 6 connections to each server. For proper performance of small request (like dozens of thumbnails), it’s very important to keep the same TCP connection, to sustain/increase bandwidth, as each TCP connection starts out relatively slow.

Yeah the server i am using (openlibrary) doesn’t seem to have a way to do multiple request of covers or even a way to combine requests for author queries (for the id) and author books

So for each book i do:

  1. get the author id (search in server)
  2. get the author book (search in server)
  3. while not found a cover in the book list go to 3)

And i found out that i was saving my thumbnails in tmp instead of the user home. (in linux tmp is deleted in startup)

In fact i’m sure i put it down sometimes when testing.
It’s better now.
:stuck_out_tongue:

Well, no. In your case, you can’t have the result of multiple queries in one response (as long as the service doesn’t support that) you can query multiple times over the same tcp connection (as long as the httpserver supports it).