背景
在很多公司测试环境使用的是内网测试,我们公司也是。
但是我们有点扯的是内网的域名没有配置内网域名解析,必须手动配置hosts才可以正常访问测试环境的域名。
如下:
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
# 公司内部域名
10.11.x.x http://www.xxxx.com
所以在测试环境时,操作步骤一般是:在电脑上配置hosts后,然后手机链接电脑上的代理,在进行测试。
在我们之前没有使用Flutter技术的时候,手机链接电脑上的代理,啥问题都没有。电脑上装一个charles
然后在手机上链接电脑的IP
但是我们使用了Flutter技术后,发现这一套不好使了,下面进入今天的主题,分析一下这样做为什么抓不到包的原因以及解决方法。
原因分析
我们知道要科学上网访问国外的一些资源需要设置代理服务器,那么下面 以Android的Java为例,讲解代理.
Java代理
Java 中代理主要的两个类Proxy和ProxySelector,先看一下 Proxy
public class Proxy {
/**
* Represents the proxy type.
*
* @since 1.5
*/
public enum Type {
/**
* Represents a direct connection, or the absence of a proxy.
*/
DIRECT,
/**
* Represents proxy for high level protocols such as HTTP or FTP.
*/
HTTP,
/**
* Represents a SOCKS (V4 or V5) proxy.
*/
SOCKS
};
private Type type;
private SocketAddress sa;
/**
* A proxy setting that represents a {@code DIRECT} connection,
* basically telling the protocol handler not to use any proxying.
* Used, for instance, to create sockets bypassing any other global
* proxy settings (like SOCKS):
* <P>
* {@code Socket s = new Socket(Proxy.NO_PROXY);}
*
*/
public final static Proxy NO_PROXY = new Proxy();
// Creates the proxy that represents a {@code DIRECT} connection.
private Proxy() {
type = Type.DIRECT;
sa = null;
}
......
看代码我们可以知道,代理一般分为DIRECT(也被称为没有代理 NO_PROXY),HTTP代理(高级协议代理,HTTP、FTP等的代理),SOCKS 代理.
这个类怎么用呢?以获取百度网页为例,设置URLConnection的代理
private final String PROXY_ADDR = "xxx.xxx.xxx.xxx";
private final int PROXY_PORT = 10086;
public void readHtml() throws IOException {
URL url = new URL("http://www.baidu.com");
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(PROXY_ADDR, PROXY_PORT));
//链接时,使用代理链接
URLConnection conn = url.openConnection(proxy);
conn.setConnectTimeout(3000);
InputStream inputStream = conn.getInputStream();
//获得输入流,开始读入....
}
目前大多数Java都使用OkHttp作为网络请求访问,OkHttp的代理设置更为简单
public void readHtml() {
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(PROXY_ADDR, PROXY_PORT));
OkHttpClient okHttpClient = new OkHttpClient.Builder().proxy(proxy).build();
}
我们的应用就是在release环境下,禁用了抓包(包含http不需要证书的抓包),设置如下:
public void init() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
if(!BuildConfig.DEBUG) {
builder.proxy(Proxy.NO_PROXY);
}
OkHttpClient okHttpClient = builder.build();
}
追看一下OkHttp源码,可以知道,http的链接最终调用到了RealConnection. 链接最终调用到了connectSocket
/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
private void connectSocket(int connectTimeout, int readTimeout, Call call,
EventListener eventListener) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
看这个函数Okhttp的注释,可以知道,Okhttp是基于socket去实现的一套完整的http协议,无论是那种代理方式,都返回的是一个socket链接对象,我们看一下new Socket的实现
public Socket(Proxy proxy) {
....
Proxy p = proxy == Proxy.NO_PROXY ? Proxy.NO_PROXY
: sun.net.ApplicationProxy.create(proxy);
Proxy.Type type = p.type();
// if (type == Proxy.Type.SOCKS || type == Proxy.Type.HTTP) {
if (type == Proxy.Type.SOCKS) {
impl = new SocksSocketImpl(p);
impl.setSocket(this);
} else {
if (p == Proxy.NO_PROXY) {
if (factory == null) {
impl = new PlainSocketImpl();
impl.setSocket(this);
} else
setImpl();
} else
throw new IllegalArgumentException("Invalid Proxy");
}
}
如果有代理,最终调用到了SocksSocketImpl§
SocksSocketImpl(Proxy proxy) {
SocketAddress a = proxy.address();
if (a instanceof InetSocketAddress) {
InetSocketAddress ad = (InetSocketAddress) a;
// Use getHostString() to avoid reverse lookups
server = ad.getHostString();
serverPort = ad.getPort();
}
}
通过代理的host和端口去链接服务. 那发现了其中原理,那么他是怎么读取系统设置的代理呢?下面来看一下ProxySelector
ProxySelector
直接显示传入Porxy对象的方法未免有点太繁琐,并且无法直接读取系统所设置的代理,Java提供了一个抽象类ProxySelector,该类的对象可以根据你要连接的URL自动选择最合适的代理,但是该类是抽象类。看一下:
public abstract class ProxySelector {
private static ProxySelector theProxySelector;
.......
public static void setDefault(ProxySelector ps) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(SecurityConstants.SET_PROXYSELECTOR_PERMISSION);
}
theProxySelector = ps;
}
public abstract List<Proxy> select(URI uri);
public abstract void connectFailed(URI uri, SocketAddress sa, IOException ioe);
}
实现一个自己的代理选择器
public void init() {
ProxySelector.setDefault(new ProxySelector() {
@Override
public List<Proxy> select(URI uri) {
List<Proxy> list = new ArrayList<>();
list.add(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(PROXY_ADDR, PROXY_PORT)));
return list;
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
//链接失败
}
});
}
OkHttp初始化的时候,可以直接设置,也可以获取默认的
public Builder proxySelector(ProxySelector proxySelector) {
this.proxySelector = proxySelector;
return this;
}
public Builder() {
·····
proxySelector = ProxySelector.getDefault();
·····
}
自动选择我们设置的代理,那么还是有问题,怎么读取手机上设置的代理呢,看一下ProxySelector我上面省略的代码,
public abstract class ProxySelector {
private static ProxySelector theProxySelector;
static {
try {
Class<?> c = Class.forName("sun.net.spi.DefaultProxySelector");
if (c != null && ProxySelector.class.isAssignableFrom(c)) {
theProxySelector = (ProxySelector) c.newInstance();
}
} catch (Exception e) {
theProxySelector = null;
}
}
...
}
而sun.net.spi.DefaultProxySelector这个实现会自己去读取系统的代理设置,这样就可以实现背景里提到的自动抓包了.
Dart 中Http的实现
上面Java可以自动去读取系统设置的代理,那么Dart的网络实现部分,难道没有类似的实现吗?不可以直接读取到系统设置的代理吗?查看Dart代码,也有类似的实现,如下
追看_findProxyFromEnvironment方法,进入到http_impl.dart中发现
static Map<String, String> _platformEnvironmentCache = Platform.environment;
但是就卡在这了,找不到Platform.environment的实现,那么Android/IOS 就读不到系统的代理设置,就不能像原生那样正常抓包了.
如果有读者找到这个对应的实现Platform.environment的网络代理部分,可以麻烦告诉我一下.
Flutter 之解决方法
为了不被产品经理,QA怼,为啥你这个就不行了呢?自己上吧,手动写一个代理设置器,让Flutter也可以正常抓包. 先看一下实现效果
实现
UI部分很简单,就不在写了,看一下网络部分怎么写的,我们网络直接使用了官方的Http,未直接使用DIO,因为那会DIO,还没出来呢.
第一步自己定义一个AppClinet,非关键部分
class AppClient extends http.BaseClient {
// ignore: non_constant_identifier_names
static final int TIME_OUT = 15;
http.Client _innerClient;
AppClient(this._innerClient);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
try {
request.headers.addAll(getHeaders());
Request httpRequest = request;
AppUtils.log(httpRequest.headers.toString());
//AppUtils.log(httpRequest.bodyFields.toString());
return await _innerClient.send(request);
} catch (e) {
print('http error =' + e.toString());
return Future.value(new http.StreamedResponse(new Stream.empty(), 400));
}
}
....
}
第二步,创建一个可以设置代理的HttpClient
void lazyInitClient(bool isForce) {
if (isForce || _appClient == null) {
HttpClient httpClient = new HttpClient();
if (AppConstants.DEVELOP) {
//environment: {'http_proxy': '192.168.11.64:8888'}
AppUtils.getProxy().then((str) {
Map<String, String> map = {};
print("str=" + str);
httpClient.findProxy = (url) {
String proxy = '';
if (str.isNotEmpty) {
map.addAll({'http_proxy': str, 'https_proxy': str});
print(map.toString());
proxy =
HttpClient.findProxyFromEnvironment(url, environment: map);
} else {
proxy = HttpClient.findProxyFromEnvironment(url);
}
print("proxy=" + proxy);
return proxy;
};
});
}
http.Client client = new IOClient(httpClient);
_appClient = new AppClient(client);
}
}
其中AppUtils为存储里面直接获取,避免每次重启都需要设置
//AppUtils
static Future<String> getProxy() async {
return await SharedPreferences.getInstance().then((prefs) {
String proxy = prefs.getString("proxy");
return proxy == null ? "" : proxy;
});
}
这样就完成了,Flutter中Dart的网络请求也可以抓包了。
其实那样是有缺点的,需要开发界面,手动设置。不需要开发界面,去自己实现一套Platform.environment Android/IOS 分别获取系统的代理,通过MethodChannel设置到网络请求中即可 。
总结
本文主要回顾了一下网络中代理的技术实现,并应用到跨平台中,这也是之前的一些踩坑实践分享出来,如果你觉得对你有帮助,欢迎点赞,谢谢.
推荐阅读作者的其他文章
Android:让你的“女神”逆袭,代码撸彩妆(画妆)
Flutter PIP(画中画)效果的实现
Android 绘制原理浅析【干货】
转载:https://blog.csdn.net/dingdegao/article/details/102463328