"NanoHttpd Micro Server" Use and Source Reading

Keywords: Java socket Session Android

Occasionally, NanoHttpd is a Java file that starts a local server in an embedded device (for example, an Android phone) and receives local requests from clients.
I carefully learned its source implementation, here I wrote a simple article in my learning order (as a learning note):

  • Understanding official descriptions
  • Write a Demo for use (local agent in Android, m3u8 in Sdcard)
  • Finally, learn its source implementation

NanoHttpd GitHub address:
https://github.com/NanoHttpd/nanohttpd

First look at the official description.

Official description of NanoHttpd

Tiny, easily embeddable HTTP server in Java.
A small, lightweight Java Http server for embedded devices;
NanoHTTPD is a light-weight HTTP server designed for embedding in other applications, released under a Modified BSD licence.
NanoHTTPD is a lightweight HTTP server designed for embedded device applications, following the revised BSD license agreement.

Core

  • Only one Java file, providing HTTP 1.1 support.

Only one Java file, supporting Http 1.1

  • No fixed config files, logging, authorization etc. (Implement by yourself if you need them. Errors are passed to java.util.logging, though.)

There are no fixed profiles, log systems, authorizations, etc. (if you need to implement them yourself).Log output from a project, implemented through java.util.logging)

  • Support for HTTPS (SSL).

Supports Https

  • Basic support for cookies.

Supports cookies

  • Supports parameter parsing of GET and POST methods.

Support POST and GET parameter requests

  • Some built-in support for HEAD, POST and DELETE requests. You can easily implement/customize any HTTP method, though.

Built-in support for HEAD, POST, DELETE requests allows you to easily implement or customize any HTTP method request.

  • Supports file upload. Uses memory for small uploads, temp files for large ones.

Supports file upload.Small file uploads use memory caching, and large files use temporary file caching.

  • Never caches anything.

Do not cache anything

  • Does not limit bandwidth, request time or simultaneous connections by default.
  • Default unlimited bandwidth, request time, and maximum request volume
  • All header names are converted to lower case so they don't vary between browsers/clients.

All Header names are converted to lowercase, so they do not vary from client to browser

  • Persistent connections (Connection "keep-alive") support allowing multiple requests to be served over a single socket connection.

Supports multiple long connection requests for a socket connection service.

1. Play m3u8 video in Sdcard by Android Local Agent

To learn NanoHttpd, a simple Demo was made: Play m3u8 video in Sdcard using Android local proxy

Demo for download

Play m3u8 video in Sdcard using Android local proxy (using NanoHttpd version 2.3.1)

NanoHttpd Version 2.3.1 Download

The result is as follows:

With the use of NanoHttpd, the Demo implementation of "Playing m3u8 videos in Android Sdcard as a local proxy" has become very simple. This is not detailed here. Interested friends can download it for themselves.

Below comes the main source tracking for learning NanoHttpd...

2. NanoHttpd Source Tracking Learning

Note: Based on NanoHttpd version 2.3.1
NanoHttpd Version 2.3.1 Download

The approximate processing flow for NanoHTTPD is:

  • Open a server-side thread, bind the corresponding port, call the ServerSocket.accept() method and enter the wait state
  • Each client connection opens a thread to execute the ClientHandler.run() method
  • In the client thread, create an HTTPSession session.Execute HTTPSession.execute()
  • HTTPSession.execute() parses uri, method, headers, parms, files and calls the method
// This method also needs to be overloaded when customizing the server
// This method passes in parameters, parses out all the data requested by the client, overloads the method for corresponding business processing
HTTPSession.serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files)
  • Organize Response data and call ChunkedOutputStream.send(outputStream) to return to the client

Suggestions: For students who do not know the organization of Http request and response data, it is recommended that you read the NanoHTTPD source code after you know it.You can also refer to another article of mine: Http Request Data Format

NanoHTTPD.start

Start learning from server startup...

/**
 * Start the server. Start Server
 *
 * @param timeout timeout to use for socket connections. timeout
 * @param daemon  start the thread daemon or not. Daemon Threads
 * @throws IOException if the socket is in use.
 */
