起因说明:由于在开发flutter web 中遇到了跨域问题,网络中多数都是通过Nginx代理之类实现的也有dart shelf_proxy 的,其中原理都是一样的,都是通过请求代理端口,根据配置进行向目标发起请求,如果项目中请求的服务器地址固定的是可以这样用的;
但是因为公司的服务端程序会卖给N个客户同时会部署N个服务器,但是程序我只能做一套只部署到一个服务器上,即一个客户端会根据不同的客户进行服务器数据访问
Android 跟ios 是通过 向公司服务器发起请求 根据客户企业id 查询对应客户服务器地址(这个服务器地址是固定的),然后查询到客户服务器地址后进行客户数据请求处理等操作,
痛点:这样一来Nginx 就没办法用了,因为不知道具体客户的服务是哪个无法进行代理,所以就有了本篇文章和代码
在Android 和ios 中因为没有跨域问题,所以可以以请求人任意服务器,但是web会存在跨域等问题;
原理:该代理是启动服务端口,该服务会允许跨域,然后服务受到请求后,发起的请求头中有目标服务器地址 Target_IP_Port字段是目标地址,然后发起请求即可
使用:把域名替换成启动的代理端口跟ip,然后把真实的请求域名跟端口放入到请求头的:Target_IP_Port中
开发中遇到的的问题1:
Invalid argument (string): Contains invalid characters.: "----------------------------019567785799041077126254
Content-Disposition: form-data; name="app_code"
我问问
----------------------------019567785799041077126254
Content-Disposition: form-data; name="Target_IP_Port"
http://127.0.0.1:3721
----------------------------019567785799041077126254
Content-Disposition: form-data; name="token"
f
----------------------------019567785799041077126254--
" 堆栈信息:
#0 _UnicodeSubsetEncoder.convert (dart:convert/ascii.dart:89:9)
#1 Latin1Codec.encode (dart:convert/latin1.dart:40:46)
#2 _IOSinkImpl.write (dart:_http/http_impl.dart:731:19)
#3 _HttpOutboundMessage.write (dart:_http/http_impl.dart:826:11)
#4 run.<anonymous closure>.<anonymous closure> (file:///F:/FlutterProjects/suxuanapp/server/proxy_http.dart:108:30)
#5 _RootZone.runUnary (dart:async/zone.dart:1450:54)
#6 _FutureListener.handleValue (dart:async/future_impl.dart:143:18)
#7 Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:696:45)
#8 Future._propagateToListeners (dart:async/future_impl.dart:725:32)
#9 Future._completeWithValue (dart:async/future_impl.dart:529:5)
#10 Future._asyncCompleteWithValue.<anonymous closure> (dart:async/future_impl.dart:567:7)
#11 _microtaskLoop (dart:async/schedule_microtask.dart:41:21)
#12 _startMicrotaskLoop (dart:async/schedule_microtask.dart:50:5)
#13 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:118:13)
#14 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:169:5)
这个异常:_UnicodeSubsetEncoder.convert 主要原因是跟踪分析发现是
HttpClientRequest.write()时候产生的;跟踪到源码在http_impl中得知,源码写入时候用的是iso8859-1 ;iso8859-1是单字节字符,遇到含有中文的utf8格式就不能转换。
这里主要是因为在接受客户端时候用utf8接收的,在我们跟踪源码时发现 ,获取iso8859-1的编码器是这样获取的 Encoding.getByName("iso8859-1");
因此我们需要把getBodyContent 获取正文内容utf-8编码器格式修改为iso8859-1的编码器进行接收,然后HttpClientRequest.write()就能正常写入了。
但是这样会导致打印日志中文乱码,我们需要把iso8859-1转换成 utf8格式:utf8.decode(value.codeUnits);到此为止大功告成。
开发中遇到的的问题2: 请求服务器app 没问题但是经过代理后发现请求服务器给返回了File not fount,这是因为当前服务收到的请求头中包含了host, 然后执行代理请求时候也一并提交给了目标服务器,该host 是自己代理服务的ip跟端口,目标服务器肯定解析不到该host,所以返回了file notfount
具体原因参考:https://blog.csdn.net/qq_40328109/article/details/99348148
参考文档:
//预检请求https://www.jianshu.com/p/0ac50bdf42aa
dart httpserver 官方文档
https://dart.dev/tutorials/server/httpserver
主要代码如下:
import 'dart:convert'; import 'dart:io'; import 'dart:convert' as convert; import 'Log.dart'; //预检请求https://www.jianshu.com/p/0ac50bdf42aa //https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS // print('请求方式:'+(request.headers[" Access-Control-Request-Method"] ).toString()); // if (request.method == 'OPTIONS') { //// //允许的源域名 //// //允许的请求类型 ////// request.response.headers.add("Access-Control-Allow-Methods","GET, POST, PUT,DELETE,OPTIONS,PATCH"); //// request.response.headers.add("Access-Control-Allow-Methods", '*'); //// request.response.headers.add("Access-Control-Allow-Credentials", true); //// request.response.headers.add("Access-Control-Allow-Headers", request.headers['access-control-request-headers']); //// request.response.cl ose(); //// return; //// } ; /**dart httpserver 官方文档 * https://dart.dev/tutorials/server/httpserver */ //请求次数 var requestCount = 0; /** * 提交给目标服务器时候需要忽略的请求头参数 如果不忽略,服务器会返回:File not found. * 原因是:host是当前代理主机与端口,是由协议进行自动添加的, 如果这里指定host ,那么真是服务器可能会解析不到就会返回File not found. * 这里不应该自己手动指定,应该有http请求自动执行 * http请求头host字段作用 : * host是HTTP 1.1协议中新增的一个请求头字段,能够很好的解决一个ip地址对应多个域名的问题。* * 当服务器接收到来自浏览器的请求时,会根据请求头中的host字段访问哪个站点。 *举个栗子,我有一台服务器A ip地址为120.79.92.223,有三个分别为www.baidu.com、www.google.com、www.sohu.com的域名解析到了这三个网站上, * 当我们通过http://www.baidu.com这个网址去访问时,DNS解析出的ip为120.79.92.223, * 这时候服务器就是根据请求头中的host字段选择使用www.baidu.com这个域名的网站程序对请求做响应 */ const ignoreHeader = { "host": "127.0.0.1:4040", }; const no_target_ip = 510; //没有目标地址 const proxy_requst_error = 511; //请求代理异常 const proxy_respones_error = 512; //代理响应异常 const proxy_error = 514; //代理相应错误 const Target_IP_Port = "src"; //放入到请求头的目标服务器地址 const src = "src"; //代理相应错误放入到url 上边的参数 /// 转换Unicode 编码 String toUnicode(String args) { var bytes = utf8.encode(args); var urlBase = base64Encode(bytes); return utf8.decode(base64Decode(urlBase)); } main() async { try { run(); } catch (e) { Log.e("代理系统异常", e); print(e); } } /** * 获取服务端地址 */ Uri getServerAddress(HttpRequest request) { if (request.uri.queryParameters.containsKey("src")) { var url= request.uri.queryParameters['src']; var uri = Uri.parse(url); return uri; } var targetIp = request.headers.value(Target_IP_Port).toString(); var uri = Uri.parse(targetIp); //转换成uri注意:这里如果携带端口号,则一定要携带scheme 否则会返回异常 //请求地址拼接修改 var proxyRequestUri = uri.resolve(request.uri.toString()); return proxyRequestUri; } void run() async { var server = await HttpServer.bind(InternetAddress.anyIPv4, 4040); Log.d('代理请求端口', '${server.port} '); server.defaultResponseHeaders.add('Access-Control-Allow-Origin', '*'); //允许跨域 server.defaultResponseHeaders .add("Access-Control-Allow-Methods", '*'); //跨域预检请求时候允许的请求方式 server.defaultResponseHeaders .add("Access-Control-Allow-Headers", "*"); //允许跨域自定义的请求头 server.defaultResponseHeaders.add("Access-Control-Allow-Credentials", true); //如果服务器端的响应中未携带 Access-Control-Allow-Credentials: true ,浏览器将不会把响应内容返回给请求的发送者。 server.defaultResponseHeaders .add("Access-Control-Max-Age", "3600"); //跨域时候预检周期,防止重复性预检 await for (HttpRequest request in server) { requestCount++; var tmpReqTag = "请求id:" + requestCount.toString(); Log.i("☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆", tmpReqTag.toString()); // var errorCode; // var errorReason; // var errormsg ={"code": errorCode,"message":errorReason}; try { if (request.method == "OPTIONS") { Log.d(tmpReqTag, "处理预检请求"); // print("-------------------------预检请求头-------------------------"); // print(request.headers); request.response ..write("预检完成") ..close(); continue; } Log.d(tmpReqTag, request.uri.queryParameters['src']); if (request.headers.value(Target_IP_Port) == null && !request.uri.queryParameters.containsKey("src")) { Log.w(tmpReqTag, "请求头或url中未携带" + Target_IP_Port+"无法代理请求目标服务器) "); request.response ..statusCode = no_target_ip ..write("欢迎使用动态代理服务 错误原因:请求头或url中未携带" + Target_IP_Port+"无法代理请求目标服务器 使用方法: 1.请求头中请增加" + Target_IP_Port +" url参数仍然放到当前路径,体参也直接提交到过来即可(即:请求头中的服务器ip跟端口以及协议,至于参数则用当前请求的) 2.url面增加 src 参数作为目标服务器地址请求的连接,体参直接放入到当前请求即可(推荐:简单易用)") ..close(); continue; } //异步处理 processing(tmpReqTag, request); } catch (e, s) { request.response.statusCode = proxy_error; request.response ..write(e) ..close(); Log.e(tmpReqTag, '发生异常 ${e} 堆栈信息: ${s}.'); continue; } } } /** * 处理请求,通过async 增加异步可以提高请求并发量级 */ void processing(tmpReqTag, request) async { getBodyContent(request).then((String value) { try { pirntRequest(tmpReqTag, request, value); //目标地址IP端口号 var proxyRequestUri = getServerAddress(request); if (proxyRequestUri.scheme == null) { proxyRequestUri.replace(scheme: "http"); } proxyRequst(tmpReqTag, request, proxyRequestUri, value); } catch (e, s) { Log.e(tmpReqTag, '发生异常 ${e} 堆栈信息: ${s}.'); request.response.statusCode = proxy_error; request.response ..write(e) ..close(); } } ); } /** * 执行请求 */ void proxyRequst(String tmpReqTag, final HttpRequest request, Uri proxyRequestUri, String value) { var proxyHttpClient = new HttpClient() ..openUrl(request.method, proxyRequestUri) // Makes a request to the external server.向外部服务器发出请求。 //.then((HttpClientRequest proxyRequest) => proxyRequest.close()) .then((HttpClientRequest proxyRequest) { try { request.headers.forEach((name, values) { if (!ignoreHeader.containsKey(name)) { proxyRequest.headers.add(name, values); } }); Log.d(tmpReqTag, "-----------发送给服务器请求头------------ ${proxyRequest.headers}"); //注意:value 是客户端传过来的,在读取时候一定要用iso8859-1读取,因为write 写入 用的就是iso8859否则中文就异常退出了 proxyRequest.write(value); } catch (e, s) { Log.d(tmpReqTag, '错误详情: $e 堆栈信息: $s'); request.response ..statusCode = proxy_requst_error ..write(e) ..close(); print(e); } return proxyRequest.close(); }).then((HttpClientResponse proxyResponse) { Log.i(tmpReqTag,"------------------响应头-------------------- ${proxyResponse.headers}"); proxyResponse.transform(convert.utf8.decoder).join().then((String value) { Log.i( tmpReqTag, "------------------响应内容--------------------- ${value}"); request.response ..statusCode = proxyResponse.statusCode ..write(value) ..close(); }); }, onError: () { request.response ..statusCode =proxy_respones_error ..close(); Log.i(tmpReqTag, "------------------响应异常---------------------"); }); } /** * 打印请求信息 */ void pirntRequest(tmpReqTag, request, String value) { Log.i(tmpReqTag.toString(), request.method); Log.i(tmpReqTag.toString(), "-----------请求头------------ ${request.headers}"); Log.i(tmpReqTag.toString(), '目标地址:' + getServerAddress(request).toString()); Log.i(tmpReqTag.toString(), "--------请求URL参数----------- "); request.uri.queryParameters.forEach((param, val) { Log.i(tmpReqTag.toString(), param + ':' + val); }); //字符编码转换原来是iso8859-1 现转换成utf-8方便打印日志查看 Log.i(tmpReqTag.toString(), "---------体参数------------- ${utf8.decode(value.codeUnits)}"); } /** * 获取表单的数据,以下代码参考,感谢大神 * http://www.cndartlang.com/844.html * 获取post的内容 */ Future<String> getBodyContent(HttpRequest request) async { /** * Post方法稍微麻烦一点 * 首先,request传送的数据时经过压缩的 * index.html中设置了utf8,因此需要UTF8解码器 * 表单提交的变量和值的字符串结构为:key=value * 如果表单提交了多个数据,用'&'对参数进行连接 * 对于提取变量的值,可以自行对字符串进行分析 * 不过也有取巧的办法: * Uri.queryParameters(String key)能解析'key=value'类型的字符串 * Uri功能很完善,协议、主机、端口、参数都能简单地获取到 * 其中,uri参数是用'?'连接的,例如: * http://www.baidu.com/s?wd=dart&ie=UTF-8 * 因此,为了Uri类能正确解析,需要在表单数据字符串前加'?' */ var encodingName = Encoding.getByName("iso_8859-1"); String strRaw = // await utf8.decoder.bind(request).join("&"); //重点:dart2用的UTF8这里补鞥用需要用这种方式 ,另外这里要用ISO 8859 -1方式获取,要不然HttpClientRequest.write() 写入服务器时候无法转换字符,从而失败 await encodingName.decoder.bind(request).join("&"); // print('-----------------体参数原始数据-------------------------------'); return strRaw; } /** * post 内容转换为KeyValue方便获取 * 这里不能转换form-data 格式 */ stringBody2KV(String strRaw) { //这里原始数据是{"name":"typeText"} 或者accessKey=队长是我&password=4555,下面通过增加? 然后通过uri通过的参数查询进行获取方便获取+ print(strRaw); try { String strUri = "?" + Uri.decodeComponent(strRaw); return Uri.parse(strUri).queryParameters; } catch (e) {} return null; }
日志打印:
class Log{ static bool iPrint=false; static bool dPrint=true; static bool wPrint=true; static bool ePrint=true; static void d(String tag, Object content){ _print("" "Debug",tag,content ); } static void w(String tag, String content){ _print("Warning",tag,content); } static void e(String tag, String content){ _print("Error",tag,content); } static void i(String tag, String content){ _print("Infor",tag,content); } static _print(String level,String tag, Object content ){ if(level=="Debug"&&dPrint){ print(level+":"+tag+":"+content.toString()); }else if(level=="Warning"&&wPrint){ print(level+":"+tag+":"+content.toString()); }else if(level=="Error"&&ePrint) { print(level+":"+tag+":"+content.toString()); } else if(level=="Infor"&&iPrint){ print(level+":"+tag+":"+content.toString()); } } }