设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间07 现在时间07 缓存数据 女人不要只算计自己喜欢的任何物品多少钱,要计算自己的青春还剩多少年;要懂得爱自己,舍得爱自己;不为别人,只为那个限量版的自己!

女人不要只算计自己喜欢的任何物品多少钱,要计算自己的青春还剩多少年;要懂得爱自己,舍得爱自己;不为别人,只为那个限量版的自己!

查看: 1902|回复: 4

OKHttp使用详解

[复制链接]

  离线 

TA的专栏

  • 打卡等级:热心大叔
  • 打卡总天数:225
  • 打卡月天数:0
  • 打卡总奖励:3549
  • 最近打卡:2025-03-19 11:38:45
等级头衔

等級:晓枫资讯-上等兵

在线时间
0 小时

积分成就
威望
0
贡献
411
主题
375
精华
0
金钱
4760
积分
842
注册时间
2023-1-22
最后登录
2025-5-31

发表于 2024-5-26 22:13:54 来自手机 | 显示全部楼层 |阅读模式
目录


  • 网络请求流程分析
  • 总结
  • 常见问题:

    • OKHttp有哪些拦截器,分别起什么作用?

  • OkHttp怎么实现连接池
  • OkHttp里面用到了什么设计模式
  • OkHttp中为什么使用构建者模式?
  • 怎么设计一个自己的网络访问框架,为什么这么设计?
OkHttp 是一套处理 HTTP 网络请求的依赖库,由 Square 公司设计研发并开源,目前可以在 Java 和 Kotlin 中使用。对于 Android App 来说,OkHttp 现在几乎已经占据了所有的网络请求操作,RetroFit + OkHttp 实现网络请求似乎成了一种标配。因此它也是每一个 Android 开发工程师的必备技能,了解其内部实现原理可以更好地进行功能扩展、封装以及优化。

网络请求流程分析

先看下 OkHttp 的基本使用:
1.png

除了直接 new OkHttpClient 之外,还可以使用内部工厂类 Builder 来设置 OkHttpClient。如下所示:
2.png

请求操作的起点从 OkHttpClient.newCall().enqueue() 方法开始:
newCall
3.png

RealCall.enqueue
调用 Dispatcher 的入队方法,执行一个异步网络请求的操作。
4.png

可以看出,最终请求操作是委托给 Dispatcher的enqueue 方法内实现的。
  1. Dispatcher 是 OkHttpClient 的调度器,是一种门户模式。主要用来实现执行、取消异步请求操作。本质上是内部维护了一个线程池去执行异步操作,
  2. 并且在 Dispatcher 内部根据一定的策略,保证最大并发个数、同一 host 主机允许执行请求的线程个数等。
复制代码
Dispatcher的enqueue 方法的具体实现如下:
5.png

可以看出,实际上就是使用线程池执行了一个 AsyncCall,而 AsyncCall 实现了 Runnable 接口,因此整个操作会在一个子线程(非 UI 线程)中执行。
继续查看 AsyncCall 中的 run 方法如下:
6.png

在 run 方法中执行了另一个 execute 方法,而真正获取请求结果的方法是在 getResponseWithInterceptorChain 方法中,从名字也能看出其内部是一个拦截器的调用链,具体代码如下:
7.png

每一个拦截器的作用如下。

  • BridgeInterceptor:主要对 Request 中的 Head 设置默认值,比如 Content-Type、Keep-Alive、Cookie 等。
  • CacheInterceptor:负责 HTTP 请求的缓存处理。
  • ConnectInterceptor:负责建立与服务器地址之间的连接,也就是 TCP 链接。
  • CallServerInterceptor:负责向服务器发送请求,并从服务器拿到远端数据结果。
  • 在添加上述几个拦截器之前,会调用 client.interceptors 将开发人员设置的拦截器添加到列表当中。
对于 Request 的 Head 以及 TCP 链接,我们能控制修改的成分不是很多。

总结

1.Okhttp是对Socket的封装。有三个主要的类,Request,Response,Call
2.默认使用new OkHttpClient() 创建初client对象。如果需要初始化网络请求的参数,如timeout,interceptor等,可以创建Builder,通过builder.build() 创建初client对象。
3.使用new Request.Builder().url().builder()创建初requst对象。
4.通过client.newCall()创建出call对象,同步使用call.excute(), 异步使用call,enqueue(). 这里Call是个接口,具体的实现在RealCall这个实现类里面。
Okhttp的高效体现在,okhttp内有个Dispatcher类,是okhttp内部维护的一个线程池,对最大连接数,host最大访问量做了初始定义。维护3个队列及1个线程池
readyAsyncCalls
  1. 待访问请求队列,里面存储准备执行的请求。
