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

CVPR 2025 MIMO: 支持视觉指代和像素grounding 的医学视觉语言模型

CVPR 2025 | MIMO&#xff1a;支持视觉指代和像素对齐的医学视觉语言模型 论文信息 标题&#xff1a;MIMO: A medical vision language model with visual referring multimodal input and pixel grounding multimodal output作者&#xff1a;Yanyuan Chen, Dexuan Xu, Yu Hu…...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)

0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述&#xff0c;后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作&#xff0c;其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...

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

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

376. Wiggle Subsequence

376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...

数据链路层的主要功能是什么

数据链路层&#xff08;OSI模型第2层&#xff09;的核心功能是在相邻网络节点&#xff08;如交换机、主机&#xff09;间提供可靠的数据帧传输服务&#xff0c;主要职责包括&#xff1a; &#x1f511; 核心功能详解&#xff1a; 帧封装与解封装 封装&#xff1a; 将网络层下发…...

【决胜公务员考试】求职OMG——见面课测验1

2025最新版&#xff01;&#xff01;&#xff01;6.8截至答题&#xff0c;大家注意呀&#xff01; 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:&#xff08; B &#xff09; A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...

多种风格导航菜单 HTML 实现(附源码)

下面我将为您展示 6 种不同风格的导航菜单实现&#xff0c;每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...

JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案

JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停​​ 1. ​​安全点(Safepoint)阻塞​​ ​​现象​​:JVM暂停但无GC日志,日志显示No GCs detected。​​原因​​:JVM等待所有线程进入安全点(如…...

代码随想录刷题day30

1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币&#xff0c;另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额&#xff0c;返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...

音视频——I2S 协议详解

I2S 协议详解 I2S (Inter-IC Sound) 协议是一种串行总线协议&#xff0c;专门用于在数字音频设备之间传输数字音频数据。它由飞利浦&#xff08;Philips&#xff09;公司开发&#xff0c;以其简单、高效和广泛的兼容性而闻名。 1. 信号线 I2S 协议通常使用三根或四根信号线&a…...