10月裸辞至今找两个月工作了,本来面试就少才四个,还都没结果,麻了。。
在家呆着没事,也懒得复习面试八股文了,就分析下OkHttp吧,Android端使用最广泛的网络框架。
版本:3.14.9
个人将项目拆解为五个部分进行理解会议,包括请求封装请求、任务调度、责任链模式、连接池以及网络数据流的读写。
封装请求
设计模式是采用Builder构建者模式,原因是构建对象参数比较灵活,其次请求对象构建完成后是不可变对象,因此使用构建者模式比较合适。
通过 curl命令 curl -v https://www.baidu.com/,可见以下打印:
> GET / HTTP/1.1
> Host: www.baidu.com
> User-Agent: curl/7.79.1
> Accept: */*
对应请求实体类Request, 将请求地址,请求方式以及请求头进行封装:
public final class Request {
final HttpUrl url;
final String method;
final Headers headers;
final @Nullable RequestBody body;
final Map<Class<?>, Object> tags;
}
RequestBody是对请求体的封装,通过GET/HEAD/POST/DELETE/PUT/PATCH封装。
HTTP协议的方法及应用场景 - susanhonly - 博客园
- HEAD:HEAD和GET本质是一样的,区别在于HEAD不含有呈现数据,而仅仅是HTTP头信息。有的人可能觉得这个方法没什么用,其实不是这样的。想象一个业务场景:欲判断某个资源是否存在,我们通常使用GET,但这里用HEAD则一样更加明确。
- PUT:这个方法比较少见。HTML表单也不支持这个。本质上来讲,PUT和POST极为相似,都是向服务器发送数据,但它们之间有一个重要区别,PUT通常指定了资源的存放位置,而POST则没有,POST的数据存放位置由服务器自己决定。举个例子:如一个用于提交博文的URL,/addBlog。如果用PUT,则提交的URL会是像这样的“/addBlog/abc123”,其中abc123就是这个博文的地址。而如果用POST,则这个地址会在提交后由服务器告知客户端。目前大部分博客都是这样的。显然,PUT和POST用途是不一样的。具体用哪个还取决于当前的业务场景。
- DELETE:删除某一个资源。基本上这个也很少见,不过还是有一些地方比如amazon的S3云服务里面就用的这个方法来删除资源。
- OPTIONS:这个方法很有趣,但极少使用。它用于获取当前URL所支持的方法。若请求成功,则它会在HTTP头部包含一个名为“Allow”的头,值是所支持的方法,如“GET,POST”。
客户端用的比较多的是GET和POST。当使用POST时,通过FormBody或者MultipartBody构建数据。FormBody用于传输表单数据,需要以JSON格式传递数据时设置为 application/json; MultipartBody用于文件上传时,通过RequestBody.create接口创建上传对象,可以是文件或者字节流。
给我们开发一个启示,如果api提供的接口请求参数比较多,可以使用Request包装的方式使得调用更简单。
任务调度
请求任务根据是否异步进行分发,任务根据同步还是异步存入不同类型的队列中,后进入责任链模块处理;异步任务中有限制:然后会根据64并发限制以及同主机5并发限制启动线程池。
因为网络接口支持取消的功能,因此无论是同步还是异步请求任务都会首先添加到队列中做记录,后续如果有取消的触发,直接从队列中取出对应的值取消就可以了。
同步队列: private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
异步队列:
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
当readyAsyncCalls中的任务通过并发限制检查之后,移入runningAsyncCalls队列中。
ArrayDeque
按照注释解释:容器大小可动态调整的非线程安全队列;元素非空;以栈方式使用时性能高于Stack,以队列方式使用时性能高于LinkedList。请求任务变化频繁,需要经常性添加与删除,AD采用循环数组实现,在增删查以及扩容方面优化多。平常开发可借鉴使用。Java ArrayDeque源码剖析 - CarpenterLee - 博客园
责任链
责任链模式必须注意先后顺序问题。
从前往后依次是:自定义拦截器,重试和重定向,配置请求头,读写缓存,连接,请求。
自定义拦截器是开发可以插入处理操作的部分,必须前置。还有WebSocket的处理省略了,平常从来没有用到过。
连接池
获取连接流程是在ConnectInterceptor中完成。创建Socket连接后,
1.从当前请求中获取连接,如果有且可用则返回,此处感觉像是处理重定向的情况。
2.从连接池中获取新连接。
3.如果未从连接池获取到,遍历路由,继续获取,这里应该是考虑到Http2协议,http2可以让多个请求合并为一个TCP连接发起。
4.如果连接池未获取到,创建新连接,并与当前请求绑定。
在连接池内部维护一个ArrayDeque的双端队列,默认的闲置的连接数为5,闲置连接时间为5分钟。当创建新连接添加入队时,会在子线程启动清理任务来删除长时间不用的连接避免资源浪费。
请求接口
利用在ConnectInterceptor中返回的ExchangeCodec与服务端交互。低层数据流工具使用了Okio的Source/Sink实现读写流。