复制代码
runningAsyncCalls
  1. 异步请求队列,里面存储正在执行,包含已经取消但是还没有结束的请求。
复制代码
runningSyncCalls
  1. 同步请求队列,正在执行的请求,包含已经取消但是还没有结束的请求。
复制代码
ExecutorService
  1. 线程池,最小0,最大Max的线程池
复制代码
在执行call.excute()的时候,调用到realcall类里的excute方法,这个是同步方法,在方法的第一行就加了锁,判断executed标记,如果是true就抛出异常,保证一个请求只被执行一次。false的话继续向下执行。调用client.dispatcher.excute()进入到dispatcher类中,向runningSyncCalls队列中添加当前这个请求。执行结束会调用finished方法
如果是异步操作,会创建一个RealCall.AsyncCall对象,AsyncCall继承的NamedRunnable接口,NamedRunnable是个runnable。进入到Dispatcher的enqueue()方法中,首先判断线程池中线程的数据,host的访问量,如果都没有达到那么加入到runningAsyncCalls中,并执行。否则加入到readyAsyncCalls队列中。
finished方法,如果是异步操作,promoteCall方法,promoteCalls()中用迭代器遍历readyAsyncCalls 然后加入到runningAsyncCalls
RealConnection
  1. 真正的连接操作类,对soket封装,http1/http2的选择,ssl协议等等信息。一个recalconnection就是一次链接
复制代码
ConnectionPool
  1. 链接池,管理http1/http2的连接,同一个address共享一个connection,实现链接的复用。
复制代码
StreamAlloction
  1. 保存了链接信息,address,HttpCodec,realconnection,connectionpool等信息
复制代码
常见问题:


OKHttp有哪些拦截器,分别起什么作用?
  1. OKHTTP
复制代码
的拦截器是把所有的拦截器放到一个list里,然后每次依次执行拦截器,并且在每个拦截器分成三部分:

  • 预处理拦截器内容
  • 通过
    1. proceed
    复制代码
    方法把请求交给下一个拦截器
  • 下一个拦截器处理完成并返回,后续处理工作。
这样依次下去就形成了一个链式调用,看看源码,具体有哪些拦截器:
  1.   Response getResponseWithInterceptorChain() throws IOException {
  2.     // Build a full stack of interceptors.
  3.     List<Interceptor> interceptors = new ArrayList<>();
  4.     interceptors.addAll(client.interceptors());
  5.     interceptors.add(retryAndFollowUpInterceptor);
  6.     interceptors.add(new BridgeInterceptor(client.cookieJar()));
  7.     interceptors.add(new CacheInterceptor(client.internalCache()));
  8.     interceptors.add(new ConnectInterceptor(client));
  9.     if (!forWebSocket) {
  10.       interceptors.addAll(client.networkInterceptors());
  11.     }
  12.     interceptors.add(new CallServerInterceptor(forWebSocket));
  13.     Interceptor.Chain chain = new RealInterceptorChain(
  14.         interceptors, null, null, null, 0, originalRequest);
  15.     return chain.proceed(originalRequest);
  16.   }
复制代码
根据源码可知,一共七个拦截器:

    1. addInterceptor(Interceptor)
    复制代码
    ,这是由开发者设置的,会按照开发者的要求,在所有的拦截器处理之前进行最早的拦截处理,比如一些公共参数,Header都可以在这里添加。
    1. RetryAndFollowUpInterceptor
    复制代码
    ,这里会对连接做一些初始化工作,以及请求失败的充实工作,重定向的后续请求工作。跟他的名字一样,就是做重试工作还有一些连接跟踪工作。
    1. BridgeInterceptor
    复制代码
    ,这里会为用户构建一个能够进行网络访问的请求,同时后续工作将网络请求回来的响应Response转化为用户可用的Response,比如添加文件类型,content-length计算添加,gzip解包。
    1. CacheInterceptor
    复制代码
    ,这里主要是处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。
    1. ConnectInterceptor
    复制代码
    ,这里主要就是负责建立连接了,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCodec
    1. networkInterceptors
    复制代码
    ,这里也是开发者自己设置的,所以本质上和第一个拦截器差不多,但是由于位置不同,所以用处也不同。这个位置添加的拦截器可以看到请求和响应的数据了,所以可以做一些网络调试。
    1. CallServerInterceptor
    复制代码
    ,这里就是进行网络数据的请求和响应了,也就是实际的网络I/O操作,通过socket读写数据。

