当前位置: 首页 > news >正文

Android Http-server 本地 web 服务

时间:2025年2月16日

地点:深圳.前海湾

需求

我们都知道 webview 可加载 URI,他有自己的协议 scheme:

  • content://  标识数据由 Content Provider 管理
  • file://     本地文件 
  • http://     网络资源

特别的,如果你想直接加载 Android 应用内 assets 内的资源你需要使用`file:///android_asset`,例如:

file:///android_asset/demo/index.html

我们本次的需求是:有一个 H5 游戏,需要 http 请求 index.html 加载、运行游戏

通常我们编写的 H5 游戏直接拖动 index.html 到浏览器打开就能正常运行游戏,当本次的游戏就是需要 http 请求才能,项目设计就是这样子啦(省略一千字)

开始

如果你有一个 index.html 的 File 对象 ,可以使用`Uri.fromFile(file)` 转换获得 Uri 可以直接加载

mWebView.loadUrl(uri.toString());

这周染上甲流,很不舒服,少废话直接上代码

  • 复制 assets 里面游戏文件到 files 目录
  • 找到 file 目录下的 index.html
  • 启动 http-server 服务
  • webview 加载 index.html
import java.io.File;public class MainActivity extends AppCompatActivity {private final String TAG = "hello";private WebView mWebView;private Handler H = new Handler(Looper.getMainLooper());private final int LOCAL_HTTP_PORT = 8081;private final String SP_KEY_INDEX_PATH = "index_path";private LocalHttpGameServer mLocalHttpGameServer;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);EdgeToEdge.enable(this);setContentView(R.layout.activity_main);ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);return insets;});// 初始化 webviewmWebView = findViewById(R.id.game_webview);initWebview();testLocalHttpServer();}private void testLocalHttpServer(Context context) {final String assetsGameFilename = "H5Game";copyAssetsGameFileToFiles(context, assetsGameFilename, new FindIndexCallback() {@Overridepublic void onResult(File indexFile) {if (indexFile == null || !indexFile.exists()) {return;}// 大概测试了下 NanoHTTPD 似乎需要在主线程启动H.post(new Runnable() {@Overridepublic void run() {// 启动 http-serverif (mLocalHttpGameServer == null) {final String gameRootPath = indexFile.getParentFile().getAbsolutePath();mLocalHttpGameServer = new LocalHttpGameServer(LOCAL_HTTP_PORT, gameRootPath);}// 访问本地服务 localhost 再合适不过// 当然你也可以使用当前网络的 IP 地址,但是你得获取 IP 地址,指不定还有什么获取敏感数据的隐私String uri = "http://localhost:" + LOCAL_HTTP_PORT + "/index.html";mWebView.loadUrl(uri);}});}});}// 把 assets 目录下的文件拷贝到应用 files 目录private void copyAssetsGameFileToFiles(Context context, String filename, FindIndexCallback callback) {if (context == null) {return;}String gameFilename = findGameFilename(context.getAssets(), filename);// 文件拷贝毕竟是耗时操作,开启一个子线程吧new Thread(new Runnable() {@Overridepublic void run() {// 读取拷贝到 files 目录后 index.html 文件路径的缓存// 防止下载再次复制文件String indexPath = SPUtil.getString(SP_KEY_INDEX_PATH, "");if (!indexPath.isEmpty() && new File(indexPath).exists()) {if (callback != null) {callback.onResult(new File(indexPath));}return;}File absGameFileDir = copyAssetsToFiles(context, gameFilename);// 拷贝到 files 目录后,找到第一个 index.html 文件缓存路径File indexHtml = findIndexHtml(absGameFileDir);if (indexHtml != null && indexHtml.exists()) {SPUtil.setString(SP_KEY_INDEX_PATH, indexHtml.getAbsolutePath());}if (callback != null) {callback.onResult(indexHtml);}}}).start();}public File copyAssetsToFiles(Context context, String assetFileName) {File filesDir = context.getFilesDir();File outputFile = new File(filesDir, assetFileName);try {String fileNames[] = context.getAssets().list(assetFileName);if (fileNames == null) {return null;}// lenght == 0 可以认为当前读取的是文件,否则是目录if (fileNames.length > 0) {if (!outputFile.exists()) {outputFile.mkdirs();}// 目录,主要路径拼接,因为需要拷贝目录下的所有文件for (String fileName : fileNames) {// 递归哦copyAssetsToFiles(context, assetFileName + File.separator + fileName);}} else {// 文件InputStream is = context.getAssets().open(assetFileName);FileOutputStream fos = new FileOutputStream(outputFile);byte[] buffer = new byte[1024];int byteCount;while ((byteCount = is.read(buffer)) != -1) {fos.write(buffer, 0, byteCount);}fos.flush();is.close();fos.close();}} catch (Exception e) {return null;}return outputFile;}private interface FindIndexCallback {void onResult(File indexFile);}public static File findIndexHtml(File directory) {if (directory == null || !directory.exists() || !directory.isDirectory()) {return null;}File[] files = directory.listFiles();if (files == null) {return null;}for (File file : files) {if (file.isFile() && file.getName().equals("index.html")) {return file;} else if (file.isDirectory()) {File index = findIndexHtml(file);if (index != null) {return index;}}}return null;}private String findGameFilename(AssetManager assets, String filename) {try {// 这里传空字符串,读取返回 assets 目录下所有的名列表String[] firstFolder = assets.list("");if (firstFolder == null || firstFolder.length == 0) {return null;}for (String firstFilename : firstFolder) {if (firstFilename == null || firstFilename.isEmpty()) {continue;}if (firstFilename.equals(filename)) {return firstFilename;}}} catch (IOException e) {}return null;}private void initWebview() {mWebView.setBackgroundColor(Color.WHITE);WebSettings webSettings = mWebView.getSettings();webSettings.setJavaScriptEnabled(true);// 游戏基本都有 jswebSettings.setDomStorageEnabled(true);webSettings.setAllowUniversalAccessFromFileURLs(true);webSettings.setAllowContentAccess(true);// 文件是要访问的,毕竟要加载本地资源webSettings.setAllowFileAccess(true);webSettings.setAllowFileAccessFromFileURLs(true);webSettings.setUseWideViewPort(true);webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);webSettings.setJavaScriptCanOpenWindowsAutomatically(true);webSettings.setLoadWithOverviewMode(true);webSettings.setDisplayZoomControls(false);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);}if (Build.VERSION.SDK_INT >= 26) {webSettings.setSafeBrowsingEnabled(true);}}
}

