• “NanoHttpd微型服务器”使用及源码阅读


    “NanoHttpd微型服务器”使用及源码阅读

    NanoHttpd仅有一个Java文件的微型Http服务器实现。其方便嵌入式设备(例如:Android设备)中启动一个本地服务器,接收客户端本地部分请求;应用场景也非常广泛,例如:本地代理方式播放m3u8视频、本地代理方式加载一些加密秘钥等。

    这里分三部分学习一个这个NanoHttpd:

    • 了解官方对NanoHttpd描述定义
    • 举例NanoHttpd在Android上的使用(Android本地代理,播放Sdcard中的m3u8视频)
    • 分析源码实现

    NanoHttpd GitHub地址:https://github.com/NanoHttpd/nanohttpd

    一、NanoHttpd 官方描述

    Tiny, easily embeddable HTTP server in Java.
    微小的,轻量级适合嵌入式设备的Java Http服务器;
    NanoHTTPD is a light-weight HTTP server designed for embedding in other applications, released under a Modified BSD licence.
    NanoHTTPD是一个轻量级的、为嵌入式设备应用设计的HTTP服务器,遵循修订后的BSD许可协议。

    核心功能描述:

    • Only one Java file, providing HTTP 1.1 support.
      仅一个Java文件,支持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.)
      没有固定的配置文件、日志系统、授权等等(如果你有需要需自己实现。工程中的日志输出,通过java.util.logging实现的)
    • Support for HTTPS (SSL).
      支持Https
    • Basic support for cookies.
      支持cookies
    • Supports parameter parsing of GET and POST methods.
      支持POST和GET 参数请求
    • Some built-in support for HEAD, POST and DELETE requests. You can easily implement/customize any HTTP method, though.
      内置支持HEAD、POST、DELETE请求,你可以方便的实现或自定义任何HTTP方法请求。
    • Supports file upload. Uses memory for small uploads, temp files for large ones.
      支持文件上传。小文件上传使用内存缓存,大文件使用临时文件缓存。
    • Never caches anything.
      不缓存任何内容
    • Does not limit bandwidth, request time or simultaneous connections by default.
    • 默认不限制带宽、请求时间 和 最大请求量
    • All header names are converted to lower case so they don't vary between browsers/clients.
      所有Header 名都被转换为小写,因此不会因客户端或浏览器的不同而有所差别
    • Persistent connections (Connection "keep-alive") support allowing multiple requests to be served over a single socket connection.
      支持一个socket连接服务多个长连接请求。

    二、NanoHttpd在Android上的使用举例

    为了学习NanoHttpd,做了一个简单Demo “Android 本地代理方式播放 Sdcard中的m3u8视频”:
    Android 本地代理方式播放 Sdcard中的m3u8视频
    https://github.com/AndroidAppCodeDemo/Android_LocalM3u8Server

    实现效果图:

    enter description here

    注:

    下边来主要来跟踪学习NanoHttpd的源码...

    三、NanoHttpd源码跟踪学习

    注:基于 NanoHttpd 2.3.1版本
    NanoHttpd 2.3.1版本下载
    https://github.com/NanoHttpd/nanohttpd/releases/tag/nanohttpd-project-2.3.1

    NanoHTTPD大概的处理流程是:

    • 开启一个服务端线程,绑定对应的端口,调用 ServerSocket.accept()方法进入等待状态
    • 每个客户端连接均开启一个线程,执行ClientHandler.run()方法
    • 客户端线程中,创建一个HTTPSession会话。执行HTTPSession.execute()
    • HTTPSession.execute() 中会完成 uri, method, headers, parms, files 的解析,并调用如下方法:
    // 自定义服务器时,亦需要重载该方法
    // 该方法传入参数中,已解析出客户端请求的所有数据,重载该方法进行相应的业务处理
    HTTPSession.serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files)
    
    • 组织Response数据,并调用ChunkedOutputStream.send(outputStream)返回给客户端

    建议:
    对于Http request、response 数据组织形式不是很了解的同学,建议自己了解后再阅读NanoHTTPD源码。
    也可参考我的另一篇文章:
    HTTP 协议详解
    https://xiaxl.blog.csdn.net/article/details/104541274

    3.1、NanoHTTPD.start

    从服务器启动开始学习...

    /**
     * Start the server. 启动服务器
     *
     * @param timeout timeout to use for socket connections. 超时时间
     * @param daemon  start the thread daemon or not. 守护线程
     * @throws IOException if the socket is in use.
     */
    public void start(final int timeout, boolean daemon) throws IOException {
        // 创建一个ServerSocket
        this.myServerSocket = this.getServerSocketFactory().create();
        this.myServerSocket.setReuseAddress(true);
    
        // 创建 ServerRunnable
        ServerRunnable serverRunnable = createServerRunnable(timeout);
        // 启动一个线程监听客户端请求
        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;
        }
    }
    

    从以上代码中,可以看到:

    • 代码前两行,创建一个ServerSocket
    • 开启一个线程,执行ServerRunnable。这里其实就是服务端启动一个线程,用来监听客户端的请求,具体代码在ServerRunnable中。

    3.2、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");
                // 等待客户端连接
                final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept();
                // 设置超时时间
                if (this.timeout > 0) {
                    finalAccept.setSoTimeout(this.timeout);
                }
                // 服务端:输入流
                final InputStream inputStream = finalAccept.getInputStream();
                Log.e(TAG, "asyncRunner.exec");
                // 执行客户端 ClientHandler
                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());
    }
    

    ServerRunnablerun()方法:

    • 调用 ServerSocket.bind 方法,绑定对应的端口
    • 调用 ServerSocket.accept() 线程进入阻塞等待状态
    • 客户端连接后,会执行createClientHandler(finalAccept, inputStream)创建一个ClientHandler,并开启一个线程,执行其对应的ClientHandler.run()方法
    • 自定义服务器时,重载Response HTTPSession.serve(uri, method, headers, parms, files)方法,对客户端的请求数据做出处理

    3.3、ClientHandler.run()

    @Override
    public void run() {
        Log.e(TAG, "---run---");
        // 服务端 输出流
        OutputStream outputStream = null;
        try {
            // 服务端的输出流
            outputStream = this.acceptSocket.getOutputStream();
            // 创建临时文件
            TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create();
            // session 会话
            HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress());
            // 执行会话
            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临时文件是为了缓存客户端Post请求的请求Body数据(如果数据较小,内存缓存;文件较大,缓存到文件中)
    • 创建一个HTTPSession会话,并执行其对应的HTTPSession.execute()方法
    • HTTPSession.execute()中会对客户端的请求进行解析

    3.4、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默认header限制8k
            byte[] buf = new byte[HTTPSession.BUFSIZE];
            this.splitbyte = 0;
            this.rlen = 0;
            // 客户端输入流
            int read = -1;
            this.inputStream.mark(HTTPSession.BUFSIZE);
            // 读取8k的数据
            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");
            }
            // 分割header数据
            while (read > 0) {
                this.rlen += read;
                // header
                this.splitbyte = findHeaderEnd(buf, this.rlen);
                // 找到header
                if (this.splitbyte > 0) {
                    break;
                }
                // 8k中剩余数据
                read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen);
            }
            // header数据不足8k,跳过header数据
            if (this.splitbyte < this.rlen) {
                this.inputStream.reset();
                this.inputStream.skip(this.splitbyte);
            }
            //
            this.parms = new HashMap<String, List<String>>();
            // 清空header列表
            if (null == this.headers) {
                this.headers = new HashMap<String, String>();
            } else {
                this.headers.clear();
            }
            // 解析 客户端请求
            // 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())
            // 构造一个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);
    
                // 发送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() 完成了 uri, method, headers, parms, files 的解析
    • 完成解析后,调用Response serve(IHTTPSession session)方法,创建了一个Response
    • 完成Response数据组织后,这里会调用ChunkedOutputStream.send(outputStream)方法将数据发出去。

    到这里,主要流程结束,其他细节需大家自己去用心研读源码了。我的Demo中增加了很多中文注释,可以帮助大家省下一部分力气,就这样了

    相关参考

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

    NanoHttpd源码分析
    https://www.iteye.com/blog/shensy-1880381

    ========== THE END ==========

    wx_gzh.jpg

  • 相关阅读:
    ABP框架(asp.net core 2.X+Vue)运行前端(转)
    Spring学习指南第3版例子程序导入idea20.02
    Docker运行jar包(转)
    centos7使用docker搭建运行环境并部署jar(转)
    centos7修改docker镜像源的方法(转)
    虚拟机下CentOS7安装Docker(转)
    Virtual Box配置CentOS7网络(图文教程)(转)
    虚拟机docker运行jar程序
    docker安装
    【591】通过 Terminal 直接获取 GitHub 源码
  • 原文地址:https://www.cnblogs.com/xiaxveliang/p/12395936.html
Copyright © 2020-2023  润新知