测试http下载

生成ca证书

bash 复制代码
#!/bin/bash

# 配置(只改这3行:服务器IP/域名、证书目录)
SERVER_IP="192.168.31.218"  # 你的Flask服务器IP(鸿蒙客户端连接用)
# CERT_DIR=$(pwd)"/certs"            # 证书保存目录
CCC=$(pwd)
echo "--CERT_DIR=-"${CCC}
CERT_DIR=$(pwd)"/certs"            # 证书保存目录
KEY_SIZE=2048               # 密钥长度(不用改)

# 创建目录
echo "CERT_DIR=-"${CERT_DIR}
if [ -d "${CERT_DIR}" ]
then
    rm -rf "${CERT_DIR}"
fi
mkdir -p $CERT_DIR && cd $CERT_DIR

# 1. 生成CA根证书(ca.key + ca.crt)
openssl genrsa -out ca.key $KEY_SIZE
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -subj "/CN=MyCA"

# 2. 生成服务端证书(server.key + server.crt)
openssl genrsa -out server.key $KEY_SIZE
openssl req -new -key server.key -out server.csr -subj "/CN=$SERVER_IP" -addext "subjectAltName=IP:$SERVER_IP"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -sha256 -days 1825

# 清理中间文件
#rm -f server.csr ca.srl
 
echo "证书生成完成!路径:$(pwd)"
echo "文件:ca.crt(鸿蒙信任)、server.key(Flask私钥)、server.crt(Flask证书)"

服务器代码

python 复制代码
from flask import Flask, request, jsonify,  Response, send_from_directory
import json
import os
from datetime import datetime
import mimetypes

app = Flask(__name__)

# 配置视频文件存储目录(可自定义绝对路径,如 "D:/videos")
VIDEO_FOLDER = os.path.join(os.path.dirname(__file__), "videos")
# 如果目录不存在,自动创建
if not os.path.exists(VIDEO_FOLDER):
    os.makedirs(VIDEO_FOLDER)
    
def getTime():
    # 1. 获取当前时间(包含日期和时间)的字符串,格式:年-月-日 时:分:秒
    current = datetime.now()
    timeStr = current.strftime("%Y-%m-%d %H:%M:%S")
    return timeStr

@app.route('/download/video/<filename>', methods=['GET'])
def download_video_basic(filename):
    """
    基础视频下载接口
    :param filename: 视频文件名(含后缀,如 test.mp4、demo.mov)
    :return: 视频文件下载响应
    """
    try:
        print('download_video_basic {0}, filename={1}'.format(getTime(), filename))
        params = request.args.to_dict()  # 转为字典,方便处理
        print('download_video_basic {0}, params={1}'.format(getTime(), params))
        # send_from_directory:从指定目录返回文件下载
        # as_attachment=True:强制浏览器下载文件(而非在线预览)
        return send_from_directory(
            directory=VIDEO_FOLDER,
            path=filename,
            as_attachment=True,
            # 可选:指定MIME类型,优化浏览器识别
            mimetype='video/mp4'  # 支持 mp4/mov/webm 等,可根据实际后缀调整
        )
    except FileNotFoundError:
        return f"错误:视频文件 {filename} 不存在", 404
    except Exception as e:
        return f"下载失败:{str(e)}", 500

def generate_video_stream(file_path, chunk_size=1024*1024):
    """
    视频文件流式读取生成器
    :param file_path: 视频文件绝对路径
    :param chunk_size: 每次读取的块大小(默认1MB,可调整)
    :return: 逐块返回文件数据
    """
    with open(file_path, 'rb') as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            yield chunk

@app.route('/download/large_video/<filename>', methods=['GET'])
def download_large_video(filename):
    """
    大视频文件流式下载接口(支持断点续传)
    :param filename: 视频文件名(含后缀)
    :return: 流式响应
    """
    # 拼接视频文件绝对路径
    file_path = os.path.join(VIDEO_FOLDER, filename)
    
    # 校验文件是否存在
    if not os.path.exists(file_path):
        return f"错误:视频文件 {filename} 不存在", 404
    
    # 获取文件大小
    file_size = os.path.getsize(file_path)
    # 自动识别视频MIME类型
    mime_type, _ = mimetypes.guess_type(file_path)
    if not mime_type:
        mime_type = 'video/mp4'  # 默认MIME类型

    # 处理断点续传(Range 请求头)
    range_header = request.headers.get('Range')
    if range_header:
        # 解析 Range:如 bytes=0-1023
        start = int(range_header.split('=')[1].split('-')[0])
        end = file_size - 1  # 默认读取到文件末尾
        # 构造响应头:支持断点续传
        response_headers = {
            'Content-Range': f'bytes {start}-{end}/{file_size}',
            'Accept-Ranges': 'bytes',
            'Content-Length': str(end - start + 1),
            'Content-Type': mime_type,
            'Content-Disposition': f'attachment; filename="{filename}"'  # 强制下载
        }
        # 生成部分文件流
        def partial_stream():
            with open(file_path, 'rb') as f:
                f.seek(start)  # 移动到断点起始位置
                while True:
                    chunk = f.read(1024*1024)
                    if not chunk:
                        break
                    yield chunk
        # 返回 206 Partial Content(断点续传响应状态码)
        return Response(partial_stream(), status=206, headers=response_headers)
    else:
        # 普通流式下载(无断点续传)
        response_headers = {
            'Content-Length': str(file_size),
            'Content-Type': mime_type,
            'Content-Disposition': f'attachment; filename="{filename}"'
        }
        return Response(
            generate_video_stream(file_path),
            status=200,
            headers=response_headers
        )

    
# 1. GET 接口(用于查询/获取数据)
@app.route('/get', methods=['GET'])
def receive_get():
    # 获取 GET 参数(支持 URL 问号后传参,如 /get?name=test&age=20)
    params = request.args.to_dict()  # 转为字典,方便处理
    print('/get {0}, params={1}'.format(getTime(), params))
    return jsonify({
        "success": True,
        "method": "GET",
        "received_params": params,
        "tip": "GET 参数示例:/get?key1=value1&key2=value2"
    }), 200

# 2. POST 接口(用于提交/发送数据)
@app.route('/post', methods=['POST'])
def receive_post():
    # 仅支持 JSON 格式(简化逻辑,常用场景)
    if request.is_json:
        data = request.get_json()
        print('receive_post data={0}'.format(json.dump(data)))
        return jsonify({
            "success": True,
            "method": "POST",
            "received_data": data
        }), 200
    # 非法格式返回错误
    return jsonify({
        "success": False,
        "method": "POST",
        "error": "仅支持 JSON 格式,请设置 Content-Type: application/json"
    }), 400

if __name__ == '__main__':
    # 启动 HTTPS 服务器(指定证书和私钥)
    CA_ROOT='../ca/certs/'
    CST = CA_ROOT + "server.crt"
    KEY = CA_ROOT + "server.key"
    
    print('main CST={0}, KEY={1}, {2}, {3}'.format(CST, KEY, os.path.exists(CST), os.path.exists(KEY)))
    app.run(host='0.0.0.0',  # 允许局域网访问
        port=5000,       # HTTPS 端口(测试用 5000,生产可改 443)
        ssl_context=(CST, KEY),  # 证书+私钥路径
        debug=True      # 生产环境关闭 debug
    )

客户端代码

typescript 复制代码
import { rcp } from "@kit.RemoteCommunicationKit";
import { LoggerUtil } from "../utils/LoggerUtil";
import { url } from "@kit.ArkTS";

const BASE_URL: string = "https://192.168.31.218:5000";

export interface IReqestParams {
  path: string;
  method?: 'GET' | "POST" | "PUT" | "DELETE";
  data?: rcp.RequestContent;
  contentType?: rcp.ContentType;
}

const TAG: string = "HttpClient: ";
type DataType = string | number | undefined | null;

