为什么我们要采用Okhttp作为网络请求框架
在java 和Android中我们通常采用HttpClient和Httpurlconnection来实现网络请求
现在曾经火爆的网络框架AsyncHttp和Volley (均基于HttpClient和Httpurlconnection)已然淡出我们的视野
Okhttp直接使用Scoket遵循网络协议实现的网络框,这是和其他开源网络框架最大的区别
Okhttp凭什么称霸Android网络框架?
- 域名解析,数据传输,连接复用,路由选择,代理选择,自动遍历可用服务器节点,缓存,自动重试,等等可控,可高度定制化网络请求。能够低成本实现复杂的业务需求
- OkHttp 和 Retrofit,Glide,Fresco 第三方库能很好的衔接,拥有良好的生态系统
- Android4.4的源码中可以看到HttpURLConnection已经替换成OkHttp实现
下面我们一起来探索OkHttp的部分原理和在项目实战中的花式运用
连接复用
Okhttp和服务器建立连接后默认保持5分钟不断开,支持5个socket连接并发,也就是五分钟内客户端如果和已连接的服务器通信不需要重新三次握手连接(三次握手确保了服务端和客户端都具备可靠的通信能力,但握手过程耗时)
HTTP Keep-Alive
在Http早期,每个http请求都要求打开一个tpc socket连接,并且使用一次之后就断开这个tcp连接
使用keep-alive可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用keep-alive机制,可以减少tcp连接建立次数,
当使用Keep-Alive模式(又称持久连接、连接重用)时,Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接
Okhttp中连接复用正是建立在Keep-alive基础之上实现的
Okhttp连接复用实在建立连接过程中使用读写ConnectionPool 中的连接
public final class ConnectionPool {
//保持连接时间
private final long keepAliveDurationNs;
//清理超时过期连接的runable
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
public ConnectionPool() {
//默认保持5分钟长连接
this(5, 5, TimeUnit.MINUTES);
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.maxIdleConnections = maxIdleConnections;
this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
// Put a floor on the keep alive duration, otherwise cleanup will spin loop.
if (keepAliveDuration <= 0) {
throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
}
}
}
Okhttp在打开网络连接时会读写连接池中保存的可用连接,以达到复用
假定客户端和服务器已建立连接,那么在有效期内客户端再次和服务端通信则不需要再次建立连接
连接复用流程图:
DNS
DNS(Domain Name System,域名系统),DNS 服务用于在网络请求时,将域名转为 IP 地址。能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的 IP 数串。
传统的基于 UDP 协议的公共 DNS 服务极易发生 DNS 劫持,从而造成安全问题。
Okhttp中的DNS
Okhttp中使用Dns.lookup将域名解析成ip,默认使用系统解析dns
如果一个域名绑定多个ip,即部署了多个服务节点。Okhttp在建立连接过程中会遍历所有ip,直至建立一个可用连接
Okhttp在ConnectInterceptor调用StreamAllocation打开连接是时会遍历所有ip直接建立一个可靠的链接
实现流程如下:
掌中通智能切换服务器
掌中通先使用的服务端中,有一个域名是带cdn的,有一个是没有的,那么我们如何做到在一个域名挂了自动切换到另一个节点呢?
回到上面的话题,访问业务服务需要同于域名访问,最终是通过服务器ip建立连接进而访问,那么我们回到Okhttp源码
简单的说okhttp建立连接的时候会遍历dns.lookup 查找出来的ip直到建立可用连接位置。
那么我们只需要在dns.lookup的时候,将带cdn和不带cdn的服务url解的域名解析出ip,合并成一个数组既可以以最低成本实现这个需求。
自定义 OKhttp Dns解析源码如下
public class CndAutoSwitchDns implements Dns {
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
if (hostname.contains(getCdnUrl()) || hostname.contains(BASE_NOCDN_URL_ADD)) {
boolean userServer2 = ShareManager.getInstance().getBoolean(Constants.USE_SERVER_TWO);
if (userServer2) {
return getInetAddressByIps(BASE_NOCDN_URL_ADD, hostname);
} else {
return getInetAddressByIps(hostname, BASE_NOCDN_URL_ADD);
}
}
return Dns.SYSTEM.lookup(hostname);
}
}
HttpDns
客户端解析DNS的过程,由客户端发出解析,容易被运营商等中间层劫持,存在劫持和低效的问题。
HttpDNS 使用HTTP协议进行域名解析,代替现有基于UDP的DNS协议,域名解析请求直接发送到HTTPDNS服务器,从而绕过运营商的Local DNS,能够避免Local DNS造成的域名劫持问题和调度不精准问题
public class HttpDns implements Dns {
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
//从自己的DNS服务更加域名查询IP
List<String> serverIpList = DnsUtil.getIpByHost(hostname);
//如果没有查询到则使用系统解析dns
if (serverIpList == null || serverIpList.isEmpty()) {
return Dns.SYSTEM.lookup(hostname);
}
//合并转换成查询到的ip
List<InetAddress> inetAddressList = new ArrayList<>();
for (String ip : serverIpList) {
inetAddressList.add(InetAddress.getByName(ip));
}
return inetAddressList;
}
}
使用过程如下非常简单
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.dns(new HttpDns());
OkHttpClient mOkHttpClient = builder.build();
如何防止charles,fiddler抓包
反编译,代理抓包,这些开发和测试中常用的技巧。charles在配置https证书后手机设置代理后,手机app中的https和http请求基本上属于裸奔
那么我们怎么设置让我们的app没那么容易被抓包呢?
charles https抓包原理,charles伪装成服务器和手机建立http是连接,中间人攻击劫持,收到客户端发出消息后和目标服务器建立连接,作为中间人和服务器通信所以可以抓包。
okhttp 默认使用系统路由选择器,默认跟随系统设置,如果手机设置代理app请求也会走代理
原理上很简单在创建Okhttpclient的时候设置一个自定义路由选择器,自定义的路由选择器设置成Proxy.NO_PROXY(不走代理)
class SafeProxySelector : ProxySelector() {
override fun select(uri: URI?): MutableList<Proxy> {
return Collections.singletonList(Proxy.NO_PROXY)
}
override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) {
}
}
Token刷新
目前中通几乎所有的系统基本上都已对接安全系统,授权信息过期后可用刷新替换新的授权信息
登录过程如下:
使用拦截器对所有请求中添加授权相关信息
发起请求后,拦截返回报文状态码,如果是授权信息过期状态,则拦截请求,刷新授权信息,刷新成功后
使用新授权信息重发请求
请求流程流程图:
刷新token拦截器
class RefreshTokenInterceptor : Interceptor {
companion object {
private const val TOKEN = "Token"
private const val OPEN_ID = "OpenId"
private const val REFRESH_CODE = 406
}
override fun intercept(chain: Interceptor.Chain): Response? {
val request = chain.request()
if (request.header(TOKEN)!=null){
return chain.proceed(request)
}
//如果本地没有token则不处理
val localToken = Sso.getToken()
if (isTokenNullOrEmpty(localToken)) {
Sso.refreshTokenListener?.refreshFailure()
return chain.proceed(request)
}
//添加本地token到请求header
val newRequest = updateRequest(request, localToken!!)
val newResponse = chain.proceed(newRequest)
//根据状态码判断是否需要刷新token
if (!needRefresh(newResponse)) {
return newResponse
}
//网络刷新token
val refreshSsoInfo = refreshToken(localToken)
if (refreshSsoInfo==null) {
//重新登录
Sso.refreshTokenListener?.refreshFailure()
return newResponse
}
//刷新token header重新发送网络请求
val refreshRequest = updateRequest(newRequest, refreshSsoInfo)
return chain.proceed(refreshRequest)
}
private fun needRefresh(newResponse: Response): Boolean {
return newResponse.code() == REFRESH_CODE
}
/**
* token为空空校验
*/
private fun isTokenNullOrEmpty(token: TokenData?): Boolean {
if (token == null) {
return true
}
if (TextUtils.isEmpty(token.access_token)) {
return true
}
return false
}
@Synchronized
fun refreshToken(@NotNull oldTokenData: TokenData): TokenData? {
val localToken = Sso.getToken()
if (localToken!=null && (localToken!!.access_token != oldTokenData.access_token)) {
return localToken
}
try {
val tokenDataResponse: retrofit2.Response<TokenData> = Sso.refreshToken(oldTokenData.refresh_token).execute()
if (tokenDataResponse.isSuccessful) {
val serverSsoInfoEntity = tokenDataResponse.body()
if (!isTokenNullOrEmpty(serverSsoInfoEntity)) {
//保存最新token到本地
Sso.saveToken(serverSsoInfoEntity!!)
return serverSsoInfoEntity
}
}
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
private fun updateRequest(request: Request, ssoInfoEntity: TokenData): Request {
return request
.newBuilder()
.header(TOKEN, ssoInfoEntity.access_token)
.header(OPEN_ID, ssoInfoEntity.openid).build()
}
}
android源码中是如何使用OkHttp作为网络请求框架
2013年Google发布Android 4.4 时,Httpurlconnection 底层采用了OkHttp实现,然而怎么实现的呢
我们来看看这段非常普通创建http连接的代码
URL url = URL("http://www.zto.com/");
url.openConnection();
顺藤摸瓜打开URL源码(以下是精简版的部分源码)
import java.net.MalformedURLException;
import java.net.URLStreamHandler;
public class URL {
//URLStreamHandler 是所有流协议处理程序的通用超类,
transient URLStreamHandler handler;
public URL(String protocol, String host, int port, String file,
URLStreamHandler handler) throws MalformedURLException {
//构造函数中穿件URLStreamHandler
if (handler == null && (handler = getURLStreamHandler(protocol)) == null) {
throw new MalformedURLException("unknown protocol: " + protocol);
}
this.handler = handler;
}
/**
* 根据协议获取URLStreamHandler
* @param protocol
* @return
*/
static URLStreamHandler getURLStreamHandler(String protocol) {
URLStreamHandler handler = null;
try {
handler = createBuiltinHandler(protocol);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return handler;
}
/**
* 创建URLStreamHandler的具体方法
* @param protocol
* @return
* @throws ClassNotFoundException
* @throws InstantiationException
* @throws IllegalAccessException
*/
private static URLStreamHandler createBuiltinHandler(String protocol)
throws ClassNotFoundException, InstantiationException, IllegalAccessException {
URLStreamHandler handler = null;
if (protocol.equals("file")) {
handler = new sun.net.www.protocol.file.Handler();
} else if (protocol.equals("ftp")) {
handler = new sun.net.www.protocol.ftp.Handler();
} else if (protocol.equals("jar")) {
handler = new sun.net.www.protocol.jar.Handler();
} else if (protocol.equals("http")) {
//如果是http请求或者https请求创建用Okhttp实现的URLStreamHandler
handler = (URLStreamHandler) Class.
forName("com.android.okhttp.HttpHandler").newInstance();
} else if (protocol.equals("https")) {
handler = (URLStreamHandler) Class.
forName("com.android.okhttp.HttpsHandler").newInstance();
}
return handler;
}
/**
* 打开http连接
* @return
* @throws java.io.IOException
*/
public URLConnection openConnection() throws java.io.IOException {
return handler.openConnection(this);
}
}
Android源码中使用的并非最新版本的Okhttp 所以,我们看这个分支源码
package com.squareup.okhttp;
import java.io.IOException;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
public final class HttpHandler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL url) throws IOException {
return new OkHttpClient().open(url);
}
@Override
protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
if (url == null || proxy == null) {
throw new IllegalArgumentException("url == null || proxy == null");
}
return new OkHttpClient().setProxy(proxy).open(url);
}
@Override
protected int getDefaultPort() {
return 80;
}
}
public final class OkHttpClient implements URLStreamHandlerFactory{
public HttpURLConnection open(URL url) {
return open(url, proxy);
}
HttpURLConnection open(URL url, Proxy proxy) {
String protocol = url.getProtocol();
OkHttpClient copy = copyWithDefaults();
copy.proxy = proxy;
if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy);
if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy);
throw new IllegalArgumentException("Unexpected protocol: " + protocol);
}
}