差点忘了,高版本 Android 设备需要配置允许 http 明文传输,AndroidManifest 需要以下配置:

  1. 必须有网络权限 <uses-permission android:name="android.permission.INTERNET" />
  2. application 配置 ​​​​​​​​​​​​​​​​​​
  • android:networkSecurityConfig="@xml/network_security_config
  • ​​​​​​​​​​​​​​​​​​​​​android:usesCleartextTraffic="true"

network_security_config.xml

<?xml version="1.0" encoding="UTF-8"?><network-security-config><base-config cleartextTrafficPermitted="true"><trust-anchors>     <certificates src="user"/>      <certificates src="system"/>    </trust-anchors>   </base-config>
</network-security-config>

http-server 服务类很简单,感谢开源

今天的主角:NanoHttpd Java中的微小、易于嵌入的HTTP服务器

这里值得关注的是 gameRootPath,有了它才能正确找到本地资源所在位置

package com.example.selfdemo.http;import android.util.Log;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;import fi.iki.elonen.NanoHTTPD;public class LocalHttpGameServer extends NanoHTTPD {private String gameRootPath = "";private final String TAG = "hello";public GameHttp(int port, String gameRootPath) {super(port);this.gameRootPath = gameRootPath;init();}public GameHttp(String hostname, int port, String gameRootPath) {super(hostname, port);this.gameRootPath = gameRootPath;init();}private void init() {try {final int TIME_OUT = 1000 * 60;start(TIME_OUT, true);//start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);Log.d(TAG, "http-server init: 启动");} catch (IOException e) {Log.d(TAG, "http-server start error = " + e);}}@Overridepublic Response serve(IHTTPSession session) {String uri = session.getUri();       String filePath = uri;//gameRootPath 游戏工作目录至关重要//有了游戏工作目录,http 请求 URL 可以更简洁、更方便if(gameRootPath != null && gameRootPath.lenght() !=0){filePath = gameRootPath + uri;}File file = new File(filePath);//web 服务请求的是资源,目录没有多大意义if (!file.exists() || !file.isFile()) {return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "404 Not Found");}//读取文件并返回try {FileInputStream fis = new FileInputStream(file);String mimeType = NanoHTTPD.getMimeTypeForFile(uri);return newFixedLengthResponse(Response.Status.OK, mimeType, fis, file.length());} catch (IOException e) {return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "500 Internal Error");}}
}

相关文章:

Android Http-server 本地 web 服务

时间&#xff1a;2025年2月16日 地点&#xff1a;深圳.前海湾 需求 我们都知道 webview 可加载 URI&#xff0c;他有自己的协议 scheme&#xff1a; content:// 标识数据由 Content Provider 管理file:// 本地文件 http:// 网络资源 特别的&#xff0c;如果你想直接…...

腾讯的webUI怎样实现deepseek外部调用 ; 腾讯云通过API怎样调用deepseek

腾讯的webUI怎样实现deepseek外部调用 目录 腾讯的webUI怎样实现deepseek外部调用腾讯云通过API怎样调用deepseekhtml方式curl方式python方式腾讯云通过API怎样调用deepseek 重点说明:不需要SK,仅仅使用ip和端口号 html方式 <!DOCTYPE html> <html lang="e…...

DeepSeek VS ChatGPT-速度、准确性和成本

撰写本文时马斯克刚刚发布了聊天机器人Grok2&#xff0c;10万张算卡体现了马斯克的财大气粗。近年来&#xff0c;人工智能模型取得了长足的发展&#xff0c;每个模型都力求在速度、准确性和成本效率方面超越其他模型。在本文中&#xff0c;我将深入研究比较中美在AI的焦点模型上…...

内外网隔离文件传输解决方案|系统与钉钉集成+等保合规,安全提升70%

一、背景与痛点 在内外网隔离的企业网络环境中&#xff0c;员工与外部协作伙伴&#xff08;如钉钉用户&#xff09;的文件传输面临以下挑战&#xff1a; 1. **安全性风险**&#xff1a;内外网直连可能导致病毒传播、数据泄露。 2. **操作繁琐**&#xff1a;传统方式需频繁切…...

Linux基础开发工具的使用(apt、vim、gcc、g++、gdb、make、makefile)

Linux软件包管理器–apt Linux安装软件的方式 在Linux下安装软件的方法有以下三种&#xff1a; 下载到程序的源代码&#xff0c;自己编译出可执行程序获取deb安装包、然后使用dpkg命令安装。&#xff08;不解决依赖关系&#xff09;通过apt进行安装软件。 小知识点&#xf…...

最新版IDEA下载安装教程

一、下载IDEA 点击前往官网下载 或者去网盘下载 点击前往百度网盘下载 点击前往夸克网盘下载 进去后点击IDEA 然后点击Download 选择自己电脑对应的系统 点击下载 等待下载即可 二、安装IDEA 下载好后双击应用程序 点击下一步 选择好安装目录后点击下一步 勾选这两项后点击…...

MacOS 15.3 卸载系统内置软件

1、关闭系统完整性&#xff08;SIP&#xff09; 进入恢复模式(recovery) 如果您使用的是黑苹果或者白苹果&#xff0c;可以选择 重启按住CommandR 进入&#xff0c;如果是M系列芯片&#xff0c;长按开机键&#xff0c;进入硬盘选择界面进入。 我是MacMini M4芯片&#xff0c;关…...

发现问题 python3.6.13+django3.2.5 只能以asgi启动server 如何解决当前问题

在 Python 3.6.13 和 Django 3.2.5 的组合下&#xff0c;如果你发现只能使用 ASGI 启动 Django 服务&#xff0c;而不能使用 WSGI&#xff0c;可能的原因有几个。我们来分析一下常见的问题和解决方案。 1. 默认 ASGI 支持 从 Django 3.0 开始&#xff0c;Django 引入了对 ASG…...

python3+TensorFlow 2.x(六)自编码器

自动编码器 自动编码器&#xff08;Autoencoder&#xff09;是一种无监督学习算法&#xff0c;主要用于数据降维、特征学习和数据生成等任务。它由编码器和解码器组成&#xff0c;目标是将输入数据压缩为低维表示&#xff08;编码&#xff09;&#xff0c;然后再从这个低维表示…...

Redis-AOF

AOF 前言什么是AOF执行后写入的好处避免额外的检查开销不会阻塞当前写操作命令的执行 潜在风险数据丢失阻塞下一个命令 三种写回策略AOF重写机制AOF后台重写数据副本的生成写时复制写时复制的阻塞问题 AOF重写缓冲区子进程重写期间工作内容 总结 前言 RDB方式不能提供强一致性…...

【DeepSeek】本地部署,保姆级教程

deepseek网站链接传送门&#xff1a;DeepSeek 在这里主要介绍DeepSeek的两种部署方法&#xff0c;一种是调用API&#xff0c;一种是本地部署。 一、API调用 1.进入网址Cherry Studio - 全能的AI助手选择立即下载 2.安装时位置建议放在其他盘&#xff0c;不要放c盘 3.进入软件后…...

并查集算法篇上期:并查集原理及实现

引入 那么我们在介绍我们并查集的原理之前&#xff0c;我们先来看一下并查集所应用的一个场景&#xff1a;那么现在我们有一个长度为n的数组&#xff0c;他们分别属于不同的集合&#xff0c;那么现在我们要查询数组当中某个元素和其他元素是否处于同一集合当中&#xff0c;或者…...

如何在WPS打开的word、excel文件中,使用AI?

1、百度搜索&#xff1a;Office AI官方下载 或者直接打开网址&#xff1a;https://www.office-ai.cn/static/introductions/officeai/smartdownload.html 打开后会直接提示开始下载中&#xff0c;下载完成后会让其选择下载存放位置&#xff1a; 选择位置&#xff0c;然后命名文…...

【Deepseek+Dify】wsl2+docker+Deepseek+Dify部署本地大模型知识库问题总结

wsl2dockerDeepseekDify部署本地大模型知识库问题总结 基于ollama部署本地文本模型和嵌入模型 部署教程 DeepSeekdify 本地知识库&#xff1a;真的太香了 问题贴&#xff1a;启动wsl中docker中的dify相关的容器 发现postgre服务和daemon服务一直在重启&#xff0c;导致前端加…...

C++初阶——简单实现vector

目录 1、前言 2、Vector.h 3、Test.cpp 1、前言 简单实现std::vector类模板。 相较于前面的string&#xff0c;vector要注意&#xff1a; 深拷贝&#xff0c;因为vector的元素可能是类类型&#xff0c;类类型元素可以通过赋值重载&#xff0c;自己实现深拷贝。 迭代器失效…...

1.21作业

1 unserialize3 当序列化字符串中属性个数大于实际属性个数时&#xff0c;不会执行反序列化 外部如果是unserialize&#xff08;&#xff09;会调用wakeup&#xff08;&#xff09;方法&#xff0c;输出“bad request”——构造url绕过wakeup 类型&#xff1a;public class&…...

深度集成DeepSeek大模型:WebSocket流式聊天实现

目录 5分钟快速接入DeepSeek大模型&#xff1a;WebSocket实时聊天指南创建应用开发后端代码 (Python/Node.js)结语 5分钟快速接入DeepSeek大模型&#xff1a;WebSocket实时聊天指南 创建应用 访问DeepSeek官网 前往 DeepSeek官网。如果还没有账号&#xff0c;需要先注册一个。…...

Jmeter连接数据库、逻辑控制器、定时器

Jmeter直连数据库 直接数据库的使用场景 直连数据库的关键配置 添加MYSQL驱动Jar包 方式一&#xff1a;在测试计划面板点击“浏览”按钮&#xff0c;将你的JDBC驱动添加进来 方式二&#xff1a;将MySQL驱动jar包放入到lib/ext目录下&#xff0c;重启JMeter 配置数据库连接信…...

『Linux笔记』进程间通信(IPC)详细介绍!

进程间通信&#xff08;IPC&#xff09;详细介绍&#xff01; 文章目录 一. 进程间通信&#xff08;IPC&#xff09;详细介绍1. 共享内存&#xff08;Shared Memory&#xff09;2. 消息队列&#xff08;Message Queues&#xff09;3. 信号量&#xff08;Semaphores&#xff09…...

Jmeter进阶篇(34)如何解决jmeter.save.saveservice.timestamp_format=ms报错?

问题描述 今天使用Jmeter完成压测执行,然后使用命令将jtl文件转换成html报告时,遇到了报错! 大致就是说jmeter里定义了一个jmeter.save.saveservice.timestamp_format=ms的时间格式,但是jtl文件中的时间格式不是标准的这个ms格式,导致无法正常解析。对于这个问题,有如下…...

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

Android Wi-Fi 连接失败日志分析

1. Android wifi 关键日志总结 (1) Wi-Fi 断开 (CTRL-EVENT-DISCONNECTED reason3) 日志相关部分&#xff1a; 06-05 10:48:40.987 943 943 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid44:9b:c1:57:a8:90 reason3 locally_generated1解析&#xff1a; CTR…...

在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:

在 HarmonyOS 应用开发中&#xff0c;手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力&#xff0c;既支持点击、长按、拖拽等基础单一手势的精细控制&#xff0c;也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档&#xff0c…...

Cesium1.95中高性能加载1500个点

一、基本方式&#xff1a; 图标使用.png比.svg性能要好 <template><div id"cesiumContainer"></div><div class"toolbar"><button id"resetButton">重新生成点</button><span id"countDisplay&qu…...

【项目实战】通过多模态+LangGraph实现PPT生成助手

PPT自动生成系统 基于LangGraph的PPT自动生成系统&#xff0c;可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析&#xff1a;自动解析Markdown文档结构PPT模板分析&#xff1a;分析PPT模板的布局和风格智能布局决策&#xff1a;匹配内容与合适的PPT布局自动…...

【Go】3、Go语言进阶与依赖管理

前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课&#xff0c;做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程&#xff0c;它的核心机制是 Goroutine 协程、Channel 通道&#xff0c;并基于CSP&#xff08;Communicating Sequential Processes&#xff0…...

【HTML-16】深入理解HTML中的块元素与行内元素

HTML元素根据其显示特性可以分为两大类&#xff1a;块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...

聊一聊接口测试的意义有哪些?

目录 一、隔离性 & 早期测试 二、保障系统集成质量 三、验证业务逻辑的核心层 四、提升测试效率与覆盖度 五、系统稳定性的守护者 六、驱动团队协作与契约管理 七、性能与扩展性的前置评估 八、持续交付的核心支撑 接口测试的意义可以从四个维度展开&#xff0c;首…...

tree 树组件大数据卡顿问题优化

问题背景 项目中有用到树组件用来做文件目录&#xff0c;但是由于这个树组件的节点越来越多&#xff0c;导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多&#xff0c;导致的浏览器卡顿&#xff0c;这里很明显就需要用到虚拟列表的技术&…...

使用 SymPy 进行向量和矩阵的高级操作

在科学计算和工程领域&#xff0c;向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能&#xff0c;能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作&#xff0c;并通过具体…...