起因说明:由于在开发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://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());
    }
  }
}