OkHttp怎么实现连接池

为什么需要连接池?
频繁的进行建立
  1. Sokcet
复制代码
连接和断开
  1. Socket
复制代码
是非常消耗网络资源和浪费时间的,所以HTTP中的
  1. keepalive
复制代码
连接对于降低延迟和提升速度有非常重要的作用。
  1. keepalive机制
复制代码
是什么呢?也就是可以在一次TCP连接中可以持续发送多份数据而不会断开连接。所以连接的多次使用,也就是复用就变得格外重要了,而复用连接就需要对连接进行管理,于是就有了连接池的概念。
OkHttp中使用
  1. ConectionPool
复制代码
实现连接池,默认支持5个并发
  1. KeepAlive
复制代码
,默认链路生命为5分钟。
怎么实现的?
1)首先,
  1. ConectionPool
复制代码
中维护了一个双端队列
  1. Deque
复制代码
,也就是两端都可以进出的队列,用来存储连接。
2)然后在
  1. ConnectInterceptor
复制代码
,也就是负责建立连接的拦截器中,首先会找可用连接,也就是从连接池中去获取连接,具体的就是会调用到
  1. ConectionPool
复制代码
的get方法。
  1. RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
  2.     assert (Thread.holdsLock(this));
  3.     for (RealConnection connection : connections) {
  4.       if (connection.isEligible(address, route)) {
  5.         streamAllocation.acquire(connection, true);
  6.         return connection;
  7.       }
  8.     }
  9.     return null;
  10.   }
复制代码
也就是遍历了双端队列,如果连接有效,就会调用acquire方法计数并返回这个连接。
3)如果没找到可用连接,就会创建新连接,并会把这个建立的连接加入到双端队列中,同时开始运行线程池中的线程,其实就是调用了
  1. ConectionPool
复制代码
的put方法。
  1. public final class ConnectionPool {
  2.     void put(RealConnection connection) {
  3.         if (!cleanupRunning) {
  4.             //没有连接的时候调用
  5.             cleanupRunning = true;
  6.             executor.execute(cleanupRunnable);
  7.         }
  8.         connections.add(connection);
  9.     }
  10. }
复制代码
其实这个线程池中只有一个线程,是用来清理连接的,也就是上述的
  1. cleanupRunnable
复制代码
  1. private final Runnable cleanupRunnable = new Runnable() {
  2.         @Override
  3.         public void run() {
  4.             while (true) {
  5.                 //执行清理,并返回下次需要清理的时间。
  6.                 long waitNanos = cleanup(System.nanoTime());
  7.                 if (waitNanos == -1) return;
  8.                 if (waitNanos > 0) {
  9.                     long waitMillis = waitNanos / 1000000L;
  10.                     waitNanos -= (waitMillis * 1000000L);
  11.                     synchronized (ConnectionPool.this) {
  12.                         //在timeout时间内释放锁
  13.                         try {
  14.                             ConnectionPool.this.wait(waitMillis, (int) waitNanos);
  15.                         } catch (InterruptedException ignored) {
  16.                         }
  17.                     }
  18.                 }
  19.             }
  20.         }
  21.     };
复制代码
这个
  1. runnable