public void start(final int timeout, boolean daemon) throws IOException {
    // Create a ServerSocket
    this.myServerSocket = this.getServerSocketFactory().create();
    this.myServerSocket.setReuseAddress(true);

    // Create ServerRunnable
    ServerRunnable serverRunnable = createServerRunnable(timeout);
    // Start a thread to listen for client requests
    this.myThread = new Thread(serverRunnable);
    this.myThread.setDaemon(daemon);
    this.myThread.setName("NanoHttpd Main Listener");
    this.myThread.start();
    //
    while (!serverRunnable.hasBinded && serverRunnable.bindException == null) {
        try {
            Thread.sleep(10L);
        } catch (Throwable e) {
            // on android this may not be allowed, that's why we
            // catch throwable the wait should be very short because we are
            // just waiting for the bind of the socket
        }
    }
    if (serverRunnable.bindException != null) {
        throw serverRunnable.bindException;
    }
}

From the code above, you can see that:

  • First two lines of code, create a ServerSocket
  • Open a thread to execute ServerRunnable.This is where the server starts a thread to listen for client requests, in ServerRunnable.

ServerRunnable.run()

@Override
public void run() {
    Log.e(TAG, "---run---");
    try {
        // bind
        myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
        hasBinded = true;
    } catch (IOException e) {
        this.bindException = e;
        return;
    }
    Log.e(TAG, "bind ok");
    do {
        try {
            Log.e(TAG, "before accept");
            // Waiting for Client Connection
            final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept();
            // Set timeout
            if (this.timeout > 0) {
                finalAccept.setSoTimeout(this.timeout);
            }
            // Server: Input Stream
            final InputStream inputStream = finalAccept.getInputStream();
            Log.e(TAG, "asyncRunner.exec");
            // Execute ClientHandler on Client
            NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream));
        } catch (IOException e) {
            NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
        }
    } while (!NanoHTTPD.this.myServerSocket.isClosed());
}

ServerRunnable run() method:

  • Call the ServerSocket.bind method to bind the corresponding port
  • The calling ServerSocket.accept() thread enters a blocked wait state
  • When the client connects, it executes createClientHandler(finalAccept, inputStream) to create a ClientHandler, and opens a thread to execute its corresponding ClientHandler.run() method
  • When customizing a server, overload the Response HTTPSession.serve(uri, method, headers, parms, files) method for appropriate business processing
  • After processing, for

ClientHandler.run()

@Override
public void run() {
    Log.e(TAG, "---run---");
    // Server Output Stream
    OutputStream outputStream = null;
    try {
        // Output stream from server
        outputStream = this.acceptSocket.getOutputStream();
        // Create Temporary File
        TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create();
        // Session session
        HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress());
        // Execute Session
        while (!this.acceptSocket.isClosed()) {
            session.execute();
        }
    } catch (Exception e) {
        // When the socket is closed by the client,
        // we throw our own SocketException
        // to break the "keep alive" loop above. If
        // the exception was anything other
        // than the expected SocketException OR a
        // SocketTimeoutException, print the
        // stacktrace
        if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) {
            NanoHTTPD.LOG.log(Level.SEVERE, "Communication with the client broken, or an bug in the handler code", e);
        }
    } finally {
        safeClose(outputStream);
        safeClose(this.inputStream);
        safeClose(this.acceptSocket);
        NanoHTTPD.this.asyncRunner.closed(this);
    }
}
  • TempFileManager temporary files are used to cache request Body data requested by client Post (if data is small, memory cache; file is large, cached to file)
  • Create an HTTPSession session and execute its corresponding HTTPSession.execute() method
  • The client's request is resolved in HTTPSession.execute()

HTTPSession.execute()


