Android Http-server 本地 web 服务

news2025/2/21 11:03:48

时间: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;

    @Override
    protected 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;
        });

        // 初始化 webview
        mWebView = findViewById(R.id.game_webview);
        initWebview();

        testLocalHttpServer();
    }

    private void testLocalHttpServer(Context context) {
        final String assetsGameFilename = "H5Game";

        copyAssetsGameFileToFiles(context, assetsGameFilename, new FindIndexCallback() {
            @Override
            public void onResult(File indexFile) {
                if (indexFile == null || !indexFile.exists()) {
                    return;
                }

                // 大概测试了下 NanoHTTPD 似乎需要在主线程启动
                H.post(new Runnable() {
                    @Override
                    public void run() {
                        // 启动 http-server
                        if (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() {
            @Override
            public 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);// 游戏基本都有 js
        webSettings.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);
        }
    }

    @Override
    public 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");
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2301885.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python实践-实现实时语音转文字本地部署版(二)

一、技术栈 python 3.10.6 vosk 需下载对应模型&#xff08;vosk-model-cn-0.22&#xff09;模型下载慢的同学看最后的资源链接。 pyaudio keyboard 二、实现功能 本地化实现麦克风语音录入&#xff0c;实时生成文字&#xff0c;并保存至本地文档。 三、实现代码 fro…

tortoiseSVN 如何克隆项目到本地

导入项目成功&#xff0c;如下图&#xff1a;

解决“QString的split()函数分割中文“报错

在使用Qt平台的QString类里的split()函数&#xff0c;分割.txt文件里中文的字符串时&#xff0c;发现中文会乱码。     问题原因&#xff1a;中文使用UTF-16编码。     解决方法&#xff1a;将.txt文件保存为UTF-16编码&#xff0c;然后使用split()去分割对应的字符串即可。…

云平台结合DeepSeek的AI模型优化实践:技术突破与应用革新

目录 前言 一、技术架构&#xff1a;算力与算法的协同基石 1. 蓝耘平台的核心优势 2. DeepSeek的模型创新 二、应用场景&#xff1a;垂直领域的智能化落地 1. 商业领域&#xff1a;智能推荐与客服 2. 工业领域&#xff1a;质检与流程优化 3. 智慧城市与医…

蓝桥杯(B组)-每日一题(1093字符逆序)

c中函数&#xff1a; reverse(首位置&#xff0c;尾位置&#xff09; reverse(s.begin(),s.end()) 头文件&#xff1a;<algorithm> #include<iostream> #include<algorithm>//运用reverse函数的头文件 using namespace std; int main() {string s;//定义一…

jsherp importItemExcel接口存在SQL注入

一、漏洞简介 很多人说管伊佳ERP&#xff08;原名&#xff1a;华夏ERP&#xff0c;英文名&#xff1a;jshERP&#xff09;是目前人气领先的国产ERP系统虽然目前只有进销存财务生产的功能&#xff0c;但后面将会推出ERP的全部功能&#xff0c;有兴趣请帮点一下 二、漏洞影响 …

一文讲清 AIO BIO NIO的区别

引言 在 Java 编程中&#xff0c;BIO&#xff08;Blocking I/O&#xff09;、NIO&#xff08;Non-blocking I/O&#xff09;和 AIO&#xff08;Asynchronous I/O&#xff09;是三种不同的 I/O 模型&#xff0c;它们在处理输入输出操作时有着不同的机制和特点&#xff0c;但是市…

文心一言大模型的“三级跳”:从收费到免费再到开源,一场AI生态的重构实验

2025年2月&#xff0c;百度文心大模型接连抛出两枚“重磅炸弹”&#xff1a;4月1日起全面免费&#xff0c;6月30日正式开源文心大模型4.5系列。这一系列动作不仅颠覆了李彦宏此前坚持的“闭源优势论”13&#xff0c;更标志着中国AI大模型竞争进入了一个全新的阶段——从技术壁垒…

Uniapp 从入门到精通:基础篇 - 搭建开发环境

Uniapp 从入门到精通:基础篇 - 搭建开发环境 前言一、Uniapp 简介1.1 什么是 Uniapp1.2 Uniapp 的优势二、搭建开发环境前的准备2.1 安装 Node.js2.2 安装 HBuilderX三、创建第一个 Uniapp 项目3.1 打开 HBuilderX 并创建项目3.2 项目结构介绍3.3 运行项目四、配置项目4.1 配置…

CSDN文章质量分查询系统【赠python爬虫、提分攻略】

CSDN文章质量分查询系统 https://www.csdn.net/qc 点击链接-----> CSDN文章质量分查询系统 <------点击链接 点击链接-----> https://www.csdn.net/qc <------点击链接 点击链接-----> CSDN文章质量分查询系统 <------点击链接 点击链…

GPT-SoVITS更新V3 win整合包

GPT-SoVITS 是由社区开发者联合打造的开源语音生成框架&#xff0c;其创新性地融合了GPT语言模型与SoVITS&#xff08;Singing Voice Inference and Timbre Synthesis&#xff09;语音合成技术&#xff0c;实现了仅需5秒语音样本即可生成高保真目标音色的突破。该项目凭借其开箱…

CSS三大特性——继承、优先级与层叠

1. 层叠性 概念&#xff1a;如果发生了样式冲突&#xff0c;那就会根据一定的规则&#xff08;选择器优先级&#xff09;&#xff0c;进行样式的层叠&#xff08;覆 盖&#xff09;。 什么是样式冲突&#xff1f; ——— 元素的同一个样式名&#xff0c;被设置了不同的值&…

敏捷开发06:用户故事估算方法介绍

估算介绍 在以前开发 IT 软件时&#xff0c;使用较多的衡量软件开发工作量的单位是&#xff1a;小时、人天 或 人月。它是预估开发时间。比如&#xff1a;这个功能张三一个人开发需要 3 天时间完成。 这种 “人天” 估算只是 “理想人天” 的估算&#xff0c;有时与实际开发完…

环境变量与本地变量

目录 本地变量的创建 环境变量VS本地变量 认识完了环境变量我们来认识一下本地变量。 本地变量的创建 我们如果直接env是看不到本地变量的&#xff0c;因为本地变量和环境变量都具有独立性&#xff0c;环境变量是系统提供的具有全局属性的变量&#xff0c;都存在bash进程的…

Visual Studio中打开多个项目

1) 找到解决方案窗口 2) 右键添加→ 选择现有项目 3) 选择.vcxproj文件打开即可

广西壮族自治区园区投促中心党委书记陶德文率团到访深兰科技

2月16日&#xff0c;广西壮族自治区园区投促中心党委书记、主任&#xff0c;自治区园区办党组成员陶德文率团来到深兰科技集团上海总部考察调研&#xff0c;并与深兰科技集团创始人、董事长陈海波等集团管理层座谈交流&#xff0c;双方围绕深兰科技人工智能项目落地广西的相关事…

1005 K 次取反后最大化的数组和(贪心)

文章目录 题目[](https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/)算法原理源码总结 题目 如上图&#xff0c;k是取反的次数&#xff0c;在数组【4&#xff0c;-1,3】中&#xff0c;当k 1&#xff0c;把-2取反为2&#xff0c;和为9&#xff1b;在数组…

Softing线上研讨会 | 自研还是购买——用于自动化产品的工业以太网

| 线上研讨会时间&#xff1a;2025年1月27日 16:00~16:30 / 23:00~23:30 基于以太网的通信在工业自动化网络中的重要性日益增加。设备制造商正面临着一大挑战——如何快速、有效且经济地将工业以太网协议集成到其产品中。其中的关键问题包括&#xff1a;是否只需集成单一的工…

SpringBoot整合Redis和Redision锁

参考文章 1.Redis 1.导入依赖 <!--Redis依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.c…

数仓搭建(hive):DM搭建(数据集市层)

DM层 数据集市层 &#xff08;Data Mart&#xff09; 粒度上卷&#xff08;Roll-up&#xff09;: 指的是沿着维度层次向上聚合汇总数据&#xff0c;从细粒度到粗粒度观察数据的操作。 示例 数仓的上一层DWS的是按日汇总 DM层基于DWS层主题日宽表上卷统计出按年,月,周的数…