我是一名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 容器的请求发送结构,可能需要自己承担网络请求中间发送的一些问题,例如:资源并发,线程等问题,也希望鸿蒙官方后续能够尽快提供解决方案。