@Override
public void execute() throws IOException {
    Log.e(TAG, "---execute---");
    Response r = null;
    try {
        // Read the first 8192 bytes.
        // The full header should fit in here.
        // Apache's default header limit is 8KB.
        // Do NOT assume that a single read will get the entire header
        // at once!
        // Apache default header limit 8k
        byte[] buf = new byte[HTTPSession.BUFSIZE];
        this.splitbyte = 0;
        this.rlen = 0;
        // Client Input Stream
        int read = -1;
        this.inputStream.mark(HTTPSession.BUFSIZE);
        // Read 8k data
        try {
            read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE);
        } catch (SSLException e) {
            throw e;
        } catch (IOException e) {
            safeClose(this.inputStream);
            safeClose(this.outputStream);
            throw new SocketException("NanoHttpd Shutdown");
        }
        if (read == -1) {
            // socket was been closed
            safeClose(this.inputStream);
            safeClose(this.outputStream);
            throw new SocketException("NanoHttpd Shutdown");
        }
        // Split header data
        while (read > 0) {
            this.rlen += read;
            // header
            this.splitbyte = findHeaderEnd(buf, this.rlen);
            // Find header
            if (this.splitbyte > 0) {
                break;
            }
            // Remaining data in 8k
            read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen);
        }
        // Header data less than 8k, skip header data
        if (this.splitbyte < this.rlen) {
            this.inputStream.reset();
            this.inputStream.skip(this.splitbyte);
        }
        //
        this.parms = new HashMap<String, List<String>>();
        // Empty header list
        if (null == this.headers) {
            this.headers = new HashMap<String, String>();
        } else {
            this.headers.clear();
        }
        // Resolve client requests
        // Create a BufferedReader for parsing the header.
        BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen)));
        // Decode the header into parms and header java properties
        Map<String, String> pre = new HashMap<String, String>();
        decodeHeader(hin, pre, this.parms, this.headers);
        //
        if (null != this.remoteIp) {
            this.headers.put("remote-addr", this.remoteIp);
            this.headers.put("http-client-ip", this.remoteIp);
        }
        Log.e(TAG, "headers: " + headers);

        this.method = Method.lookup(pre.get("method"));
        if (this.method == null) {
            throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. HTTP verb " + pre.get("method") + " unhandled.");
        }
        Log.e(TAG, "method: " + method);

        this.uri = pre.get("uri");
        Log.e(TAG, "uri: " + uri);

        this.cookies = new CookieHandler(this.headers);
        Log.e(TAG, "cookies: " + this.cookies.cookies);

        String connection = this.headers.get("connection");
        Log.e(TAG, "connection: " + connection);
        boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (connection == null || !connection.matches("(?i).*close.*"));
        Log.e(TAG, "keepAlive: " + keepAlive);
        // Ok, now do the serve()

        // TODO: long body_size = getBodySize();
        // TODO: long pos_before_serve = this.inputStream.totalRead()
        // (requires implementation for totalRead())
        // Construct a response
        r = serve(HTTPSession.this);
        // TODO: this.inputStream.skip(body_size -
        // (this.inputStream.totalRead() - pos_before_serve))

        if (r == null) {
            throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
        } else {
            String acceptEncoding = this.headers.get("accept-encoding");
            this.cookies.unloadQueue(r);
            // method
            r.setRequestMethod(this.method);
            r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip"));
            r.setKeepAlive(keepAlive);

            // Send response
            r.send(this.outputStream);
        }
        if (!keepAlive || r.isCloseConnection()) {
            throw new SocketException("NanoHttpd Shutdown");
        }
    } catch (SocketException e) {
        // throw it out to close socket object (finalAccept)
        throw e;
    } catch (SocketTimeoutException ste) {
        // treat socket timeouts the same way we treat socket exceptions
        // i.e. close the stream & finalAccept object by throwing the
        // exception up the call stack.
        throw ste;
    } catch (SSLException ssle) {
        Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SSL PROTOCOL FAILURE: " + ssle.getMessage());
        resp.send(this.outputStream);
        safeClose(this.outputStream);
    } catch (IOException ioe) {
        Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
        resp.send(this.outputStream);
        safeClose(this.outputStream);
    } catch (ResponseException re) {
        Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
        resp.send(this.outputStream);
        safeClose(this.outputStream);
    } finally {
        safeClose(r);
        this.tempFileManager.clear();
    }
}
  • HTTPSession.execute() completes the parsing of uri, method, headers, parms, files
  • When the resolution is complete, a Response is created by calling the Response service (IHTTPSession session) method
  • Once the Response data organization is complete, the ChunkedOutputStream.send(outputStream) method is called to send out the data.

At this point, the main process is over, and the other details require you to study the source code yourself.I added a lot of Chinese notes to my Demo to help you save some energy, and that's it

3. Relevant References

NanoHttpd GitHub

NanoHttpd Source Code Analysis

Posted by DwarV on Tue, 26 Nov 2019 19:11:12 -0800