zoukankan      html  css  js  c++  java
  • 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());
        }
      }
    }
  • 相关阅读:
    python 报错 AttributeError: 'Series' object has no attribute 'as_matrix'
    python 报错 NameError: name 'xrange' is not defined
    python报错 AxisError: axis 0 is out of bounds for array of dimension 0
    Python 列表中的浅拷贝与深拷贝
    python列表中查找元素
    Python中两个变量交换
    Python中*和**的使用
    Airtest启动报错:ERROR:gup_process_transport_factory.cc<1019>] Lost UI share context
    adb的使用
    Python函数
  • 原文地址:https://www.cnblogs.com/lizhanqi/p/13633103.html
Copyright © 2011-2022 走看看