起因說明:由於在開發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\r\nContent-Disposition: form-data; name=\"app_code\"\r\n\r\n我問問\r\n----------------------------019567785799041077126254\r\nContent-Disposition: form-data; name=\"Target_IP_Port\"\r\n\r\nhttp://127.0.0.1:3721\r\n----------------------------019567785799041077126254\r\nContent-Disposition: form-data; name=\"token\"\r\n\r\nf\r\n----------------------------019567785799041077126254--\r\n" 堆棧信息:
#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("歡迎使用動態代理服務 \n錯誤原因:請求頭或url中未攜帶" + Target_IP_Port+"無法代理請求目標服務器\n使用方法: \n1.請求頭中請增加" + Target_IP_Port +" url參數仍然放到當前路徑,體參也直接提交到過來即可(即:請求頭中的服務器ip跟端口以及協議,至於參數則用當前請求的) \n2.url面增加 src 參數作為目標服務器地址請求的連接,體參直接放入到當前請求即可(推薦:簡單易用)") ..close(); continue; } //異步處理 processing(tmpReqTag, request); } catch (e, s) { request.response.statusCode = proxy_error; request.response ..write(e) ..close(); Log.e(tmpReqTag, '發生異常 ${e} \n 堆棧信息:\n ${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} \n堆棧信息:\n ${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, "-----------發送給服務器請求頭------------\n${proxyRequest.headers}"); //注意:value 是客戶端傳過來的,在讀取時候一定要用iso8859-1讀取,因為write 寫入 用的就是iso8859否則中文就異常退出了 proxyRequest.write(value); } catch (e, s) { Log.d(tmpReqTag, '錯誤詳情:\n $e 堆棧信息:\n $s'); request.response ..statusCode = proxy_requst_error ..write(e) ..close(); print(e); } return proxyRequest.close(); }).then((HttpClientResponse proxyResponse) { Log.i(tmpReqTag,"------------------響應頭--------------------\n${proxyResponse.headers}"); proxyResponse.transform(convert.utf8.decoder).join().then((String value) { Log.i( tmpReqTag, "------------------響應內容---------------------\n${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(), "-----------請求頭------------\n${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(), "---------體參數-------------\n${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()); } } }
