Z'zyc

我是一名iOS开发者,我是一名非计算机专业出生的程序员。

你可以通过 1748439277@qq.com 联系我

Z'zyc

我是一名iOS开发者,我是一名非计算机专业出生的程序员。

如果学习很幸苦,那就尝试无知的代价

记录一次鸿蒙下web容器跨域问题解决经历
2024-05-25 技术分享 阅 659

引言

最近纯血鸿蒙(Harmony Next)的概念很火热,我们公司也在3月初正式推进纯血鸿蒙的应用开发,不得不说纯血鸿蒙系统的 UI 表现和 iOS 毫无差异,响应速度相当快,在开发后了一段时间后,我才明白,这可能得益于JS出色的执行性能,果然,继JS支配了后端之后,终于可以“直接”写App了,相信不久的将来,JS将一统江湖。

好了,进入正题,这一次在开发鸿蒙的工程中,遇到了老朋友“跨域”问题,实际上跨域问题我们接触过也解决了很多次,但这一次,在鸿蒙下的解决经历,又让我成长了一点。

我遇到了什么问题?

在和团队伙伴共同熬夜的一个月里,我们完成了公司内部纯血鸿蒙应用开发框架,同时,正积极的向产品侧进行推广研发,在推进一个礼拜后,就有伙伴通知我们,所有的小程序应用无法访问,(这里的小程序应用指的是我们自己研发的小程序容器,并不是微信小程序)我们上层的小程序应用量级很大,这是灾难性的问题,需要尽快处理。

我也是第一时间对问题进行了剖析排查,在通过反复的调试、打断点发现,小程序的资源被正确渲染,小程序上的接口请求始终没有发送出去,其实到这里,我并没有想到是跨域问题,因为 vconsole 上没有错误信息,其次是因为自己知识淡泊,在调试中,我发现jquery的$.ajax可以发送出去,axios的请求发送不出去,还和H5框架同学掐起了架,希望对方解决,这个一会儿再说,总之,没往跨域方向上思考,只想到前端框架是不是做了什么不一样的操作,后来,通过 arkweb 控件上的 onConsole 回调方法,我得倒了错误信息:

ArkWeb 代码

Web({ xxx })
.onConsole((event: object)=>{
  ELogger.debug(this.TAG,
    "onConsole "
      + `message:${event["message"].getMessage()} `
      + `line:${event["message"].getLineNumber()} `
      + `sourceid:${event["message"].getSourceId()} `
      + `level:${event["message"].getMessageLevel()} `
  )
  return false
})

错误信息:

XXXWebView, onConsole message:Access to XMLHttpRequest at 'xxx' from origin 'null' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present 
on the requested resource. line:0 sourceid:file:///xxx level:4