export class HttpClient {
  combine(baseURL: string, path: string, params: Record<string, DataType>): string {
    let prefix: string = baseURL + path;
    if (params) {
      let buf = new url.URLParams();
      Object.keys(params).forEach((key: string) => {
        let value = params[key];
        if (typeof value === "string") {
          buf.append(key, value);
        } else if (value === undefined) {
          buf.append(key, 'undefined');
        } else if (value === null) {
          buf.append(key, 'null');
        } else if (typeof value === "number") {
          buf.append(key, String(value));
        } else if (typeof value === 'object'){
          buf.append(key, JSON.stringify(value));
        }
        else {
          buf.append(key, String(value));
        }
      });

      let content: string = buf.toString();
      if (content && content.length > 0) {
        return prefix + "?" + content;
      }
    }
    return prefix;
  }

  request<R>(params: IReqestParams): Promise<R> {
    let hint: string = "request: "
    return new Promise((resolve, reject) => {
      let url: string = BASE_URL + params.path;
      if (params.method === "GET") {
        url = this.combine(BASE_URL, params.path, params.data as Record<string, DataType>);
      }
      let request: rcp.Request = new rcp.Request(url, params.method)
      request.headers = {
        accept: 'application/json',
        "content-type": params.contentType ?? 'application/json'
      };

      if (params.method !== 'GET') {
        request.content = params.data;
      }
      request.configuration = {
        security: { remoteValidation: 'skip' }
      };

      let session: rcp.Session = rcp.createSession();
      session.fetch(request)
        .then((response: rcp.Response) => {
          LoggerUtil.info(TAG, hint, "then response = ", response?.toJSON());
          session.close();
          resolve(response as R);
        })
        .catch((error: BusinessError) => {
          LoggerUtil.error(TAG, hint, "error code = ", error?.code, ", message = ", error?.message);
          session.close();
          reject(error);
        })
    });
  }

  upload<R>(path: string, data: rcp.MultipartForm): Promise<R> {
    return this.request({
      path: path,
      method: "POST",
      data: data,
      contentType: 'multipart/form-data'
    })
  }
}

测试文件

typescript 复制代码
import { IBestUploaderFile } from '@ibestservices/ibest-ui-v2';
import { HttpClient } from '../common/HttpClient';
import { rcp } from '@kit.RemoteCommunicationKit';
import { LoggerUtil } from '../utils/LoggerUtil';
import { FileUtil } from '@pura/harmony-utils';
import fs from '@ohos.file.fs';
import { picker } from '@kit.CoreFileKit';

const TAG: string = "LoggerUtil: Index"

@Entry
@ComponentV2
struct Index {
  @Local uploadFiles: IBestUploaderFile[] = [];
  httpClient: HttpClient = new HttpClient();

  aboutToAppear(): void {

  }

  async saveLocalPhotoAlbum(url: string, fileName: string) {
    let saveUris: Array<string> = [];
    try {
      const photoSaveOptions = new picker.PhotoSaveOptions(); // 创建文件管理器保存选项实例
      photoSaveOptions.newFileNames = [fileName];
      const photoViewPicker = new picker.PhotoViewPicker();

      try {
        let photoSaveResult = await photoViewPicker.save(photoSaveOptions);
        if (photoSaveResult !== undefined) {
          saveUris = photoSaveResult;
          console.info('photoViewPicker.save to file succeed and uris are:' + photoSaveResult);
        }
      } catch (error) {
        let err: BusinessError = error as BusinessError;
        LoggerUtil.error(TAG,
          `[picker] Invoke photoViewPicker.save failed, code is ${err.code}, message is ${err.message}`);
      }
    } catch (error) {
      let err: BusinessError = error as BusinessError;
      LoggerUtil.error(TAG, "[picker] photoViewPickerSave error = " + JSON.stringify(err));
    }

    try {
      let photoSelect = fs.openSync(url, fs.OpenMode.READ_ONLY);
      let photoSave = fs.openSync(saveUris[0], fs.OpenMode.WRITE_ONLY);
      fs.copyFileSync(photoSelect.fd, photoSave.fd);
      fs.close(photoSelect);
      fs.close(photoSave);
    } catch (error) {
      let err: BusinessError = error as BusinessError;
      LoggerUtil.info(TAG, "[picker] Photo Save error = " + JSON.stringify(err));
    }
  }

