背景
在很多公司測試環境使用的是內網測試,我們公司也是。
但是我們有點扯的是內網的域名沒有配置內網域名解析,必須手動配置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(p)
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 繪製原理淺析【乾貨】