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
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