我看到了跨域信息报错,很显然,本地H5资源发送 ajax 的请求,请求头上带上了 Origin:null,导致了跨域,😑,我早该想到的,(这里需要说明下,我们的服务端不认可 Origin:null ,这不安全,但认可 file://, 所以我们过去的解决方案是通过系统提供的配置,让客户端将 Origin:null 修改为 Origin:file://,结合服务端配置就可以解决),知道问题就很容易了,因为在 Android、iOS 上,可以通过系统提供的配置进行修改:

Android 配置:

WebSettings webSettings = webView.getSettings(); 
webSettings.setJavaScriptEnabled(true); 
webSettings.setAllowFileAccess(true); 
webSettings.setAllowContentAccess(true);

iOS 配置:

WKWebViewConfiguration *conf = [[WKWebViewConfiguration alloc] init];
[conf setValue:@(YES) forKey:@"allowUniversalAccessFromFileURLs"];

所以,我觉的问题解决了,只需要找下资料,看下鸿蒙下 arkweb 对应的配置参数就行了,很遗憾,在翻边了官网的资料、论坛后,得倒的答案是不支持。

image.png

这就有点难办,我们给鸿蒙官方提交了工单,也在鸿蒙“专事专办”群反馈了问题,但鸿蒙一直没有回复消息。如果解决不了,会迫使H5的同学对所有小程序应用进行请求调用方式的修改,还要再进行一个全量应用更新,怕不是淹没在诸位同学的口水中🐶。

紧急补救

在上文中说到一个小插曲,就是,我和H5框架同学掐架,因为,我这里测试出$.ajax的请求可以发送出去,axios的请求发送不出去,结果被H5框架的同学直接教育了一番,大致意思就是客户端有个预请求的技术,很显然,我对这块不了解,我怕被打脸,所以就自己去研究了解。

人类历史上目前最伟大的发明,应该就是 ChatGPT 了,他应该是独立学习最好的老师。借助GPT的知识,原来在浏览器中的 Ajax 请求分为复杂请求简单请求类型,简单理解,这两类请求的头信息不一样。

简单请求的特性说明:

HTTP方法:仅限于 `GET`、`POST` 和 `HEAD`。
Accept:指定客户端能够处理的内容类型。例如,`text/html`、`application/json`等。
Accept-Language
Content-Language
Content-Type(仅限于 `application/x-www-form-urlencoded`、`multipart/form-data` 或 `text/plain`)
不触发预检请求:浏览器不会先发送一个 `OPTIONS` 请求进行预检

复杂请求的特性说明:

1、HTTP方法:使用了 `PUT`、`DELETE`、`CONNECT`、`OPTIONS`、`TRACE`、`PATCH` 等方法,
或是 `GET`、`POST` 和 `HEAD` 方法中包含了非简单请求头。

2、请求头:包含了除了简单请求头之外的其他头信息,或是 `Content-Type` 为非简单类型(如
`application/json`)。

3、触发预检请求:浏览器会先发送一个 `OPTIONS` 请求进行预检,以确定服务器是否允许实际请求。

$.ajax就是基础的请求头信息,所以不会发送预请求,经过测试,在调用 $.ajax时,加入自定义的 headers请求头信息后,效果就和 axios一致了,发生跨域问题。在我们的业务请求请求头中,包含了 Authorizationclientid等字段信息,所以肯定会被认为是复杂网络请求,这类请求会发起 OPTIONS 的预请求,OPTIONS请求的响应结果不正确,就会报请求错误,导致后续的POST请求无法发起。

这时候,有同学就会问了,你的意思是客户端对复杂网络请求会发两次请求,一次OPTIONS,一次才是正确的POST,那这样不是会影响性能码?没错,客户端会发起两次请求,对性能的影响忽略不计,因为 OPTIONS 预请求只包含了最基础的头信息和和接口地址,不包含 POST 数据体,所以服务端并不会做过多的逻辑运算,一般服务端都会根据既定的配置信息,来响应这个请求能不能调用,并且,复杂请求发起的场景一般在用户操作之后,或者在某个业务节点下执行,并不会频繁调用,所以影响可以忽略不计(如果出现频繁调用的情况,兄弟,是不是你的业务逻辑有点问题🐕)。

回到正题,重新回到应用小程序打不开的接口上,我在 arkweb 提供的请求拦截回调方法中抓取了发送请求的地址和请求头信息,然后,通过 postman 模拟请求对服务端进行发送,因为postman并不会像浏览器那样发送复杂请求,所以我得自己先模拟 OPTIONS 请求发送,结果发现我们的服务端返回 400,原因是 Origin:null,如果不带这个Origin:nullOPTIONS请求就是200。

因为之前承诺给对方尽快解决,满足演示需求,所以我们在测试服务器上临时允许了 Origin:null 请求头通过,允许OPTIONS请求通过的方式有两种:容器侧修改该程序侧修改

OPTIONS请求会先达到容器侧,所以可以通过 nginx、或者 tomcat 的配置直接返回响应,无需修改该代码。

nginx 配置

server {
    listen 80;
    location / {
        # 配置实际的处理逻辑
        ...
        # 添加 CORS 头部支持
        if ($request_method = 'OPTIONS') {
            # 这一步很重要!!!浏览器会根据这个配置来决策这个请求是否跨域
            add_header 'Access-Control-Allow-Origin' 'null';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            # 如果需要支持其他 HTTP 方法,可以在这里添加
            # add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
            # 如果需要支持 NULL 选项,可以添加一个特殊的头部
            # add_header 'Access-Control-Allow-Headers' '*';
            # 返回204状态码代表成功处理了CORS预检请求
            return 204;
        }
    }
}

tomcat 配置

在你的应用程序的 WEB-INF/web.xml 文件中添加 CORS 过滤器配置:

<filter>
    <filter-name>CorsFilter</filter-name>
    <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
    <init-param>
        <param-name>cors.allowed.origins</param-name>
        <param-value>*</param-value>
    </init-param>
    <init-param>
        <param-name>cors.allowed.methods</param-name>
        <param-value>GET,POST,HEAD,OPTIONS,PUT,DELETE</param-value>
    </init-param>
    <init-param>
        <param-name>cors.allowed.headers</param-name>
        <param-value>Content-Type,Authorization</param-value>
    </init-param>
    <init-param>
        <param-name>cors.exposed.headers</param-name>
        <param-value>Content-Type,Authorization</param-value>
    </init-param>
    <init-param>
        <param-name>cors.support.credentials</param-name>
        <param-value>true</param-value>
    </init-param>
    <init-param>
        <param-name>cors.preflight.maxage</param-name>
        <param-value>1800</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>CorsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

程序侧修改直接改程序代码,处理所有的 OPTIONS请求,并统一响应返回,这里给一段 java 示例:

你还可以编写一个自定义的过滤器来处理 CORS 请求,并在 web.xml 中配置该过滤器:

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CustomCorsFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletResponse res = (HttpServletResponse) response;
        res.setHeader("Access-Control-Allow-Origin", "*");
        res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
        res.setHeader("Access-Control-Expose-Headers", "Content-Type, Authorization");
        res.setHeader("Access-Control-Allow-Credentials", "true");
        res.setHeader("Access-Control-Max-Age", "1800");

        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {}
}

