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