我是一名iOS开发者,我是一名非计算机专业出生的程序员。
如果学习很幸苦,那就尝试无知的代价
最近纯血鸿蒙(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 对应的配置参数就行了,很遗憾,在翻边了官网的资料、论坛后,得倒的答案是不支持。
这就有点难办,我们给鸿蒙官方提交了工单,也在鸿蒙“专事专办”群反馈了问题,但鸿蒙一直没有回复消息。如果解决不了,会迫使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一致了,发生跨域问题。在我们的业务请求请求头中,包含了Authorization、clientid等字段信息,所以肯定会被认为是复杂网络请求,这类请求会发起OPTIONS的预请求,OPTIONS请求的响应结果不正确,就会报请求错误,导致后续的POST请求无法发起。
这时候,有同学就会问了,你的意思是客户端对复杂网络请求会发两次请求,一次OPTIONS,一次才是正确的POST,那这样不是会影响性能码?没错,客户端会发起两次请求,对性能的影响忽略不计,因为OPTIONS预请求只包含了最基础的头信息和和接口地址,不包含POST数据体,所以服务端并不会做过多的逻辑运算,一般服务端都会根据既定的配置信息,来响应这个请求能不能调用,并且,复杂请求发起的场景一般在用户操作之后,或者在某个业务节点下执行,并不会频繁调用,所以影响可以忽略不计(如果出现频繁调用的情况,兄弟,是不是你的业务逻辑有点问题🐕)。
回到正题,重新回到应用小程序打不开的接口上,我在 arkweb 提供的请求拦截回调方法中抓取了发送请求的地址和请求头信息,然后,通过postman模拟请求对服务端进行发送,因为postman并不会像浏览器那样发送复杂请求,所以我得自己先模拟OPTIONS请求发送,结果发现我们的服务端返回 400,原因是Origin:null,如果不带这个Origin:null,OPTIONS请求就是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 组件下,支持下列拦截的方法:
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 的数据:
我们通过下面的代码,尝试修改请求头信息
.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是当前作用域的变量,🤡竟是我自己。
好吧,那只能走第三条路了,代理请求,自行转发,在 ArkTS 层面,这个也很容易做到,工作量也不是很大,同样是在onInterceptRequest方法中,我们通过系统网络请求转发拦截的Origin:null的请求,可当我在写Post请求的时候,我发现了问题,WebResourceRequest这个对象,他喵的,居然没有提供获取httpBody的数据。
哎,我也是第一时间,在“专事专办”群反馈了问题,截止到写这篇文章,鸿蒙没给出解决方案,看来这条路又走失败了。
一切似乎又回到了原点,花了这么长时间的调研,最终没有结果,有点不甘心,于是我继续翻阅鸿蒙官网的资料,企图找到解决思路。
在漫长的阅读中,我看到了自己最不愿意看到的部分,因为,鸿蒙中大部分 API 除了提供 ArkTS 的API,同时也会提供 C/C++ 的 API。
在官网中,我看到了 C/C++ 拦截 arkweb 请求的部分,继续全局搜这个头文件,我看到了鸿蒙提供的一些示例代码:
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++ 来解决这个问题,但还是一筹莫展,所以决定先看下鸿蒙提供的示例代码,这不看不知道:
这里的示例代码中,最终响应的数据,是读取本地文件数据返回,我要本地数据干什么🤔...
看来示例代码并么有提供 C++ 发送网络请求的办法,好,现在有了,第一个任务 C++ 如何发送网络请求?在翻阅鸿蒙的官方资料之后,得倒的答案是,C/C++ 不提供网络请求的发送模块🔪,到这里,我的心态是有点崩溃的,因为,有人会天天催你问题解决没,一方面自己还不知道这个问题能不能解决。
在翻阅鸿蒙开发者论坛中,找到了点线索,原来有人也遇到了类似的问题(但是解决我这个跨域的问题),有人提出编译 libcurl.so 库给到工程使用,这确实是一个可行的方案,于是,我开始研究如何编译 so 库文件。
借助 GPT 的力量,我了解了在C++的网络发送开源三方库,libcurl、Boost.Asio、Poco、httplib,在这里,我看到 libcurl 在鸿蒙论坛里也有提到,并且 libcurl 知名度很高,所以在这里就选择了 libcurl。于是进行下一步询问,如何编译 libcurl.so 的库,GPT 给了我一系列的方法,按照 GPT 的指南,我始终编译出的是 arm64-v8a/x86_64 的 a 库文件。我尝试吧 a 库文件放到工程中运行,并不能运行起来。(这个问题我一直没有解决)我同样也尝试把 libcurl 的源码,塞到工程中,仍然运行不起来。
我继续问 GPT,上述 C++ 的这些三方库中,可移植性较好的是哪个,我得倒的答案是 httplib,我打开的 httplib 的 github,发现这个库就有一个 .h 文件,直接可以塞到工程中运行,这可把我开心坏了,果然塞到工程中,项目运行起来了,我尝试发了个请求,果然可以正常发送。
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
同时还有个一个问题,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。然后,将库放到工程中,编辑好库引用配置,然后执行,这一次,成功了,没有闪退!。
接下来,我根据 httplib 的配置,将上 openssl 宏定义配置,然后运行报错,问题有两个。
1、httplib 需要 openssl3.0 以上,解决很简单,直接注释掉报错的代码就可以。
2、openssl 不支持 SSL_get1_peer_certificate 方法,直接修改 SSL_get_peer_certificate 就可以。
修改完成后,再次运行,可以正常执行,开始测试 https 请求,仍然无法解析,但是不会闪退,断点发现在 httplib ssl 证书校验过程中发生失败,导致没有数据返回,这可能和 httplib 需要 openssl3.0 有关,但是 openssl3.0 目前我还不会编译,所以我没有解决这个问题,还是用了一个参数关闭 ssl 校验,如下:
再次执行发送 https 请求,正确获取到了结果。很好,到目前为止,我所需要的东西都有了,剩下的就是将这些内容串联起来,就完成了。
那我要做的事情,整理如下:
通过 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 容器的请求发送结构,可能需要自己承担网络请求中间发送的一些问题,例如:资源并发,线程等问题,也希望鸿蒙官方后续能够尽快提供解决方案。