复制代码
会不停的调用cleanup方法清理线程池,并返回下一次清理的时间间隔,然后进入wait等待。
怎么清理的呢?看看源码:
  1. long cleanup(long now) {
  2.     synchronized (this) {
  3.       //遍历连接
  4.       for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
  5.         RealConnection connection = i.next();
  6.         //检查连接是否是空闲状态,
  7.         //不是,则inUseConnectionCount + 1
  8.         //是 ,则idleConnectionCount + 1
  9.         if (pruneAndGetAllocationCount(connection, now) > 0) {
  10.           inUseConnectionCount++;
  11.           continue;
  12.         }
  13.         idleConnectionCount++;
  14.         // If the connection is ready to be evicted, we're done.
  15.         long idleDurationNs = now - connection.idleAtNanos;
  16.         if (idleDurationNs > longestIdleDurationNs) {
  17.           longestIdleDurationNs = idleDurationNs;
  18.           longestIdleConnection = connection;
  19.         }
  20.       }
  21.       //如果超过keepAliveDurationNs或maxIdleConnections,
  22.       //从双端队列connections中移除
  23.       if (longestIdleDurationNs >= this.keepAliveDurationNs
  24.           || idleConnectionCount > this.maxIdleConnections) {      
  25.         connections.remove(longestIdleConnection);
  26.       } else if (idleConnectionCount > 0) {      //如果空闲连接次数>0,返回将要到期的时间
  27.         // A connection will be ready to evict soon.
  28.         return keepAliveDurationNs - longestIdleDurationNs;
  29.       } else if (inUseConnectionCount > 0) {
  30.         // 连接依然在使用中,返回保持连接的周期5分钟
  31.         return keepAliveDurationNs;
  32.       } else {
  33.         // No connections, idle or in use.
  34.         cleanupRunning = false;
  35.         return -1;
  36.       }
  37.     }
  38.     closeQuietly(longestIdleConnection.socket());
  39.     // Cleanup again immediately.
  40.     return 0;
  41.   }
复制代码
也就是当如果空闲连接
  1. maxIdleConnections
复制代码
超过5个或者keepalive时间大于5分钟,则将该连接清理掉。
4)这里有个问题,怎样属于空闲连接?
其实就是有关刚才说到的一个方法
  1. acquire
复制代码
计数方法:
  1.   public void acquire(RealConnection connection, boolean reportedAcquired) {
  2.     assert (Thread.holdsLock(connectionPool));
  3.     if (this.connection != null) throw new IllegalStateException();
  4.     this.connection = connection;
  5.     this.reportedAcquired = reportedAcquired;
  6.     connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  7.   }
复制代码
  1. RealConnection
复制代码
中,有一个
  1. StreamAllocation
复制代码
虚引用列表
  1. allocations
复制代码
。每创建一个连接,就会把连接对应的
  1. StreamAllocationReference
复制代码
添加进该列表中,如果连接关闭以后就将该对象移除。
其实可以这样理解,在上层反复调用acquire和release函数,来增加或减少connection.allocations所维持的集合的大小,到最后如果size大于0,则代表RealConnection还在使用连接,如果size等于0,那就说明已经处于空闲状态了
5)连接池的工作就这么多,并不复杂,主要就是管理双端队列
  1. Deque<RealConnection>
复制代码
,可以用的连接就直接用,然后定期清理连接,同时通过对
  1. StreamAllocation
复制代码
的引用计数实现自动回收。

OkHttp里面用到了什么设计模式

责任链模式
这个不要太明显,可以说是okhttp的精髓所在了,主要体现就是拦截器的使用,具体代码可以看看上述的拦截器介绍。
建造者模式
在Okhttp中,建造者模式也是用的挺多的,主要用处是将对象的创建与表示相分离,用Builder组装各项配置。
比如Request:
  1. public class Request {
  2.   public static class Builder {
  3.     @Nullable HttpUrl url;
  4.     String method;
  5.     Headers.Builder headers;
  6.     @Nullable RequestBody body;
  7.     public Request build() {
  8.       return new Request(this);
  9.     }
  10.   }
  11. }
复制代码
工厂模式
工厂模式和建造者模式类似,区别就在于工厂模式侧重点在于对象的生成过程,而建造者模式主要是侧重对象的各个参数配置。
例子有CacheInterceptor拦截器中又个CacheStrategy对象:
  1.     CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
  2.     public Factory(long nowMillis, Request request, Response cacheResponse) {
  3.       this.nowMillis = nowMillis;
  4.       this.request = request;
  5.       this.cacheResponse = cacheResponse;
  6.       if (cacheResponse != null) {
  7.         this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
  8.         this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
  9.         Headers headers = cacheResponse.headers();
  10.         for (int i = 0, size = headers.size(); i < size; i++) {
  11.           String fieldName = headers.name(i);
  12.           String value = headers.value(i);
  13.           if ("Date".equalsIgnoreCase(fieldName)) {
  14.             servedDate = HttpDate.parse(value);
  15.             servedDateString = value;
  16.           } else if ("Expires".equalsIgnoreCase(fieldName)) {
  17.             expires = HttpDate.parse(value);
  18.           } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
  19.             lastModified = HttpDate.parse(value);
  20.             lastModifiedString = value;
  21.           } else if ("ETag".equalsIgnoreCase(fieldName)) {
  22.             etag = value;
  23.           } else if ("Age".equalsIgnoreCase(fieldName)) {
  24.             ageSeconds = HttpHeaders.parseSeconds(value, -1);
  25.           }
  26.         }
  27.       }
  28.     }
