当前位置: 首页 > 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格式,导致无法正常解析。对于这个问题,有如下…...

反向工程与模型迁移:打造未来商品详情API的可持续创新体系

在电商行业蓬勃发展的当下&#xff0c;商品详情API作为连接电商平台与开发者、商家及用户的关键纽带&#xff0c;其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息&#xff08;如名称、价格、库存等&#xff09;的获取与展示&#xff0c;已难以满足市场对个性化、智能…...

Zustand 状态管理库:极简而强大的解决方案

Zustand 是一个轻量级、快速和可扩展的状态管理库&#xff0c;特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...

AtCoder 第409​场初级竞赛 A~E题解

A Conflict 【题目链接】 原题链接&#xff1a;A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串&#xff0c;只有在同时为 o 时输出 Yes 并结束程序&#xff0c;否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...

前端导出带有合并单元格的列表

// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...

将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?

Otsu 是一种自动阈值化方法&#xff0c;用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理&#xff0c;能够自动确定一个阈值&#xff0c;将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...

SQL慢可能是触发了ring buffer

简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...

LLMs 系列实操科普(1)

写在前面&#xff1a; 本期内容我们继续 Andrej Karpathy 的《How I use LLMs》讲座内容&#xff0c;原视频时长 ~130 分钟&#xff0c;以实操演示主流的一些 LLMs 的使用&#xff0c;由于涉及到实操&#xff0c;实际上并不适合以文字整理&#xff0c;但还是决定尽量整理一份笔…...

MinIO Docker 部署:仅开放一个端口

MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...

HubSpot推出与ChatGPT的深度集成引发兴奋与担忧

上周三&#xff0c;HubSpot宣布已构建与ChatGPT的深度集成&#xff0c;这一消息在HubSpot用户和营销技术观察者中引发了极大的兴奋&#xff0c;但同时也存在一些关于数据安全的担忧。 许多网络声音声称&#xff0c;这对SaaS应用程序和人工智能而言是一场范式转变。 但向任何技…...

阿里云Ubuntu 22.04 64位搭建Flask流程(亲测)

cd /home 进入home盘 安装虚拟环境&#xff1a; 1、安装virtualenv pip install virtualenv 2.创建新的虚拟环境&#xff1a; virtualenv myenv 3、激活虚拟环境&#xff08;激活环境可以在当前环境下安装包&#xff09; source myenv/bin/activate 此时&#xff0c;终端…...