  build() {
    Column({ space: 16 }) {
      Button("get")
        .onClick(() => {
          this.httpClient.request<rcp.Response>({
            path: "/get",
            method: "GET",
            data: { "k1": "v1", "k2": "v2", "k3": "v3" },
            contentType: 'application/x-www-form-urlencoded'
          })
            .then((response: rcp.Response) => {
              LoggerUtil.info(TAG, "GET response = ", response?.toJSON());
            })
            .catch((error: BusinessError) => {
              LoggerUtil.error(TAG, "error ", error?.message);
            })
        });

      Button("post upload")
        .onClick(() => {
          this.httpClient.request<rcp.Response>({
            path: "/post",
            method: "POST",
            data: "key1=v1&k2=v2",
            contentType: 'multipart/form-data'
          })
            .then((response: rcp.Response) => {
              LoggerUtil.info(TAG, "GET response = ", response?.toJSON());
            })
            .catch((error: BusinessError) => {
              LoggerUtil.error(TAG, "error ", error?.message);
            })
        });

      Button("download small video")
        .onClick(() => {
          // /download/video/22mluan.mp4
          let fileName: string = "22mluan.mp4"
          this.httpClient.request<rcp.Response>({
            path: "/download/video/" + fileName,
            method: "GET",
            // data: {filename: "22mluan.mp4"},
            contentType: 'multipart/form-data'
          })
            .then(async (response: rcp.Response) => {
              let len = response.body?.byteLength
              LoggerUtil.info(TAG, "GET response = ", JSON.stringify(response?.toJSON()));
              LoggerUtil.info(TAG, "GET len = ", len);
              let root: string = this.getUIContext().getHostContext()?.filesDir + "/videos/";
              LoggerUtil.info(TAG, " BEFORE isExist = ", FileUtil.accessSync(root));
              if (FileUtil.accessSync(root)) {
                FileUtil.rmdirSync(root);
              }
              FileUtil.mkdirSync(root, true);
              LoggerUtil.info(TAG, " after isExist = ", FileUtil.accessSync(root));
              let path: string = root + fileName;
              FileUtil.writeEasy(path, response.body);
              LoggerUtil.info(TAG, " after isFileExist = ", FileUtil.isFile(path));
              await this.saveLocalPhotoAlbum(path, fileName);
              LoggerUtil.info(TAG, "OK after isFileExist = ", FileUtil.isFile(path));
            })
            .catch((error: BusinessError) => {
              LoggerUtil.error(TAG, "error ", error?.message);
            })
        });
    }.width('100%')
    .height('100%')
  }
}
相关推荐
代码游侠2 小时前
应用——SQLite3 C 编程学习
linux·服务器·c语言·数据库·笔记·网络协议·sqlite
秋深枫叶红2 小时前
嵌入式第四十一篇——网络编程——udp和tcp
网络·网络协议·学习·udp
开***能2 小时前
精准控能耗,协议零阻碍!EtherCAT转 Profinet网关技术赋能
服务器·网络·人工智能
QT 小鲜肉2 小时前
【Linux命令大全】001.文件管理之split命令(实操篇)
linux·运维·服务器·网络·笔记
草莓熊Lotso2 小时前
Qt 入门核心指南:从框架认知到环境搭建 + Qt Creator 实战
xml·开发语言·网络·c++·人工智能·qt·页面
寂寞恋上夜2 小时前
边界条件检查清单:数据为空/超长/特殊字符/越界(附测试用例)
服务器·网络·测试用例·markdown转xmind·在线思维导图生成器
松涛和鸣2 小时前
42、SQLite3 :字典入库与数据查询
linux·前端·网络·数据库·udp·sqlite
QT 小鲜肉2 小时前
【Linux命令大全】001.文件管理之rcp命令(实操篇)
linux·服务器·网络·chrome·笔记
代码游侠2 小时前
学习笔记——SQLite3 编程与 HTML 基础
网络·笔记·算法·sqlite·html