web.xml 中配置自定义过滤器:

<filter>
    <filter-name>CustomCorsFilter</filter-name>
    <filter-class>com.example.CustomCorsFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>CustomCorsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

我们选择了容器侧修改,配置完成之后,重启服务,再次用 postman 发送带 Origin:null的请求,结果 200,好了,直接上真机,小程序应用正确渲染,并且数据正确被加载。

到这里,解决了吗?并没有,应该是用问题解决了一个问题,因为 Origin:null不被公司服务端认可,所以,我还是需要找到修改 ArkWeb ajax 请求头的方法,接下来,就开始了很长时间的调研。

ArkWeb

现在,摆在眼前的几条路:

  1. 等待鸿蒙官方解决问题,提供合适的解决方案。(这是一个涉及到系统底层的问题,如果要解决,肯定是系统需要更新,鸿蒙下一次的系统更新,还不知道啥时候呢,而我们的应用在6月下旬就要上线推广,根本的等不起,结尾有彩蛋。)
  2. 调研 ArkWeb 拦截请求,并自定义修改请求头的办法。
  3. 拦截 ArkWeb 请求,并自行发起请求。(这是最终的解决方案,但实际上是第2条路走不通,才走的这条路。)

很明显,第二条路改动最小,比较适合我们公司的场景,因此,开始了第二条路的研究。在 ArkWeb 组件下,支持下列拦截的方法:

Web({ ... })
.onInterceptRequest((event) : WebResourceResponse|null =>{
  let request:WebResourceRequest =  event?.request;
  return null

onInterceptRequest 是一个拦截 html 页面请求的方法,效果和 Android 的 shouldInterceptRequest 比较像,如果返回 null,系统会找按照原来的请求走,如果返回一个自定义的 WebResourceResponse,系统会返回我们自定义提供的响应数据。好,那剩下就是把这里的请求中 Origin:null 的数据修改 Origin:file://,就好了。

通过,打印 request.headers 信息,确实可以找到的 Origin:null 的数据:

image.png

我们通过下面的代码,尝试修改请求头信息

.onInterceptRequest((event) : WebResourceResponse|null =>{
  let headers = event?.request.getRequestHeader();
  for (let headerKV of headers!) {
    if (headerKV.headerKey == "Origin" && headerKV.headerValue == "null") {
      headerKV.headerValue = "file://";
    }
  }
  console.log(this.TAG, JSON.stringify(headers));
  console.log(this.TAG, JSON.stringify(event?.request.getRequestHeader()));
  return null
})

理论上,如果 getRequestHeader 方法返回的是一个内存变量,实际上是可以解决的,可结果发现,headers 中的值修改了,但 getRequestHeader 返回的仍旧是原来的数据,很显然 getRequestHeader底层并不是一个内存数值,而是通过某种持久化数据组装的对象,headers是当前作用域的变量,🤡竟是我自己。

image.png

好吧,那只能走第三条路了,代理请求,自行转发,在 ArkTS 层面,这个也很容易做到,工作量也不是很大,同样是在 onInterceptRequest 方法中,我们通过系统网络请求转发拦截的 Origin:null 的请求,可当我在写 Post请求的时候,我发现了问题,WebResourceRequest 这个对象,他喵的,居然没有提供获取 httpBody 的数据。

image.png

哎,我也是第一时间,在“专事专办”群反馈了问题,截止到写这篇文章,鸿蒙没给出解决方案,看来这条路又走失败了。

继续调研

一切似乎又回到了原点,花了这么长时间的调研,最终没有结果,有点不甘心,于是我继续翻阅鸿蒙官网的资料,企图找到解决思路。

在漫长的阅读中,我看到了自己最不愿意看到的部分,因为,鸿蒙中大部分 API 除了提供 ArkTS 的API,同时也会提供 C/C++ 的 API。

image.png

在官网中,我看到了 C/C++ 拦截 arkweb 请求的部分,继续全局搜这个头文件,我看到了鸿蒙提供的一些示例代码:

image.png

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/web-scheme-handler-V5

在这篇文档中,我看到了获取 httpbody 的方法,鸿蒙官方也提供完整请求示例。目前,还有个问题,这个 API 是基于 API12 的 SDK,我们目前所有的研发都是基于 API11 的 SDK ,如果要使用 API12,我们所有同学的测试设备都得升级 API12,所有的 IDE 都得升级到支持 API12,这里面的风险不可估量,最关键, C++ 是真不懂🐶。

最终,经过团队沟通,权衡利弊之下,撸起袖子,干!,我决定先独自升级踩坑。

坚持

在和鸿蒙的同学沟通后,我们升级了一台 API12 的真机设备,同时,我将 IDE、模拟器升级到了最新版本,当我运行现有工程时,APP喜闻乐见的闪退🤦,原因是 import 动态导入方法的逻辑变更,这里大概花了一天时间,解决了问题,并把现有工程正常跑了起来。(API12 的 UI 动画确实比 API11 的精致了许多,肉眼可见)

虽然要决定用 C++ 来解决这个问题,但还是一筹莫展,所以决定先看下鸿蒙提供的示例代码,这不看不知道:

image.png

这里的示例代码中,最终响应的数据,是读取本地文件数据返回,我要本地数据干什么🤔...

看来示例代码并么有提供 C++ 发送网络请求的办法,好,现在有了,第一个任务 C++ 如何发送网络请求?在翻阅鸿蒙的官方资料之后,得倒的答案是,C/C++ 不提供网络请求的发送模块🔪,到这里,我的心态是有点崩溃的,因为,有人会天天催你问题解决没,一方面自己还不知道这个问题能不能解决。

在翻阅鸿蒙开发者论坛中,找到了点线索,原来有人也遇到了类似的问题(但是解决我这个跨域的问题),有人提出编译 libcurl.so 库给到工程使用,这确实是一个可行的方案,于是,我开始研究如何编译 so 库文件。

借助 GPT 的力量,我了解了在C++的网络发送开源三方库,libcurlBoost.AsioPocohttplib,在这里,我看到 libcurl 在鸿蒙论坛里也有提到,并且 libcurl 知名度很高,所以在这里就选择了 libcurl。于是进行下一步询问,如何编译 libcurl.so 的库,GPT 给了我一系列的方法,按照 GPT 的指南,我始终编译出的是 arm64-v8a/x86_64 的 a 库文件。我尝试吧 a 库文件放到工程中运行,并不能运行起来。(这个问题我一直没有解决)我同样也尝试把 libcurl 的源码,塞到工程中,仍然运行不起来。

我继续问 GPT,上述 C++ 的这些三方库中,可移植性较好的是哪个,我得倒的答案是 httplib,我打开的 httplib 的 github,发现这个库就有一个 .h 文件,直接可以塞到工程中运行,这可把我开心坏了,果然塞到工程中,项目运行起来了,我尝试发了个请求,果然可以正常发送。

image.png

https://github.com/yhirose/cpp-httplib

到这里,我尝试了下 https 能不能发送,果然出问题,https 的发送直接闪退,继续问 GPT,原来 httplib 需要引入 openssl3.0 的库,配合宏定义CPPHTTPLIB_OPENSSL_SUPPORT才能支持。我想继续询问 openssl3.0 如何编译,GPT 给了我一系列教程,这次我按照流程正常编译出了 arm64,x86_64的 libcrypto.so,ssl.so文件。

我第一时间把它们放到了工程中运行,并在工程的 CMakeLists.txt 中引入了这个 so 库文件,可惜,一运行项目,直接闪退。心态又一次炸裂,不过这一次,我冷静分析了下,运行不起来是有原因的,继续阅读鸿蒙官网的零散细碎,以及论坛的资料后,我得倒了答案。

原来使用系统自带 CMake 编译出来的程序,仅支持对 mac 程序使用,不能被鸿蒙工程使用。同时 鸿蒙提供了 NDK版本的 CMake 编译程序,辅助开发者编译自己的C++库:

在这篇文章的指引下,我理解了编译的原理

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/build-with-ndk-cmake-0000001774120786

# 大概在这个目录中
/Users/${YOUR NAME}/Library/Huawei/Sdk/HarmonyOS-NEXT-DB1/base/native/build-tools/cmake/bin/cmake

image.png

同时还有个一个问题,openssl 不支持 cmake 的编译模式,仅支持 ./Configure 编译,而鸿蒙的ndk仅支持 CMake 的编译形式,所以我们需要让 openssl 支持 CMake 编译,在 gitee 上我找到了这块内容。

https://gitcode.com/janbar/openssl-cmake/overview

这位老哥,给了一个现成修改完配置的 openssl cmake 的编译工程,但可惜时 1.1.1 版本,根据老歌的使用指南,结合鸿蒙 NDK 的 CMake 编译形式,我成功的编译出 libcrypto.so 和 libssl.so。然后,将库放到工程中,编辑好库引用配置,然后执行,这一次,成功了,没有闪退!。

image.png

接下来,我根据 httplib 的配置,将上 openssl 宏定义配置,然后运行报错,问题有两个。

1、httplib 需要 openssl3.0 以上,解决很简单,直接注释掉报错的代码就可以。

image.png

2、openssl 不支持 SSL_get1_peer_certificate 方法,直接修改 SSL_get_peer_certificate 就可以。

image.png

修改完成后,再次运行,可以正常执行,开始测试 https 请求,仍然无法解析,但是不会闪退,断点发现在 httplib ssl 证书校验过程中发生失败,导致没有数据返回,这可能和 httplib 需要 openssl3.0 有关,但是 openssl3.0 目前我还不会编译,所以我没有解决这个问题,还是用了一个参数关闭 ssl 校验,如下:

image.png

再次执行发送 https 请求,正确获取到了结果。很好,到目前为止,我所需要的东西都有了,剩下的就是将这些内容串联起来,就完成了。

串联流程

那我要做的事情,整理如下:

  1. 拦截 arkweb 请求头中 origin:null 的请求;
  2. 转发这类请求,支持 http、https、options;
  3. post 请求支持数据获取,文件上传;
  4. 响应结果返回给客户端;

拦截

通过 arkweb 提供的 api,我们开始拦截请求,通过下列方法。

void setSchemetHandlerByCustom(char *scheme) {
    OH_ArkWeb_SetSchemeHandler(scheme, "ec-scheme-handler", g_schemeHandler);
    OH_ArkWebServiceWorker_SetSchemeHandler(scheme, g_schemeHandler);
}

// 设置SchemeHandler。
static napi_value InitWebSchemeHandler(napi_env env, napi_callback_info info) {
    OH_ArkWeb_CreateSchemeHandler(&g_schemeHandler);
    OH_ArkWebSchemeHandler_SetOnRequestStart(g_schemeHandler, OnURLRequestStart);
    
    setSchemetHandlerByCustom("http");
    setSchemetHandlerByCustom("https");
    setSchemetHandlerByCustom("options");

    return nullptr;
}

在 arkweb 引擎初始化的时候,执行拦截器的初始化。

WebView.WebviewController.initializeWebEngine()
InitWebSchemeHandler()
WebView.WebviewController.setWebDebuggingAccess(true)

在拦截方法中过滤出 origin:null 的请求,并取消网络发送。

// 请求开始的回调,在该函数中我们创建一个RawfileRequest来实现对Web内核请求的拦截。
void OnURLRequestStart(const ArkWeb_SchemeHandler *schemeHandler, ArkWeb_ResourceRequest *resourceRequest,
                       const ArkWeb_ResourceHandler *resourceHandler, bool *intercept) {

    ArkWeb_RequestHeaderList *requestHeaderList;
    OH_ArkWebResourceRequest_GetRequestHeaders(resourceRequest, &requestHeaderList);
    size_t size = OH_ArkWebRequestHeaderList_GetSize(requestHeaderList);
    
    bool flag = false;
    httplib::Headers headers;
    // 为了避免转发请求的时候,再次进行 for 循环,所以在这里提前采集请求头信息
    for (int i = 0; i<size; i++) {
        char *key;
        char *value;
        OH_ArkWebRequestHeaderList_GetHeader(requestHeaderList, i, &key, &value);
        if ((*key == *"Origin") && (*value == *"null")) {
            flag = true;
        }
        // 插入临时变量
        headers.insert(std::make_pair(key, value));
        
        OH_ArkWeb_ReleaseString(key);
        OH_ArkWeb_ReleaseString(value);
    }

    // 设置是否拦截该资源请求
    *intercept = flag;
    if (flag) {
        // 如果开始拦截,则自己发送网络请求
        StartRequest(resourceRequest, headers, resourceHandler);
    }
    OH_ArkWebRequestHeaderList_Destroy(requestHeaderList);
}

拦截的部分就完成了。

发送请求

因为,拦截器设置的是全局指针,但在请求发送的时候,要考虑很多的变量仅仅是跟当前请求对象绑定,所以在这里我定义了个 RawRequest 的结构,用来存储和其相关的数据,例如这个请求的地址,方法,buffer请求体数据,响应句柄等。

void initRequest(ArkWeb_ResourceRequest *resourceRequest, httplib::Headers headers, RawRequest &result) {
    char *url;
    OH_ArkWebResourceRequest_GetUrl(resourceRequest, &url);
    char *method;
    OH_ArkWebResourceRequest_GetMethod(resourceRequest, &method);

    result.resourceRequest = resourceRequest;
    result.url = const_cast<char*>(url);
    result.method = const_cast<char*>(method);
    result.headers = headers;
}

void StartRequest(ArkWeb_ResourceRequest *resourceRequest, httplib::Headers headers, const ArkWeb_ResourceHandler *resourceHandler) { 
    RawRequest rawReq;
    initRequest(resourceRequest, headers, rawReq);
    rawReq.resourceHandler = resourceHandler;
    rawReq.sendRequest();
}

在发送请求的时候需要做个判断,请求可能有请求体,也可能没有,如果没有请求体的话,请直接发送,如果有的话,我们需要把请求体数据捞出来之后,再准备发送。

void sendRequest() {
    ArkWeb_HttpBodyStream *httpBodyStream;
    OH_ArkWebResourceRequest_GetHttpBodyStream(this->resourceRequest, &httpBodyStream);
    this->arkWeb_HttpBodyStream = httpBodyStream;
    if (httpBodyStream) {
        add_to_multimap(httpBodyStream, *this);
        OH_ArkWebHttpBodyStream_SetReadCallback(httpBodyStream, ReadCallback);
        OH_ArkWebHttpBodyStream_Init(httpBodyStream, InitCallback);
    } else {
        // 如果没有数据体,直接发送
        uint8_t buf[0];
        _send(buf, 0);
    }
}

发送请求会根据不同的方法类型发送请求。

  void _send(uint8_t *bodyBuffer,  uint64_t buffSize) {
        // 解析当前 url 地址
        Url url_parser;
        parse_url(url.c_str(), url_parser);
        httplib::Client cli(url_parser.getHost().c_str());
        // 关闭 ssl 校验
        // TODO:默认关闭 SSL 忽略,httplib 本身需要 openSSL 3.0 的版本,但我使用的是 1.1.0 版本,可能在 ssl解析层面存在问题,
        // TODO:为什么使用 openSSL 1.1.0 而不是3.0? 原因是:基于鸿蒙 ndk 编译只有 1.1.0 成功,3.0 的编译需要后续学习才能继续推进;
        cli.enable_server_certificate_verification(false);
        this->cli = &cli;
        // 设置请求头 headers
        headers.erase("Origin");
//         headers.insert(std::make_pair("Origin", "file://"));
        cli.set_default_headers(headers);
        // 将 method 类型转换小写进行匹配
        std::string methodStr(method);
        std::transform(methodStr.begin(), methodStr.end(), methodStr.begin(), ::tolower);
        auto dest_path = url_parser.getPath().c_str();
        LibEcPrint("method %{public}s", methodStr.c_str());
        // 获取请求类型
        httplib::Result resp;
        if (methodStr == "post") {
            if (bodyBuffer && buffSize > 0) {
                auto contentType = headers.find("content-type");
                // 设置请求体数据
                // 将 uint8_t 数据转换为 std::string
                char *char_ptr = reinterpret_cast<char *>(bodyBuffer);
                std::string data_str(char_ptr);
                resp = cli.Post(dest_path, char_ptr, buffSize, contentType->second);
            } else {
                resp = cli.Post(dest_path);
            }
        } else if (methodStr == "get") {
            resp = cli.Get(dest_path);
        } else if (methodStr == "options") {
            resp = cli.Options(dest_path);
        } else {
            // TODO:该接口类型不支持发送
            this->responseClientError("不支持的接口发送类型");
            return;
        }

        if (resp) {
            LibEcPrint("开始响应", "");
            LibEcPrint("body %{public}s", resp->body.c_str());
            this->responseHandler(resp);
        } else {
            LibEcPrint("没有数据响应", "");
            this->responseClientError("请检查服务器是否正常,或者 https 证书是否正常");
        }
    }
    

解析请求体数据发送

我们从 httpBodyStream 中获取到请求的 buffer,然后通过 httplib 发送请求。同时,因为httpBodyStream获取的作用域不一致,所以我们去需要借助全局的 map 管理,来跨作用域共享 Rawrequest 对象,存储的方式可以根据的 httpBodyStream 作为key就可以。

// HttpBodyStream的读回调。
void ReadCallback(const ArkWeb_HttpBodyStream *httpBodyStream, uint8_t *buffer, int bytesRead) {
    if (global_multimap.count(httpBodyStream) > 0) {
        auto rawfileRequest = global_multimap.find(httpBodyStream)->second;
        bool isEof = OH_ArkWebHttpBodyStream_IsEof(httpBodyStream);
        if (isEof) {
            LibEcPrint("bufferSize %{public}d", bytesRead);
            rawfileRequest._send(buffer, bytesRead);
        } else {
            // 如果没有请求发生错误
            LibEcPrint("数据长度不对", "");
        }
    } else {
        // 如果没有请求发生错误
        LibEcPrint("请求发生错误,该请求,在全局 map 中没有找到", "");
    }
}

// ArkWeb_HttpBodyStream的初始化回调。
void InitCallback(const ArkWeb_HttpBodyStream *httpBodyStream, ArkWeb_NetError result) {
    if (global_multimap.count(httpBodyStream) > 0) {
        uint64_t bufferSize = OH_ArkWebHttpBodyStream_GetSize(httpBodyStream);
        auto rawfileRequest = global_multimap.find(httpBodyStream)->second;
        // 一把读取完整数据
        LibEcPrint("bufferSize %{public}d", bufferSize);
        rawfileRequest.bufferSize = bufferSize;
        rawfileRequest.buffer = new uint8_t[bufferSize];
        memset(rawfileRequest.buffer, 0, bufferSize);
        OH_ArkWebHttpBodyStream_Read(httpBodyStream, rawfileRequest.buffer, bufferSize);
    } else {
        // 如果没有请求发生错误
        LibEcPrint("请求发生错误,该请求,在全局 map 中没有找到", "");
    }
}

响应数据

获取到结果后,我们解析对应的响应数据,通过对应的响应的句柄返回给前端数据即可。

void responseHandler(httplib::Result &resp) {
    // 进行数据的响应
    ArkWeb_Response *response;
    OH_ArkWeb_CreateResponse(&response);
    // 设置响应字符集
    if (resp->headers.count("content-type") > 0) {
        auto contentTypeStr = resp->headers.find("content-type")->second;
        if (contentTypeStr.find("charset=") != std::string::npos) {
            std::string token;
            auto splits = this->splitString(contentTypeStr.c_str(), "charset=");
            auto charsetstr = splits.at(1);
            std::transform(charsetstr.begin(), charsetstr.end(), charsetstr.begin(), ::toupper);
            OH_ArkWebResponse_SetCharset(response, charsetstr.c_str());
        } else {
            // 设置响应体的编码格式。
            OH_ArkWebResponse_SetCharset(response, "UTF-8");
        }
    }
    // 设置HTTP状态码为200。
    LibEcPrint("response status %{public}d", resp->status);
    OH_ArkWebResponse_SetStatus(response, resp->status);
    // 设置响应头
    for (const auto &pair : resp->headers) {
        LibEcPrint("response headers key : %{public}s  value : %{public}s", pair.first.c_str(), pair.second.c_str());
        OH_ArkWebResponse_SetHeaderByName(response, pair.first.c_str(), pair.second.c_str(), true);
    }
    // 手动允许跨域返回
    OH_ArkWebResponse_SetHeaderByName(response, "Access-Control-Allow-Origin", "*", true);

    // 将为被拦截的请求创建的响应头传递给Web组件。
    OH_ArkWebResourceHandler_DidReceiveResponse(this->resourceHandler, response);
    // 该函数可以调用多次,数据可以分多份来传递给Web组件。
    // 方法 1: 使用固定大小的 uint8_t 数组
    size_t length = resp->body.size();
    uint8_t respBuffer[length];
    // 复制字符串到 uint8_t 数组
    for (size_t i = 0; i < length; ++i) {
        respBuffer[i] = static_cast<uint8_t>(resp->body[i]);
    }
    LibEcPrint("response body value : %{public}s", resp->body.c_str());
    OH_ArkWebResourceHandler_DidReceiveData(this->resourceHandler, respBuffer, length);
    int category = resp->status / 100; // 获取状态码的百位数字
    /**
     *    1xx:信息性响应
     *    2xx:成功响应
      *   3xx:重定向
      *   4xx:客户端错误
      *   5xx:服务器错误
     * */
    // 读取响应体结束,当然如果希望该请求失败的话也可以通过调用; 传递给Web组件一个错误码并结束该请求。
    OH_ArkWebResourceHandler_DidFinish(this->resourceHandler);
    // 清除内存
    this->cleanRequest();
}

最后还要记得清理内存数据。

void cleanRequest() {
    LibEcPrint("before clean %{public}d", global_multimap.size());
    clean_from_multimap(this->arkWeb_HttpBodyStream);
    LibEcPrint("after clean %{public}d", global_multimap.size());
}

好了,这样我们就完成了代理转发,最后运行,我们得倒结果并返回给了前端。解决到这里,真是长舒一口气,

总结

虽然本次问题算勉强解决,但还遗留一些问题,例如 httplib 不支持 ssl 解析,目前是不做证书校验,这可能需要后续在学习 openssl3.0 的编译方式才可能解决。

另外,这种处理方式破坏了原来 web 容器的请求发送结构,可能需要自己承担网络请求中间发送的一些问题,例如:资源并发,线程等问题,也希望鸿蒙官方后续能够尽快提供解决方案。