• 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 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());
        }
      }
    }
  • 相关阅读:
    HDU 3081 Marriage Match II
    HDU 4292 Food
    HDU 4322 Candy
    HDU 4183 Pahom on Water
    POJ 1966 Cable TV Network
    HDU 3605 Escape
    HDU 3338 Kakuro Extension
    HDU 3572 Task Schedule
    HDU 3998 Sequence
    Burning Midnight Oil
  • 原文地址:https://www.cnblogs.com/lizhanqi/p/13633103.html
Copyright © 2020-2023  润新知