AndServer introduction

The WebServer and Web framework of the Android platform support static website deployment, dynamic Http interface, and reverse proxy server.

Source code address: github.com/yanzhenjie/…

The document address: yanzhenjie. Dead simple. IO/AndServer /

Features:

  • Deploying a Static Website
  • Dynamic HTTP API
  • Global request interceptor
  • Global exception handler
  • Global message converter

AndServer use

Using a very simple, constructor pattern, create a Server instance and then call Startup and shutdown to start and shut it down

/ / server construction
Server server = AndServer.webServer(context)
    .port(8080)
    .timeout(10, TimeUnit.SECONDS)
    .build();

// startup the server.
server.startup();


// shutdown the server.
server.shutdown();
Copy the code

See AndServer for more usage

AndServer overall architecture

The AndServer base relies on ServerSocket and Apache HttpCore to receive and parse requests. The core of AndServer is the implementation of WebFramework, the main user distribution processing interception, as well as Session, Cookie, Cache and other processing.

Hierarchy diagram

The official document provides the following blueprints for the hierarchical architecture:

Among them

  • Socket layer: Use ServerSocket
  • HttpParse: Implemented through Apache HttpCore
  • FrameWork and Handler are the core parts of AndServer. Main implementation: request processing, content distribution, interceptor and session, cookie, cache and so on

Runtime flow chart

ServerSocket creation process and Http resolution process

Graph TD webserver. startup --> create ServerSocket bind local IP and port --> serversocket.accept

