android at服务器 andserver手机服务器
- 一、概述
- 1.1 基本Api:单服务器多站点
- 二、Socket通信承载功能一览
- 三、管理器 Server
- 3.1 AndServer
- 3.2 Server
- 3.3 Core
- 3.3.1 持有变量
- 3.3.2 启动
- 3.3.3 停止与判断
- 3.4 DispatchRequestHandler请求分发器
- 3.4.1 持有变量
- 3.4.2 核心分发功能
- 四、RequestHandler请求处理器
- 4.1 登录接口示例
- 4.2 SimpleRequestHandler 快捷处理器
- 4.2.1 View响应中间件
- 4.2.1.1OkView 成功响应的快捷方式
- 4.2.1.2 RedirectView 重定向响应
- 4.3 Reponse响应
- 4.3.1 返回图片
- 4.3.2 下载文件
- 4.3.3 上传图片
- 4.3.4 返回Json
- 4.4 请求方法限制
- 五、WebSite 站点响应器
- 5.1 SimpleWebsite 基类站点响应器
- 5.2 自定义站点
- 5.3 AssetsWebsite 基于Assets站点
- 5.3.1 网站首页和index.html
- 5.4 实现
- 5.5 StorageWebsite 基于SD卡
- 5.5.1 网站首页和index.html
- 5.5.2 实现
- 5.6 FileBrowse文件浏览器
- 5.6.1 路径说明
- 5.6.2 实现
- 六、拦截器
- 6.1 拦截登录示例
- 七、过滤器
- 7.1 简单示例
- 7.2 Http缓存过滤器
- 7.2.1 内置缓存过滤器
- 7.2.2 LastModified背景
- 7.2.3 ETag背景
- 7.2.4 过滤器实现
- 7.2.5 接口实现
- 八、ExceptionResolver异常解决者
- 8.1 默认异常及其处理器
- 8.1.1 BaseException 基本类型异常
- 8.1.2 默认异常处理器
- 8.2 自定义异常及其处理器
- 九、其他
- 9.1 Request解析
- 官方网站笔误
- 参考文献
一、概述
- 官方介绍
- github
AndServer是一个Android平台的WebServer服务器和WebServer开发框架,作为Web服务器它支持了IP、端口的绑定,以及SSL自定义证书。作为Web开发框架它支持静态网站部署、动态Http接口、文件的上传下载。
作为一个平台框架,它的最核心的功能应该是对Web开发的支持,它的这些特性可能是你喜欢的:
- Request拦截器
- RequestHandler过滤器
- 全局异常处理者
1.1 基本Api:单服务器多站点
从AndServer1.1.0版本开始,有了站点这个概念。每一个Android机器看作一台服务器,一台服务器可以搭建多个站点,也就是下文中要提到的Server。
从某种意义而言,可以理解为:一台服务器可以部署多个网站,不同的网站绑定不同的端口监听,也就是单服务器多站点;当前站点内,会对当前 IP:Port/Path的Path做响应
下述代码除了端口必须要指定外,几乎所有的Api都不是必须的,开发者选择自己需要的进行设置即可。一般情况下,我们至少会注册一个Http接口的路径和对应的处理器。
如果不绑定IP地址,从mServer.getInetAddress()拿到的IP地址可能是0.0.0.0,这种情况下,服务器默认监听了本机当前局域网IP,例如192.168.1.11
Server mServer; ... mServer = AndServer.serverBuilder() // 服务器设置部分: .sslContext() // 设置SSLConext,加载SSL证书。 .sslSocketInitializer() // 对SSLServerSocket进行一些初始化设置。 .inetAddress() // 设置服务器要监听的网络地址。 .port() // 设置服务器要监听的端口。 .timeout() // Socket的超时时间。 .listener() // 服务器监听。 // Web框架设置部分: .interceptor() // Request/Response对的拦截器。 .website() // 设置网站。 .registerHandler() // 注册一个Http Api路径和对应的处理器。 .filter() // RequestHandler的过滤器。 .exceptionResolver() // 异常解决者。 .build(); ... mServer.start(); // 启动服务器。 mServer.shutdown(); // 关闭服务器。 boolean isRunnging = mServer.isRunning(); // 服务器是否在运行。 InetAddress address = mServer.getInetAddress(); // 拿到服务器监听的网络地址二、Socket通信承载功能一览
AndServer的底层依赖并不是直接写了Socket,而是使用了封装的框架[Apache HttpCore]
可以参看(4.2.46)AndroidGodEye源码整体结构分析 中的原生写法
循环体内:
2.1 Socket socket = mServerSocket.accept();响应一次请求
2.2 mRequestHandler.handle(socket) 处理本次请求
2.2.1 获取请求体中的Path
2.2.2 根据 “后缀命名规则(/空,.html,.js等)“和“已支持Servlet列表(/login,/regiter…)”进行响应,前者返回本地文件数据流,后者返回java处理数据流
2.2.2 根据请求体中的参数,进行对应响应
2.2.2 html页面元素中的标签资源,会再次向服务器做请求,类似“后缀命名规则(/空,.html,.js等)逻辑
2.2.3 返回“HTTP/1.0 200 OK”,“Content-Type: ”,“Content-Length:”头部,以及对应数据体
2.3 socket.close() 结束本次请求
【图1 processon】
在承载功能的基础上,我们对AndServer涉及的基本API进行下字面解读:
服务器设置部分:
- inetAddress(InetAddress var1) 设置服务器要监听的网络地址,默认是本机内网IP
- port(int var1) 设置服务器要监听的端口
- timeout(int var1, TimeUnit var2) Socket的超时时间
- listener(Server.ServerListener var1) 服务器启动、停止及异常监听
- void onStarted();
- void onStopped();
- void onError(Exception var1);
Web框架设置部分:
- registerHandler(String var1, RequestHandler var2) 可多次调用,配置2.2.2中描述的“指定Path”对应的“处理handler,其实就是servlet声明”
- website(WebSite webSite) 单次调用,实现2.2.2中描述,根据指定后缀命名规则,返回本地文件的数据流
- WebSite其实就是一种特殊的RequestHandler,它自带拦截规则(),满足拦截规则的Path就被直接截停
- interceptor(Interceptor interceptor) 单次调用,包裹2.2过程的收尾,在请求进入和请求出去时进行拦截,如果某个请求被拦截,那么所有的Website或者RequestHandler都将收不到任何请求
- boolean onBeforeExecute(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException;
进行逻辑判断是否要拦截,返回true将拦截,返回false不拦截 - void onAfterExecute(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException;
如果不拦截将在请求处理完毕后会调用onAfterExecute
- filter(Filter filter) 单次调用,仅处理请求过程,用于过滤RequestHandler
- void doFilter(RequestHandler handler, HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException;
- exceptionResolver(ExceptionResolver resolver) 单次调用,用来处理在每一个request/response对中发生的所有异常,包括Interceptor、RequestHandler、Filter等的处理过程
- 相当于服务器异常时,应该如何返回数据给客户端
【图2 processon】
三、管理器 Server
- AndServer 暴露给外部使用的server构建器
- Server 管理器能力接口
- 启动、停止、判断运行中、获取ip
- Builder接口能力,配置API
- ServerListener 服务器操作生命周期回调
- Core:真正的管理器实现
3.1 AndServer
暴露给外部使用的server构建器
public class AndServer { private AndServer() { } public static Server.Builder serverBuilder() { return Core.newBuilder(); } }3.2 Server
管理器能力接口
- 启动、停止、判断运行中、获取ip
- Builder接口能力,配置API
- ServerListener 服务器操作生命周期回调
3.3 Core
AndServer核心依赖于HttpCore的HttpServer
自己的业务实现主要在DispatchRequestHandler
- 启动、停止、判断运行中、获取ip
- 配置API,持有相关配置参数
- bulider模式实现 (不再列出,具体看源码)
- ServerListener 服务器操作生命周期回调
3.3.1 持有变量
/** * 服务器设置部分 */ private final InetAddress mInetAddress;//服务器要监听的网络地址 private final int mPort; // 服务器要监听的端口 private final int mTimeout;//Socket的超时时间 private final SSLContext mSSLContext;//SSLConext,加载SSL证书 private final SSLSocketInitializer mSSLSocketInitializer;//对SSLServerSocket进行一些初始化设置 private final ServerListener mListener;//服务器监听 /** * Web框架设置部分 */ private final Map<String, RequestHandler> mRequestHandlerMap;//servlets列表,Path:Servlet的映射关系 private final WebSite mWebSite;//本地网站 private final Interceptor mInterceptor;//全局Request/Response拦截器 private final Filter mFilter;//全局请求过滤器 private final ExceptionResolver mExceptionResolver;//全局异常处理器 /** * 数据:AndServer核心依赖于HttpCore的HttpServer */ private HttpServer mHttpServer; private boolean isRunning;3.3.2 启动
- 【步骤1】为AndServer配置一个全部路径的HttpRequestHandler处理器,AndServer的相关过滤逻辑通过DispatchRequestHandler实现
- 【步骤2】构建HttpServer
- 【步骤3】标示启动状态
- 【步骤4】启动服务器
- 【步骤5】触发启动生命周期
- 【步骤6】虚拟机关闭时的调用钩子,在虚拟机关闭的时候调用这些钩子线程
3.3.3 停止与判断
@Override public boolean isRunning() { return isRunning; } @Override public InetAddress getInetAddress() { if (isRunning) return mHttpServer.getInetAddress(); return null; } /** * Stop core server. */ @Override public void shutdown() { if (!isRunning) return; Executors.getInstance().execute(new Runnable() { @Override public void run() { if (mHttpServer != null) mHttpServer.shutdown(3, TimeUnit.MINUTES);//【步骤1】停止 Executors.getInstance().post(new Runnable() { @Override public void run() { if (mListener != null) mListener.onStopped();//【步骤2】触发启动生命周期 } }); } }); }3.4 DispatchRequestHandler请求分发器
其实就是HttpServer的请求处理器,虽然HttpServer也支持针对Path的处理,但是在这里AndServer进行了包装,所有的URL都由该请求处理器处理,并在该处理器内部实现自己的业务分发
3.4.1 持有变量
private Map<String, RequestHandler> mRequestHandlerMapper = new LinkedHashMap<>();//servlets列表,Path:Servlet的映射关系 private WebSite mWebSite;//本地网站 private Interceptor mInterceptor;//全局Request/Response拦截器 private Filter mFilter;//全局请求过滤器 private ExceptionResolver mExceptionResolver = sDefaultExceptionResolver;//全局异常处理器 private static ExceptionResolver sDefaultExceptionResolver = new SimpleExceptionResolver();//默认的全局处理器 /*****************************get | set*********************************************/ void setInterceptor(Interceptor interceptor) { mInterceptor = interceptor; } void setWebSite(WebSite webSite) { this.mWebSite = webSite; } void registerRequestHandler(String path, RequestHandler handler) { mRequestHandlerMapper.put(path, handler); } void setFilter(Filter filter) { this.mFilter = filter; } void setExceptionResolver(ExceptionResolver exceptionResolver) { mExceptionResolver = exceptionResolver; }3.4.2 核心分发功能
- 【步骤1:触发全局拦截器的拦截事件】
- 【步骤2:处理请求】
- 【步骤2.1:获取本次请求对应的RequestHandler处理器】
2.1 是,则返回该WebSite
2.2 不是,则从 《Path:Servlet的映射表》中获取对应处理器
- 【步骤2.2:本次请求无对应的RequestHandler处理器,抛出异常,进入异常处理器】
- 【步骤2.3:对应的RequestHandler处理器 准备处理请求】
【步骤2.3.1:校验本次请求请求方式是否满足 当前处理器限定】
【步骤2.4:全局过滤器不为null,则进入过滤器(内部进行响应处理)】
【步骤2.5:全局过滤器为null,则直接调用处理器处理】
- 【步骤3:触发全局拦截器的拦截事件】
- 【步骤4:触发全局异常处理器的异常处理】
【图3 】
/*****************************核心:分发与过滤*******************************************/ @Override public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { try { //【步骤1:触发全局拦截器的拦截事件】 if (mInterceptor != null && mInterceptor.onBeforeExecute(request, response, context)) return; //【步骤2:处理请求】 //【步骤2.1:获取本次请求对应的RequestHandler处理器】 RequestHandler handler = getRequestHandler(request, context); if (handler == null) {//【步骤2.2:本次请求无对应的RequestHandler处理器,抛出无对应处理器异常,进入异常处理器】 String path = getRequestPath(request); throw new NotFoundException(path); } else { //【步骤2.3:对应的RequestHandler处理器 准备处理请求】 handleRequest(handler, request, response, context); } //【步骤3:触发全局拦截器的拦截事件】 if (mInterceptor != null) mInterceptor.onAfterExecute(request, response, context); } catch (Exception e) { //【步骤4:触发全局异常处理器的异常处理】 try { mExceptionResolver.resolveException(e, request, response, context); } catch (Exception ee) { sDefaultExceptionResolver.resolveException(e, request, response, context); } } } /** * 【步骤2.1:获取本次请求对应的RequestHandler处理器】 * 1. 获取Path * 2. 判断是否被 WebSite(特殊的RequestHandler处理器)拦截 * 2.1 是,则返回该WebSite * 2.2 不是,则从 《Path:Servlet的映射表》中获取对应处理器 */ private RequestHandler getRequestHandler(HttpRequest request, HttpContext context) throws HttpException, IOException { String path = HttpRequestParser.getRequestPath(request); if (mWebSite != null && mWebSite.intercept(request, context)) { return mWebSite; } return mRequestHandlerMapper.get(path); } /** * Handle Request with handler. * 【步骤2.3:对应的RequestHandler处理器 准备处理请求】 */ private void handleRequest(RequestHandler handler, HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { //【步骤2.3.1:校验本次请求请求方式是否满足 当前处理器限定】 verifyHandler(request, handler); if (mFilter != null) { //【步骤2.3.2:全局过滤器不为null,则进入过滤器(内部进行响应处理)】 mFilter.doFilter(handler, request, response, context); } else { //【步骤2.3.3:全局过滤器为null,则直接调用处理器处理】 handler.handle(request, response, context); } } //【步骤2.3.1:校验本次请求请求方式是否满足 当前处理器限定】 private void verifyHandler(HttpRequest request, RequestHandler handler) throws BaseException { RequestMethod requestMethod = RequestMethod.reverse(request.getRequestLine().getMethod()); Class<?> clazz = handler.getClass(); try { Method handlerMethod = clazz.getMethod("handle", HttpRequest.class, HttpResponse.class, HttpContext.class); RequestMapping requestMapping = handlerMethod.getAnnotation(RequestMapping.class); if (requestMapping != null) { RequestMethod[] requestMethods = requestMapping.method(); List<RequestMethod> requestMethodList = Arrays.asList(requestMethods); if (!requestMethodList.contains(requestMethod)) { throw new MethodNotSupported(requestMethod); } } } catch (NoSuchMethodException ignored) { } }四、RequestHandler请求处理器
DispatchRequestHandler请求分发器会将一次具体的请求,转交给处理器处理。
在AndServer中,每一个Http的path就对应一个RequestHandler。它好比SpringMVC中Controller的某一个方法上加了RequestMapping注解一样,但是很遗憾AndServer目前没有提供像SpringMVC那样的注解来实现path注册(它是我的计划,AndServer2.0将会用编译时注解来实现)
public interface RequestHandler { /** * When is the client request is triggered. * * @param request {@link HttpRequest}. * @param response {@link HttpResponse}. * @param context {@link HttpContext}. * @throws HttpException may be. * @throws IOException read data. */ void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException; }实现一个RequestHandler步骤:
4.1 登录接口示例
HttpRequestParser是一系列工具类方法,能快速帮我们从HttpRequest抽取相关配置参数
public class LoginHandler implements RequestHandler { @Override public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { Map<String, String> params = HttpRequestParser.parseParams(request); if (!params.containsKey("username") || !params.containsKey("password")) { StringEntity stringEntity = new StringEntity("缺少参数", "utf-8"); response.setStatusCode(400); response.setEntity(stringEntity); return; } String userName = URLDecoder.decode(params.get("username"), "utf-8"); String password = URLDecoder.decode(params.get("password"), "utf-8"); if ("123".equals(userName) && "123".equals(password)) { StringEntity stringEntity = new StringEntity("登录成功", "utf-8"); response.setStatusCode(200); response.setEntity(stringEntity); } else { StringEntity stringEntity = new StringEntity("登录失败", "utf-8"); response.setStatusCode(400); response.setEntity(stringEntity); } } }4.2 SimpleRequestHandler 快捷处理器
AndServer还提供了一个简单的RequestHandler的实现SimpleRequestHandler,子类只需要:
我们来看下修改后的Login接口:
public class LoginHandler extends SimpleRequestHandler { @Override public View handle(HttpRequest request) throws HttpException, IOException { Map<String, String> params = HttpRequestParser.parseParams(request); if (!params.containsKey("username") || !params.containsKey("password")) { return new View(400, "缺少参数"); } String userName = URLDecoder.decode(params.get("username"), "utf-8"); String password = URLDecoder.decode(params.get("password"), "utf-8"); if ("123".equals(userName) && "123".equals(password)) { return new View(400, "登录成功"); } else { return new View(400, "帐号或者密码错误"); } } }4.2.1 View响应中间件
- View被用在一些接口的简单实现类中,比如RequestHandler的简单实现类SimpleRequestHandler,减少了入参,可以直接返回View对象
- View会被类SimpleRequestHandler拆解,并设置到Response中
HttpEntity :HttpCore支持的数据类型,Response数据体,底层用数据流实现
- AbstractHttpEntity
- BasicHttpEntity
- ByteArrayEntity
- EntityTemplate
- FileEntity
- InputStreamEntity
- SerializableEntity
- StringEntity
- HttpEntityWrapper
- BufferedHttpEntity
4.2.1.1OkView 成功响应的快捷方式
public class OkView extends View { public OkView() { super(200); } public OkView(String httpBody) { super(200, httpBody); } public OkView(HttpEntity httpEntity) { super(200, httpEntity); } }使用如下:
return new OkView("成功") return new OkView(JSON.toJSONString(user))4.2.1.2 RedirectView 重定向响应
public class RedirectView extends View { private static final String LOCATION = "Location"; public RedirectView(String path) { super(302); setHeader(LOCATION, path); } }使用如下:
return new RedirectView("/user/admin") return new RedirectView("http://www.yanzhenjie.com")4.3 Reponse响应
HttpEntity :HttpCore支持的数据类型,Response数据体,底层用数据流实现
- AbstractHttpEntity
- BasicHttpEntity
- ByteArrayEntity
- EntityTemplate
- FileEntity
- InputStreamEntity
- SerializableEntity
- StringEntity
- HttpEntityWrapper
- BufferedHttpEntity
4.3.1 返回图片
返回图片的方式有很多种,最终我们都要返回HttpEntity这个对象,所以图片可以来自assets,drawable、SD卡,下面是一个来自SD卡的图片示例:
public class ImageHandler extends SimpleRequestHandler { private File mFile = new File(Environment.getExternalStorageDirectory(), "xxx.jpg"); @Override protected View handle(HttpRequest request) throws HttpException, IOException { HttpEntity httpEntity = new FileEntity(mFile); return new View(200, httpEntity); } }4.3.2 下载文件
public class FileHandler extends SimpleRequestHandler { @Override public View handle(HttpRequest request) throws HttpException, IOException { // 为了示例,创建一个临时文件。 File file = File.createTempFile("AndServer", ".txt", App.get().getCacheDir()); OutputStream outputStream = new FileOutputStream(file); IOUtils.write("天上掉下个林妹妹。", outputStream, Charset.defaultCharset()); HttpEntity httpEntity = new FileEntity(file); View view = new View(200, httpEntity); view.addHeader("Content-Disposition", "attachment;filename=AndServer.txt"); return view; } }这里其实和返回图片章节没啥却别,唯一值得注意的是:
view.addHeader("Content-Disposition", "attachment;filename=AndServer.txt");这里我们添加了一个Content-Disposition的响应头,attachment的意思是告诉浏览器,这个文件应该被下载,filename=AndServer.txt的意思是告诉浏览器,这个文件默认被命名为AndServer.txt。
如果不添加上述相应头,比如没有attachment大部分浏览器会直接打开自己可以直接打开的文件,没有filename=AndServer.txt浏览器将会用自己的算法给文件取一个默认名称。
4.3.3 上传图片
作为服务器,在某些情况下可能会接受客户端上传文件的请求。
上传文件涉及到文件保存,所以代码稍微多几行:
public class UploadHandler implements RequestHandler { /** * 保存文件的文件夹。 */ private File mDirectory = Environment.getExternalStorageDirectory(); @Override public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { if (!HttpRequestParser.isMultipartContentRequest(request)) { // 是否Form传文件的请求。 response(403, "说好的文件呢", response); } else { try { processFileUpload(request); response(200, "上传成功", response); } catch (Exception e) { response(500, "保存文件失败", response); } } } private void response(int responseCode, String message, HttpResponse response) throws HttpException, IOException { response.setStatusCode(responseCode); response.setEntity(new StringEntity(message, "utf-8")); } /** * 保存文件和参数处理。 */ private void processFileUpload(HttpRequest request) throws Exception { FileItemFactory factory = new DiskFileItemFactory(1024 * 1024, mDirectory); HttpFileUpload fileUpload = new HttpFileUpload(factory); // 你还可以监听上传进度: // fileUpload.setProgressListener(new ProgressListener(){...}); HttpUploadContext context = new HttpUploadContext((HttpEntityEnclosingRequest) request); List<FileItem> fileItems = fileUpload.parseRequest(context); for (FileItem fileItem : fileItems) { if (fileItem.isFormField()) { // 普通参数。 String key = fileItem.getName(); // 表单参数名。 String value = fileItem.getString(); // 表单参数值。 ...; } else { // 文件。 // fileItem.getFieldName(); // 表单参数名。 // fileItem.getName(); // 客户端指定的文件名。 // fileItem.getSize(); // 文件大小。 // fileItem.getContentType(); // 文件的MimeType。 // 把流写到文件夹。 File uploadedFile = new File(mDirectory, fileItem.getName()); fileItem.write(uploadedFile); } } } }4.3.4 返回Json
返回JSON就比较简单了,其实JSON就是把实体对象转为JSON字符串发送给客户端,归根结底就是发送字符串。
这里我们只要把实体对象转为JSON字符串解决即可:
public class LoginHandler extends SimpleRequestHandler { @Override public View handle(HttpRequest request) throws HttpException, IOException { User user = new User(); user.setName("林妹妹"); user.setSex("女"); String json = JSON.toJSONString(user); return new OkView(json); } }4.4 请求方法限制
对应 《【步骤2.3.1:校验本次请求请求方式是否满足 当前处理器限定】》
很多时候我们需要限制某个接口的请求方法,比如POST、PUT、GET,为了避免每个接口都需要我们检查请求方法,AndServer提供了注解来简化它。
我们只需要在RequestHandler#handle()上加上@RequestMapping()注解即可,例如我们限制某个接口只能用POST请求:
public class LoginHandler implements RequestHandler { @RequestMapping(method = {RequestMethod.POST, RequestMethod.PUT, RequestMethod.GET}) @Override public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { ...; } }注意:如果使用注解必须要在RequestHandler#handle()方法上,因为《【步骤2.3.1:校验本次请求请求方式是否满足 当前处理器限定】》是从handler中读取注解的,也就是说它不支持SimpleRequestHandler的Handle(View)实现,但是你可以通过重写类似SimpleGetRequestHandler的基类来实现
五、WebSite 站点响应器
其实WebSite就是一种特殊的RequestHandler,全局唯一,它自带一个intercept函数。
业务请求在到达后会,先触发boolean WebSiete.intercept(HttpRequest request, HttpContext context),如果被拦截,则不再由RequestHandler处理
我们可以理解为:RequestHandle主要用于返回java运算的数据,而WebSite主要用于返回本地资源(.html/.js等)所转化的数据
public interface WebSite extends RequestHandler { boolean intercept(HttpRequest request, HttpContext context) throws HttpException, IOException; }5.1 SimpleWebsite 基类站点响应器
5.2 自定义站点
5.3 AssetsWebsite 基于Assets站点
AssetsWebsite可以部署assets中的内容为网站,只要传入你要部署的网站在assets中的路径即可。比如我的网站放在/assets/web/webiste目录下,那么我可以这么写:
AssetManager assetsManager = context.getAssets(); Website website = new AssetsWebsite(assetsManager, "assets/web/website");AssetsWebsite会扫描根路径下的全部文件,并形成HttpRequestPath:FilePath的映射表;收到相关请求后,根据FilePath获取到FIle并转换为对应数据流返回
这里值得注意的是,因为assets比较特殊,所以在路径的前后不要带上/,不然将无法解析。
- 例如/assets/web下的网站:new AssetsWebsite(getAssets(), “web”);
- 例如/assets下的网站:new AssetsWebsite(getAssets(), “”);
注意:AssetsWebsite不支持热插拔
5.3.1 网站首页和index.html
传入的目录中如果存在index.html将作为网站的首页,每一个目录中如果存在index.html,将作为这个目录路径的默认页面。
例如我们把assets根目录作为网站部署目录:
assets ├─index.html ├─user │ ├─index.html │ └─admin.html ├─shop │ └─index.html └─items └─index.html假设我们在某个手机上部署new AssetsWebsite(getAssets(), “assets”);为网站,这个手机的本机局域网IP是192.168.1.11,我们指定AndServer监听的端口是8080。那么对于上方这个结构
- 默认首页是http://192.168.1.11:8080或者http://192.168.1.11:8080/index.html,那么访问到的资源就是/assets/index.html
- 访问http://192.168.1.11:8080/user访问到的资源是/assets/user/index.html
5.4 实现
- 持有变量:
- 拦截判断:“资源文件列表”中是否存在
- 数据响应:返回指定文件
- 示例:
5.5 StorageWebsite 基于SD卡
StorageWebsite可以部署SD卡中的内容为网站,只要传入你要部署的网站在SD卡中的路径即可。比如我的网站放在/sdcard/andserver/webiste目录下,那么我可以这么写:
Website website = new StorageWebsite("/sdcard/andserver/website");这里值得注意的是因为SD卡也是磁盘,所以路径必须是绝对路劲。
注意:StorageWebsite支持热插拔
5.5.1 网站首页和index.html
传入的目录中如果存在index.html将作为网站的首页,每一个目录中如果存在index.html,将作为这个目录路径的默认页面。
例如我们把/sdcard/andserver/webiste目录作为网站部署目录:
... ├─index.html ├─user │ ├─index.html │ └─admin.html ├─shop │ └─index.html └─items └─index.html假设我们在某个手机上部署new StorageWebsite(“/sdcard/andserver/webiste”);为网站,这个手机的本机局域网IP是192.168.1.11,我们指定AndServer监听的端口是8080。
那么对于上方这个结构,默认首页是http://192.168.1.11:8080或者http://192.168.1.11:8080/index.html,那么访问到的资源就是/sdcard/andserver/webiste/index.html。
5.5.2 实现
- 持有变量:
- 拦截:指定路径的文件是否存在
- 响应:返回指定的文件
5.6 FileBrowse文件浏览器
FileBrowse可以部署SD卡中的内容供别人以文件的形式浏览。
Website website = new FileBrowser("/sdcard");这里值得注意的是因为SD卡也是磁盘,所以路径必须是绝对路劲。
注意:FileBrowser支持热插拔。
5.6.1 路径说明
如我们把/sdcard目录作为部署目录:
假设我们在某个手机上部署new FileBrowse(“/sdcard”);目录,这个手机的本机局域网IP是192.168.1.11,我们指定AndServer监听的端口是8080
当用户在地址栏输入http://192.168.1.11:8080时并访问后,用户将看到一个网页,网页上面会列出/sdcard下的所有文件和目录,并且在文件和目录上加上超链接。当用户点击文件时,浏览器可能会直接打开这个文件或者让用户下载这个文件;当用户点击目录时,用户将会看到一个新页面中列出了他点击的目录中的所有目录和文件
5.6.2 实现
- 持有变量:
- 拦截:指定路径的文件或文件夹是否存在
- 返回:返回指定路径的文件数据流或文件夹html
六、拦截器
拦截器的功能是在请求进入和请求出去时进行拦截,如果某个请求被拦截,那么所有的Website或者RequestHandler都将收不到任何请求。
拦截器是作用于全局的
public interface Interceptor { /** * When receiving a request, first ask if you intercept, * if intercepted it will not be distributed to any {@code RequestHandler}. */ boolean onBeforeExecute(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException; /** * Called after any {@code RequestHandler} response. */ void onAfterExecute(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException; }6.1 拦截登录示例
假如我们要拦截某些接口的用户登录,我们可以用注解和反射实现。
- 思路:
- 定义一个需要登录的注解
- 需要登录的接口处理器的handle()方法上加上该注解
- 拦截器是作用于全局的,那么在onBeforeExecute中获取到指定的Handler,然后通过反射读取函数注解进行拦截
如果你读完了异常解决者一章的文档,那么我们可以简化一下我们的拦截器:
//定义自定义异常 public class NotLoginException extends BaseException { ... } //抛出的异常,会被全局的异常处理器hold @Override public boolean onBeforeExecute(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { Class<?> clazz = handler.getClass(); Method method = clazz.getMethod("handle", HttpRequest.class, HttpResponse.class, HttpResponse.class, HttpContext.class); NeedLogin needLogin = method.getAnnotation(NeedLogin.class); if (needLogin != null) { if(needLogin.need()) { String token = request.getHeader("Token"); if(verifyToken(token)) { // 假设这里验证Token是否有效。 new throw NotLoginException("请您先登录"); } } } return false; }七、过滤器
- Inteceptor是拦截请求/响应的,也就是拦截Request/Response对的。每一个Request/Response对都会经过拦截器
- Filter是过滤RequestHandler的。
- 当服务器接受到一个请求后,先经过Inteceptor拦截,如果没有被拦截,会根据Request匹配对应的RequestHandler,如果没有找到则抛出NotFoundException异常
- 如果找到对应的RequestHandler,如果开发者设置了Filter,那么把RequestHandler和Request/Response对交给Filter处理,如果没有设置Filter,则由对应的RequestHandler处理
7.1 简单示例
7.2 Http缓存过滤器
过滤器是非常有用的,比如Http缓存协议的实现就可以基于过滤器来做。
在AndServer中,有一个HttpCacheFilter类,它实现了Http缓存协议部分,下面是它实现的几个关键头:
- Cache-Control
- Last-Modified
- If-Modified-Since
- If-Unmodified-Since
- ETag
- If-None-Match
7.2.1 内置缓存过滤器
100%建议开启Website的缓存,它能提高用来提高服务器性能,尤其像AndServer这种运行在手机端的服务器。
Server server = AndServer.serverBuilder() ... .filter(new HttpCacheFilter()) .build(); ...如上述代码即可打开AndServer自带的几个Website的缓存:
- AssetsWebsite
- StorageWebsite
但是自己的RequestHandler必须通过实现对应接口来实现,可以参考AssetsWebsite
7.2.2 LastModified背景
7.2.3 ETag背景
7.2.4 过滤器实现
public class HttpCacheFilter implements Filter { private static final String CACHE_CONTROL = "Cache-Control"; private static final String LAST_MODIFIED = "Last-Modified"; private static final String IF_MODIFIED_SINCE = "If-Modified-Since"; private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; private static final String E_TAG = "ETag"; private static final String IF_NONE_MATCH = "If-None-Match"; @Override public void doFilter(RequestHandler handler, HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { boolean isLastModified; long sourceLastModified = -1;//指定资源最后修改时间 if (isLastModified = handler instanceof LastModified) {//处理器支持 LastModified标签头 sourceLastModified = ((LastModified) handler).getLastModified(request); } boolean isETag; String sourceETag = null;//指定资源的唯一标示 if (isETag = handler instanceof ETag) {//处理器支持 ETag标签头 sourceETag = ((ETag) handler).getETag(request); } Header ifUnmodifiedSinceHeader = request.getFirstHeader(IF_UNMODIFIED_SINCE); if (isLastModified && ifUnmodifiedSinceHeader != null) { if (!validateIfUnmodifiedSince(request, sourceLastModified)) { response.setStatusCode(412); return; } } Header ifModifiedSinceHeader = request.getFirstHeader(IF_MODIFIED_SINCE); Header ifNoneMatchHeader = request.getFirstHeader(IF_NONE_MATCH); if (isLastModified && isETag) { if (ifModifiedSinceHeader != null && ifNoneMatchHeader != null) { if (validateIfModifiedSince(request, sourceLastModified) && validateIfNoneMatch(request, sourceETag)) { response.setStatusCode(304); response.addHeader(CACHE_CONTROL, "public"); response.addHeader(LAST_MODIFIED, generateLastModified(sourceLastModified)); response.addHeader(E_TAG, generateETag(sourceETag)); return; } } } if (isLastModified && ifModifiedSinceHeader != null) { if (validateIfModifiedSince(request, sourceLastModified)) { response.setStatusCode(304); response.addHeader(CACHE_CONTROL, "public"); response.addHeader(LAST_MODIFIED, generateLastModified(sourceLastModified)); return; } } handler.handle(request, response, context); if (isLastModified && sourceLastModified >= 0) { response.addHeader(LAST_MODIFIED, generateLastModified(sourceLastModified)); } if (isETag && sourceETag != null) { response.addHeader(E_TAG, generateETag(sourceETag)); } if (isLastModified) { response.addHeader(CACHE_CONTROL, "public"); } } }7.2.5 接口实现
我们以FileBrowser为例
@Override public long getLastModified(HttpRequest request) throws IOException { String httpPath = trimEndSlash(getRequestPath(request)); File source = findPathSource(httpPath); if (source != null && source.isFile()) return source.lastModified(); return -1; } @Override public String getETag(HttpRequest request) throws IOException { String httpPath = trimEndSlash(getRequestPath(request)); File source = findPathSource(httpPath); if (source != null && source.isFile()) { long sourceSize = source.length(); String sourcePath = source.getAbsolutePath(); long lastModified = source.lastModified(); return sourceSize + sourcePath + lastModified; } return null; }八、ExceptionResolver异常解决者
异常解决者全局唯一,用来处理在每一个request/response对中发生的所有异常,包括Interceptor、RequestHandler、Filter等的处理过程。
它好比SpringMVC中的HandlerExceptionResolver或者@ControllerAdvice + @ExceptionHandler。
我们来看看它的代码:
public interface ExceptionResolver { void resolveException(Exception e, HttpRequest request, HttpResponse response, HttpContext context); }8.1 默认异常及其处理器
8.1.1 BaseException 基本类型异常
AndServer中定义了一个基本的异常BaseException:
public class BaseException extends HttpException { private int mHttpCode; private HttpEntity mHttpBody; public BaseException() { this(500, "Unknown exception occurred on server."); } public BaseException(int httpCode, String httpBody) { super(httpBody); this.mHttpCode = httpCode; this.mHttpBody = new StringEntity(httpBody, ContentType.TEXT_PLAIN); } public int getHttpCode() { return mHttpCode; } public HttpEntity getHttpBody() { return mHttpBody; } }它包含了返回给客户端的响应码和数据,如果不需要添加响应头等信息时,我们的自定义异常可以继承它。
例如,在Http接口章节提到的请求方法中,AndServer会检查请求方法和handle()的注解的请求方法是否匹配,如果不匹配则会抛出一个MethodNotSupported异常,这个异常就是继承自BaseException。
AndServer中在处理请求时会抛出的两个异常:
- NotFoundException,path指定的资源没有找到
- MethodNotSupported,接口不支持的请求方法
8.1.2 默认异常处理器
默认的异常解决者先判断异常是否是自定义异常,如果是那么返回自定义的响应码和响应数据;如果不是则返回500响应码,并告诉客户端哪一个接口出了问题
public class SimpleExceptionResolver implements ExceptionResolver { @Override public final void resolveException(Exception e, HttpRequest request, HttpResponse response, HttpContext context) { View view = resolveException(e, request, response); response.setStatusCode(view.getHttpCode()); response.setEntity(view.getHttpEntity()); response.setHeaders(view.getHeaders()); } public View resolveException(Exception e, HttpRequest request, HttpResponse response) { return resolveException(e); } protected View resolveException(Exception e) { if (e instanceof BaseException) { BaseException exception = (BaseException) e; return new View(exception.getHttpCode(), exception.getHttpBody()); } String message = String.format("Server error occurred:\n%1$s", e.getMessage()); HttpEntity httpEntity = new StringEntity(message, ContentType.TEXT_PLAIN); return new View(500, httpEntity); } }8.2 自定义异常及其处理器
自定义异常解决者可以实现ExceptionResolver接口或者继承SimpleExceptionResolver类。
为了方便,我们这里以继承SimpleExceptionResolver类为例
先定义一个异常的基类:
public class MyException extends HttpException { private int code; private String message; public MyException(int code, String message) { this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; } }定义异常解决者
public class MyResolver extends SimpleExceptionResolver { @ovrride protected View resolveException(Exception e) { if (e instanceof MyException) { MyException exception = (MyException) e; String json = JSON.toJSONString(exception); return new View(exception.getCode(), json); } // 其它未知异常处理。 String message = String.format("服务器发生异常:\n%1$s", e.getMessage()); MyException exception = new MyException(500, message); return new View(exception.getCode(), json); } }这样处理后,当服务器抛出异常后,客户端接收到的数据将是JSON格式:
{ "code":"401", "message":"请您先登录" }未知异常:
{ "code":"500", "message":"服务器发生异常:/user/admin" }九、其他
9.1 Request解析
这里介绍一个比较有用的帮助类HttpRequestParser:
public class HttpRequestParser { /** * 解析当前Request的请求参数。 */ public static Map<String, String> parseParams(HttpRequest request); /** * 解析当前Request的请求参数,可控制是否把参数名转为小写。 */ public static Map<String, String> parseParams(HttpRequest request, boolean lowerCaseNames); /** * 拿到当前Request的path。 */ public static String getRequestPath(HttpRequest request); /** * 当前Requst是否是允许带有Body的。 */ public static boolean isAllowRequestBody(HttpRequest request); /** * 拿到当前Request的请求方法。 */ public static RequestMethod getRequestMethod(HttpRequest request); /** * 当期Request是否带有表单Body。 */ public static boolean isMultipartContentRequest(HttpRequest request); /** * 解析Request的某个Date类型的头为毫秒,如果不存在则为-1。 */ public static long parseDateHeader(HttpRequest request, String headerName); }还有其它几个方法,几乎不会用到,这里不做介绍,有兴趣的开发者可以自行看源码。
官方网站笔误
20180315采集
在逻辑上其实是支持,但是目前的写法是FileBrowse、StorageWebsite、AssetsWebsite不能共存,完全可以通过写一个自定义的WebSite来实现,也可以通过修改框架,将单WebSite改为多WebSite
参考文献
- AndServer - 中文说明