复制代码
观察者模式
之前我写过一篇文章,是关于Okhttp中websocket的使用,由于webSocket属于长连接,所以需要进行监听,这里是用到了观察者模式:
  1.   final WebSocketListener listener;
  2.   @Override public void onReadMessage(String text) throws IOException {
  3.     listener.onMessage(this, text);
  4.   }
复制代码
单例模式
这个就不举例了,每个项目都会有

OkHttp中为什么使用构建者模式?

使用多个简单的对象一步一步构建成一个复杂的对象;

  • 优点: 当内部数据过于复杂的时候,可以非常方便的构建出我们想要的对象,并且不是所有的参数我们都需要进行传递;
  • 缺点: 代码会有冗余

怎么设计一个自己的网络访问框架,为什么这么设计?

我目前还没有正式设计过网络访问框架,
是我自己设计的话,我会从以下两个方面考虑

  • 先参考现有的框架,找一个比较合适的框架作为启动点,比如说,基于上面讲到的
    1. okhttp
    复制代码
    的优点,选择
    1. okhttp
    复制代码
    的源码进行阅读,并且将主线的流程抽取出,为什么这么做,因为
    1. okhttp
    复制代码
    里面虽然涉及到了很多的内容,但是我们用到的内容并不是特别多;保证先能运行起来一个基本的框架;
  • 考虑拓展,有了基本框架之后,我会按照我目前在项目中遇到的一些需求或者网路方面的问题,看看能不能基于我这个框架进行优化,比如服务器它设置的缓存策略,
    我应该如何去编写客户端的缓存策略去对应服务器的,还比如说,可能刚刚去建立基本的框架时,不会考虑
    1. HTTPS
    复制代码
    的问题,那么也会基于后来都要求
    1. https
    复制代码
    ,进行拓展;
为什么要基于
  1. Okhttp
复制代码
,就是因为它是基于Socket,从我个人角度讲,如果能更底层的深入了解相关知识,这对我未来的技术有很大的帮助;
如何考虑app的安全性?
1:使用https协议进行交互
2:数据交互时,根据业务分出哪些是敏感信息,凡是敏感信息使用对称加密方式,如果是类似密码的,则使用不可逆的加密方式;md5
3:考虑跟钱相关,或者同等重要的数据接口,需要做多重验证,比如:前端加密请求参数,合并请求参数生成MD5码,服务器端做多重认证,最好能对比本地数据库或者缓存之类的信息;
4:混淆,
5: app加固,dex文件进行加密,这种方式,可以通过”内存下载“,不安全,也只是为了增加破解难度;
6:将加密算法,一些核心数据添加到so文件中;
到此这篇关于OKHttp详解的文章就介绍到这了,更多相关OKHttp详解内容请搜索晓枫资讯以前的文章或继续浏览下面的相关文章希望大家以后多多支持晓枫资讯!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
11
积分
2
注册时间
2024-1-21
最后登录
2024-1-21

发表于 2024-9-25 16:16:05 | 显示全部楼层
顶顶更健康!!!
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
13
积分
6
注册时间
2022-12-27
最后登录
2022-12-27

发表于 2024-10-20 19:18:27 | 显示全部楼层
感谢楼主分享。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

  • 打卡等级:常驻代表
  • 打卡总天数:32
  • 打卡月天数:0
  • 打卡总奖励:394
  • 最近打卡:2025-04-12 21:49:08
等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
451
积分
74
注册时间
2023-1-13
最后登录
2025-4-12

发表于 2024-10-31 12:13:22 | 显示全部楼层
感谢楼主,顶。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
18
积分
16
注册时间
2022-12-27
最后登录
2022-12-27

发表于 2025-4-7 13:54:34 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼
4楼
5楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表