WebServer is the instance we created above. The Server startup method is as follows:

    @Override
    public void startup(a) {
        if (isRunning) {
            return;
        }
        Executors.getInstance().execute(new Runnable() {
            @Override
            public void run(a) {
                try {
                    // Create an HttpServer
                    mHttpServer = createHttpSerVer();
                    // Key code 2: Start HttpServer
                    mHttpServer.start();
                    
                    isRunning = true;
                    // Listen back, exception handling, etc. }catch (final Exception e) {
                    // Listen back, exception handling, etc. }}}); }Copy the code

The key part is to create the HttpServer and start it.

    private HttpServer createHttpSerVer(a) {
        return ServerBootstrap.bootstrap()
                .setServerSocketFactory(mSocketFactory)
                .setSocketConfig(
                        SocketConfig.custom()
                                .setSoKeepAlive(true)
                                .setSoReuseAddress(true)
                                .setTcpNoDelay(true)
                                .setSoTimeout(mTimeout)
                                .setBacklogSize(BUFFER)
                                .setRcvBufSize(BUFFER)
                                .setSndBufSize(BUFFER)
                                .setSoLinger(0)
                                .build()
                )
                .setLocalAddress(mInetAddress)
                .setListenerPort(mPort)
                .setSslContext(mSSLContext)
                .setSslSetupHandler(new SSLSetup(mSSLSocketInitializer))
                .setServerInfo(AndServer.INFO)
                // Register HttpRequestHandler
                .registerHandler("*", requestHandler())
                .setExceptionLogger(ExceptionLogger.NO_OP)
                .create();
    }
Copy the code

HttpRequestHandler registerHandler() : HttpRequestHandler registerHandler() : HttpRequestHandler registerHandler() : HttpRequestHandler registerHandler() : HttpRequestHandler registerHandler() : HttpRequestHandler The concrete implementation of requestHandler() is discussed below.

The HttpServer startup process is as follows: Create a serverSocket object and bind the IP address to the port number.

    public void start(a) throws IOException {
        if (this.status.compareAndSet(Status.READY, Status.ACTIVE)) {
            // Create ServerSocket
            this.serverSocket = this.serverSocketFactory.createServerSocket();
            this.serverSocket.setReuseAddress(this.socketConfig.isSoReuseAddress());
            // Key code 2: bind the local IP and port
            this.serverSocket.bind(new InetSocketAddress(this.ifAddress, this.port),
                this.socketConfig.getBacklogSize());
            if (this.socketConfig.getRcvBufSize() > 0) {
                this.serverSocket.setReceiveBufferSize(this.socketConfig.getRcvBufSize());
            }
            if (this.sslSetupHandler ! =null && this.serverSocket instanceof SSLServerSocket) {
                this.sslSetupHandler.initialize((SSLServerSocket) this.serverSocket);
            }
            / / RequestListener construction
            this.requestListener = new RequestListener(
                    this.socketConfig,
                    this.serverSocket,
                    this.httpService,
                    this.connectionFactory,
                    this.exceptionLogger,
                    this.workerExecutorService);
            this.listenerExecutorService.execute(this.requestListener); }}Copy the code

Once created, a requestListener object is constructed and executed in the thread pool.

When RequestListener runs, it calls the Accept () method of the ServerSocket, which blocks until a request is received, after which the socket object is returned, as shown in key code 1.

    @Override
    public void run(a) {
        try {
            while(! isTerminated() && ! Thread.interrupted()) {Listens for a connection to be made to this
                // socket and accepts it. The method blocks until a connection
                // is made.
                final Socket socket = this.serversocket.accept();
                socket.setSoTimeout(this.socketConfig.getSoTimeout());
                socket.setKeepAlive(this.socketConfig.isSoKeepAlive());
                socket.setTcpNoDelay(this.socketConfig.isTcpNoDelay());
                if (this.socketConfig.getRcvBufSize() > 0) {
                    socket.setReceiveBufferSize(this.socketConfig.getRcvBufSize());
                }
                if (this.socketConfig.getSndBufSize() > 0) {
                    socket.setSendBufferSize(this.socketConfig.getSndBufSize());
                }
                if (this.socketConfig.getSoLinger() >= 0) {
                    socket.setSoLinger(true.this.socketConfig.getSoLinger());
                }
                // Construct HttpServerConnection from socket
                final HttpServerConnection conn = this.connectionFactory.createConnection(socket);
                final Worker worker = new Worker(this.httpService, conn, this.exceptionLogger);
                / / perform the work
                this.executorService.execute(worker); }}catch (final Exception ex) {
            this.exceptionLogger.log(ex); }}Copy the code

After the socket object is generated, an HttpServerConnection object is created based on the socket object and executed in the Worker. The details of the Worker class are as follows:

 @Override
    public void run(a) {
        try {
            final BasicHttpContext localContext = new BasicHttpContext();
            final HttpCoreContext context = HttpCoreContext.adapt(localContext);
            while(! Thread.interrupted() &&this.conn.isOpen()) {
                // Key code 1: Handle conn via the handleRequest method of HttpService
                this.httpservice.handleRequest(this.conn, context);
                localContext.clear();
            }
            // Key code 2: close conn
            this.conn.close();
        } catch (final Exception ex) {
            this.exceptionLogger.log(ex);
        } finally {
            try {
                this.conn.shutdown();
            } catch (final IOException ex) {
                this.exceptionLogger.log(ex); }}}Copy the code

In code 1 above, the httpService handleRequest is called to handle the Connection request and return the response as follows

    /** * Handles receives one HTTP request over the given connection within the * given execution context and sends a response back to the client. */
    public void handleRequest(
            final HttpServerConnection conn,
            final HttpContext context) throws IOException, HttpException {
        context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
        HttpRequest request = null;
        HttpResponse response = null;
        try {
            // Key code 1: Receive requestrequest = conn.receiveRequestHeader(); context.setAttribute(HttpCoreContext.HTTP_REQUEST, request); .if (response == null) {
                // Create response
                response = this.responseFactory.newHttpResponse(HttpVersion.HTTP_1_1,
                        HttpStatus.SC_OK, context);
                this.processor.process(request, context);
                // Key code 3: execute HttpRequestHandler -- more on this laterdoService(request, response, context); }... }catch (final HttpException ex) {
            response = this.responseFactory.newHttpResponse
                (HttpVersion.HTTP_1_0, HttpStatus.SC_INTERNAL_SERVER_ERROR,
                 context);
            handleException(ex, response);
        }

        context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);

        this.processor.process(response, context);
        // send
        conn.sendResponseHeader(response);
        if (canResponseHaveBody(request, response)) {
            conn.sendResponseEntity(response);
        }
        conn.flush();

        if (!this.connStrategy.keepAlive(response, context)) { conn.close(); }}Copy the code

Instead of focusing on the details of receiving the Request and creating the Response object, we’ll just call the doService method once we get the two objects. At this point, the Request and Response objects are processed further. We will not look at the specific processing here, and then call the HttpServerConnection send method. The client then receives the response message.

Conn. SendResponseHeader and conn. SendResponseEntity

WebFramework

As mentioned above, there is a Framework layer in the overall hierarchy, which is also the core of AndServer. Next, we will analyze this layer.

Application layer architecture diagram

The architecture of the application layer is as follows:

  • Dispatcher: distribution
  • Intercept Interceptor:
  • HandlerAdapter: Handler adapter
  • Handler: Actual processing logic
  • ViewResolver: Transform generates response

Application layer runtime flow chart

The following is a flow chart of the application layer:

According to the actual code logic, we summarize the following method call flow chart:

Graph of TD DispatcherHandler. Handler - > get the adapter need to intercept the HandlerAdapter -- -- > get processor RequestHandler > interceptor processing such as cache intercepting - > RequestHandler. Handle --> View wraps ResponseBody --> ViewResolver generates Response

One of the methods that we’ll talk about later in the Webserver startup process is registerHandler(“*”, requestHandler()), which basically registers HttpRequestHandler, RequestHandler () is implemented in WebServer as follows:

    @Override
    protected HttpRequestHandler requestHandler(a) {
        DispatcherHandler handler = new DispatcherHandler(mContext);
        ComponentRegister register = new ComponentRegister(mContext);
        try {
            // This will be covered later
            register.register(handler, mGroup);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        return handler;
    }
Copy the code

HttpRequestHandler public class DispatcherHandler implements HttpRequestHandler, Register {}

The actual implementation of registerHandler is to put the handler into a map

    public final ServerBootstrap registerHandler(final String pattern, final HttpRequestHandler handler) {
        if (pattern == null || handler == null) {
            return this;
        }
        if (handlerMap == null) {
            handlerMap = new HashMap<String, HttpRequestHandler>();
        }
        handlerMap.put(pattern, handler);
        return this;
    }
Copy the code

It will eventually be passed to the HttpRequestHandlerMapper object in the HttpService, which we won’t care about here.

When we talk about the overall architecture, we don’t continue after we get to doService(), but instead focus on the concrete implementation of the method doService()

    protected void doService(
            final HttpRequest request,
            final HttpResponse response,
            final HttpContext context) throws HttpException, IOException {
        HttpRequestHandler handler = null;
        if (this.handlerMapper ! =null) {
            HttpRequestHandler: HttpRequestHandler: HttpRequestHandler: HttpRequestHandler: HttpRequestHandler
            handler = this.handlerMapper.lookup(request);
        }
        if(handler ! =null) {
            // Execute the handle method
            handler.handle(request, response, context);
        } else{ response.setStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); }}Copy the code

HttpRequestHandler: HttpRequestHandler: HttpRequestHandler: HttpRequestHandler: HttpRequestHandler: Then call handler’s handle method.

Let’s look at Handle, where the actual logic is in our implementation of the DispatcherHandler class

       @Override
    public void handle(org.apache.httpcore.HttpRequest req, org.apache.httpcore.HttpResponse res, org.apache.httpcore.protocol.HttpContext con) {
        HttpRequest request = new StandardRequest(req, new StandardContext(con), this, mSessionManager);
        HttpResponse response = new StandardResponse(res);
        // Call handle
        handle(request, response);
    }

    private void handle(HttpRequest request, HttpResponse response) {
        MultipartResolver multipartResolver = new StandardMultipartResolver();
        try {
            if (multipartResolver.isMultipart(request)) {
                configMultipart(multipartResolver);
                request = multipartResolver.resolveMultipart(request);
            }

            Determine adapter for the current request.
            HandlerAdapter ha = getHandlerAdapter(request);
            if (ha == null) {
                throw new NotFoundException(request.getPath());
            }

            // Determine handler for the current request.
            RequestHandler handler = ha.getHandler(request);
            if (handler == null) {
                throw new NotFoundException(request.getPath());
            }

            // interceptor: interceptor, e.g.
            if (preHandle(request, response, handler)) {
                return;
            }

            // Actually invoke the handler.
            request.setAttribute(HttpContext.ANDROID_CONTEXT, mContext);
            request.setAttribute(HttpContext.HTTP_MESSAGE_CONVERTER, mConverter);
            // Get the view object
            View view = handler.handle(request, response);
            // Key code 5: parse the view and convert it to HTTP package content
            mViewResolver.resolve(view, request, response);
            processSession(request, response);
        } catch (Throwable err) {
            try {
                // Key code 6: exception handling
                mResolver.onResolve(request, response, err);
            } catch (Exception e) {
                e = new ServerInternalException(e);
                response.setStatus(StatusCode.SC_INTERNAL_SERVER_ERROR);
                response.setBody(new StringBody(e.getMessage()));
            }
            processSession(request, response);
        } finally {
            if (request instanceofMultipartRequest) { multipartResolver.cleanupMultipart((MultipartRequest) request); }}}Copy the code

The process is divided into six parts to process request and response. Here we use static resource deployment as an example to explain the process.

To implement static resource deployment at the application layer, you only need to set the following parameters:

The above procedure uses Website. Let’s look at what Website is via StorageWebsite:

public class StorageWebsite extends BasicWebsite implements Patterns {}

public abstract class BasicWebsite extends Website {}

public abstract class Website implements HandlerAdapter, ETag, LastModified {}

This shows that Website implements a HandlerAdapter. The HandlerAdapter structure is as follows:

public interface HandlerAdapter {
    /** * Whether to intercept the current request. */
    boolean intercept(@NonNull HttpRequest request);

    /** * Get the handler that handles the current request. */
    @Nullable
    RequestHandler getHandler(@NonNull HttpRequest request);
}
Copy the code

Get the RequestHandler from the getHandler in the HandlerAdapter.

Moving on to WebConfig via the annotation tag, the annotation generates a registration class ConfigRegister(implementing the OnRegister class) with an OnRegister method:

  @Override
  public void onRegister(Context context, String group, Register register) {
    WebConfig config = mMap.get(group);
    if(config == null) {
      config = mMap.get("default");
    }
    if(config ! =null) {
      Delegate delegate = Delegate.newInstance();
      // Key code 1: onConfig timing in the code above
      config.onConfig(context, delegate);
      List<Website> list = delegate.getWebsites();
      if(list ! =null && !list.isEmpty()) {
        for (Website website : list) {
          // Key code 2: Add adapter Add website to registerregister.addAdapter(website); } } Multipart multipart = delegate.getMultipart(); register.setMultipart(multipart); }}Copy the code

The above procedure implements a call to WebConfig’s onConfig to add Website, and then adds all websites to an object called Register. Let’s look up what a Register object is.

ConfigRegister onRegister is called in ComponentRegister as follows:

public void register(Register register, String group)
        throws InstantiationException, IllegalAccessException {
        AssetManager manager = mContext.getAssets();
        String[] pathList = null;
        try {
            pathList = manager.list("");
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (pathList == null || pathList.length == 0) {
            return;
        }

        for (String path: pathList) {
            if (path.endsWith(ANDSERVER_REGISTER_SUFFIX)) {
                String packageName = path.substring(0, path.lastIndexOf(ANDSERVER_REGISTER_SUFFIX));
                for (String clazz: REGISTER_LIST) {
                    String className = String.format("%s%s%s", packageName, PROCESSOR_PACKAGE, clazz);
                    // Key code 2registerClass(register, group, className); }}}}private void registerClass(Register register, String group, String className)
        throws InstantiationException, IllegalAccessException {
        try{ Class<? > clazz = Class.forName(className);if (OnRegister.class.isAssignableFrom(clazz)) {
                OnRegister load = (OnRegister) clazz.newInstance();
                // Key code 1: call OnRegister after reflection gets OnRegisterload.onRegister(mContext, group, register); }}catch (ClassNotFoundException ignored) {
        }
    }
Copy the code

Here, reflection gets the OnRegister class and calls the OnRegister method.

So the key is ComponentRegister, the class we saw earlier in the requestHandler() method:

    @Override
    protected HttpRequestHandler requestHandler(a) {
        DispatcherHandler handler = new DispatcherHandler(mContext);
        ComponentRegister register = new ComponentRegister(mContext);
        try {
            // Handler is passed to ComponentRegister as a register
            register.register(handler, mGroup);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        return handler;
    }
Copy the code

The register object is an instance of the DispatcherHandler class. Now we have added website to the DispatcherHandler. Let’s go back to the Handle method of DispatcherHandler. The first step is to get the HandlerAdapter and decide whether to handle:

Determine adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(request);         
Copy the code
    private HandlerAdapter getHandlerAdapter(HttpRequest request) {
        for (HandlerAdapter ha: mAdapterList) {
            if (ha.intercept(request)) {
                returnha; }}return null;
    }
Copy the code

Determine whether to process in StorageWebsite:

    @Override
    public boolean intercept(@NonNull HttpRequest request) {
        String httpPath = request.getPath();
        File file = findPathFile(httpPath);
        returnfile ! =null;
    }
Copy the code

Key code 1 is clear.

Moving on, step 2 gets the RequestHandler

// Determine handler for the current request.
RequestHandler handler = ha.getHandler(request);
Copy the code

Go to Website, which implements the getHandler() method and returns RequestHandler

    @Nullable
    @Override
    public RequestHandler getHandler(@NonNull HttpRequest request) {
        return new RequestHandler() {
            @Nullable
            @Override
            public String getETag(@NonNull HttpRequest request) throws Throwable {
                return Website.this.getETag(request);
            }

            @Override
            public long getLastModified(@NonNull HttpRequest request) throws Throwable {
                return Website.this.getLastModified(request);
            }

            @Override
            public View handle(@NonNull HttpRequest request, @NonNull HttpResponse response) throws Throwable {
                return newBodyView(getBody(request, response)); }}; }Copy the code

The structure of the RequestHandler is as follows

public interface RequestHandler extends ETag.LastModified {

    /** * Use the given handler to handle this request. */
    View handle(@NonNull HttpRequest request, @NonNull HttpResponse response) throws Throwable;
}
Copy the code

Next step 4, key code 4: Get the View object

// Get the view object
View view = handler.handle(request, response);
Copy the code

In website, wrap the return value of the getBody() method through BodyView

@Override
public View handle(@NonNull HttpRequest request, @NonNull HttpResponse response) throws Throwable {
    return new BodyView(getBody(request, response));
}
Copy the code

So the key is the getBody method, getBody returns ResponseBody

    @NonNull
    public abstract ResponseBody getBody(@NonNull HttpRequest request, @NonNull HttpResponse response)
        throws IOException;
Copy the code

So the View structure is a wrapper around the ResponseBody

Let’s look at the implementation of the getBody() method on the StorageWebsite, which is actually our local resource file


    @NonNull
    @Override
    public ResponseBody getBody(@NonNull HttpRequest request, @NonNull HttpResponse response) throws IOException {
        String httpPath = request.getPath();
        File targetFile = new File(mRootPath, httpPath);
        if (targetFile.exists() && targetFile.isFile()) {
            return new FileBody(targetFile);
        }

        File indexFile = new File(targetFile, getIndexFileName());
        if (indexFile.exists() && indexFile.isFile()) {
            if(! httpPath.endsWith(File.separator)) { String redirectPath = addEndSlash(httpPath); String query = queryString(request); response.sendRedirect(redirectPath +"?" + query);
                return new StringBody("");
            }

            return new FileBody(indexFile);
        }

        throw new NotFoundException(httpPath);
    }
Copy the code

Then look at step 5, key code 5: parse the view and convert it to HTTP package content

// Key code 5: parse the view and convert it to HTTP package content
mViewResolver.resolve(view, request, response);
Copy the code
    public void resolve(@Nullable View view, @NonNull HttpRequest request, @NonNull HttpResponse response) {
        if (view == null) {
            return;
        }
        // Get output from view
        Object output = view.output();
        if (view.rest()) {
            resolveRest(output, request, response);
        } else{ resolvePath(output, request, response); }}private void resolveRest(Object output, @NonNull HttpRequest request, @NonNull HttpResponse response) {
        if (output instanceof ResponseBody) {
            response.setBody((ResponseBody) output);
        } else if(mConverter ! =null) {
            response.setBody(mConverter.convert(output, obtainProduce(request)));
        } else if (output == null) {
            response.setBody(new StringBody(""));
        } else if (output instanceof String) {
            response.setBody(new StringBody(output.toString(), obtainProduce(request)));
        } else {
            response.setBody(newStringBody(output.toString())); }}Copy the code

This step basically sets the ResponseBody to response. At this point, the process ends and the requester receives the response.

We skipped the interceptor processing logic above. The interceptor processing flow is similar to the HandlerAdapter and is added to the DispatcherHandler via annotations. Let’s look at an example of handling interceptions:

public class ModifiedInterceptor implements HandlerInterceptor {

    @Override
    public boolean onIntercept(@NonNull HttpRequest request, @NonNull HttpResponse response,
        @NonNull RequestHandler handler) {
        // Process cache header, if supported by the handler.
        HttpMethod method = request.getMethod();
        if (method == HttpMethod.GET || method == HttpMethod.HEAD) {
            String eTag = null;
            try {
                / / get the eTag
                eTag = handler.getETag(request);
            } catch (Throwable e) {
                Log.w(AndServer.TAG, e);
            }
            long lastModified = -1;
            try {
                / / get lastModified
                lastModified = handler.getLastModified(request);
            } catch (Throwable e) {
                Log.w(AndServer.TAG, e);
            }
            return new Modified(request, response).process(eTag, lastModified);
        }
        return false; }}Copy the code
    public boolean process(@Nullable String eTag, long lastModified) {
        if (isNotModified) {
            return true;
        }

        // See https://tools.ietf.org/html/rfc7232#section-6
        if (validateIfUnmodifiedSince(lastModified)) {
            if(! isNotModified) { mResponse.setStatus(StatusCode.SC_LENGTH_REQUIRED); }return isNotModified;
        }

        // First, prioritized.
        boolean validated = validateIfNoneMatch(eTag);
        // Second.
        if(! validated) { validateIfModifiedSince(lastModified); }// Update response
        HttpMethod method = mRequest.getMethod();
        boolean isGetHead = (method == HttpMethod.GET || method == HttpMethod.HEAD);
        if (isNotModified) {
            mResponse.setStatus(isGetHead ? StatusCode.SC_NOT_MODIFIED : StatusCode.SC_LENGTH_REQUIRED);
        }
        if (isGetHead) {
            if (lastModified > 0 && mResponse.getHeader(LAST_MODIFIED) == null) {
                mResponse.setDateHeader(LAST_MODIFIED, lastModified);
            }
            if(! TextUtils.isEmpty(eTag) && mResponse.getHeader(ETAG) ==null) {
                mResponse.setHeader(ETAG, padETagIfNecessary(eTag));
            }
            mResponse.setHeader(CACHE_CONTROL, "private");
        }
        return isNotModified;
    }
Copy the code

The Process method in Modified sets eTag and lastModified to the Header of Response