flutter web 動態代理請求


  起因說明:由於在開發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());
    }
  }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM