目的有二:学习一下著名开源项目的架构;大致了解一下http
硬知识
- Cache-Control:RFC
- 可以放在Cache-Control(http1.1以后)下,也可以放到Pragma(兼容http1.0)下
- 可能会有多个
- max-age:>0则使用缓存,否则请求服务端确认是否使用缓存
- max-stale:请求时,能容许的超过freshness的最长时长,也就是接受服务端认为超时的缓存的时间
- s-maxage:用于共享缓存(CDN),语义同max-age
- min-fresh:请求时,仅接受min-fresh时间后还未过期的数据
- no-cache:请求时,服务端评估缓存结果;返回时,服务端返回的结果必须经过服务端评估才能再次使用,返回的结果可以针对性的指定header的某些field不可重用
- no-store:不缓存
- no-transform:中间人不能修改数据
- only-if-cached:请求只接受缓存的结果
- must-revalidate:缓存超时后,一定要去服务端校验一次
- private:仅返回用,私有缓存,仅存在对应用户名下的cache,不能存在share cache
- public:使用share cache存储,细节比较多,还是看rfc吧
OkHttp功能
也可以说是Http1.1+WebDAV支持的功能(请求的路径):
- Address:
- 选择使用的协议——Protocol
- 连接配置——ConnectionSpec(看起来是配置tls的)
- DNS——Dns接口,可以替换
- SSL——SSLSocketFactory和HostnameVerifier(Java rt中)、Certificate Pinning
- 请求校验——Authenticator,对于407的返回码,为请求添加Authorization Header
- Proxy和选择器——Proxy、ProxySelector(java.net)
- Request:RFC
- Methods:
- GET:不能有request body
- HEAD:不能有request body,服务端只返回header数据
- POST:必须有request body,除非有明确的cache信息,否则不cache
- PUT:必须有request body,替换服务端对应的数据
- DELETE:可以有request body,删除服务端对应的数据
- CONNECT:不能有request body,开启一个到对应端口的数据通道
- OPTIONS:可以有request body,获得服务端支持的请求参数
- TRACE:不能有request body,就是trace route的http版
- WebDAV:一个对http1.1方法类型的扩展
- Header:因为类型很多且可扩展,所以OkHttp直接暴露了kv接口,没有setter/builder
- 控制类:
- Cache-Control,见上
- Expect:仅支持100-continue,如果服务端不能满足该行为,直接返回417
- Host:目标地址
- Max-Forwards:针对TRACE和OPTIONS方法,限制传播数量
- Pragma:兼容1.0的Cache-Control
- Range:指定希望获得的数据块位置,在多线程下载、断点续传时用
- TE:除了chuncked之外额外支持的编码
- 判断类:
- If-Match:数据是否对应,必须使用强校验,内容是服务端和客户端的私有协议。主要用在改服务端数据
- If-None-Match:数据是否不存在,必须使用弱校验。主要用来更新cache
- If-Modified-Since:更新cache或减少Proxy转发次数
- If-Unmodified-Since:更新服务端,同If-Match
- 内容类型类,都可以用 q=小数 来说明对于指定类型的偏好程度(这个q还是很有借鉴意义的):
- Accept:接受的媒体类型
- Accept-Charset
- Accept-Encoding
- Accept-Language
- 内容类:
- From:来自的人
- Referer:来自的URI
- User-Agent:设备信息
- 控制类:
- Methods:
- Response:
- Code(终于知道在哪找了):
- 1XX:信息类
- 100 Continue:继续发送下一步请求
- 101 Switching Protocols
- 2XX:成功类
- 200 OK
- 201 Created:满足客户端要求的资源已经被创建了
- 202 Accepted
- 203 Non-Authoritative Information:Proxy已经把原服务端的数据修改了
- 204 No Content
- 205 Reset Content:重置客户端的输入内容
- 3XX:重定向
- 300 Multiple Choices
- 301 Moved Permanently
- 302 Found:临时修改位置
- 303 See Other:指定到另一个资源,可以换Method
- 307 Temporary Redirect:跟302差不多,但是不能换Method
- 4XX:客户端错误
- 400 Bad Request
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable:user-agent跟资源八字不合
- 408 Request Timeout
- 409 Conflict:资源状态不相符
- 410 Gone:永久性的404
- 411 Length Required
- 413 Payload Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 417 Expectation Failed
- 426 Upgrade Required:必须升级协议
- 5XX:服务端错误
- 500 Internal Server Error
- 501 Not Implemented
- 502 Bad Gateway:代理服务器返回,表明被代理服务器坏掉了
- 503 Service Unavailable:临时性的501
- 504 Gateway Timeout
- 505 HTTP Version Not Supported
- 1XX:信息类
- Header
- 控制类
- Age:中间人返回的结果,代表结果的年龄
- Cache-Control
- Expires:结果的过期日期
- Warning:返回码不能表征的细节消息,也是3位错误码
- 控制类
- Code(终于知道在哪找了):
OkHttp请求流程(仅以RealCall.execute为例,应该是覆盖了全部http流程)
- getResponseWithInterceptorChain():使用Interceptor修改Request。与Http相关的是Gzip压缩request body
- getResponse():填入内容类型类请求头。因为可能有gzip,所以这一步要在修改response的最后一步
- getResponse():初始化HttpEngine,其中,会根据request,生成一个地址对象,并使用连接池初始化一个StreamAllocation(细节见后)
- getResponse():while循环,开始处理包含的子请求(返回码)
- HttpEngine.networkRequest():填入连接相关的数据,不可cookie(可以用java的CookieHandler)
- 查看Cache的response是否可用(使用Request和Response里的判断类、控制类和Cache-Control信息)
- 开始连接
- 向流中写入Request Header[和body]
- 读取数据
- 看是否有Connection字段是否为close,关闭连接
- 特殊处理204/205,返回有数据就报错
- 更新cookie
- 合并或者更新cache
- 检查有没有后续请求(401/407/408/3XX)
- 检查重定向次数是否过多
- [重新开始新子请求]或[返回response]
StreamAllocation
- 区分Http2和1.1的标志是是否支持长连接
- 关于http2第一个答案
合理设计
- 使用Builder把成员变量的setter从复杂的逻辑对象里剥离出来,让结构清晰一些,也做到了对象的immutable——OkHttpClient.Builder
但是,可能有个FieldWrapper更加方便:Buildee中需要Builder配置的所有Field都放到FieldWrapper中。Builder在构造函数中new一个FieldWrapper,在build时,把FieldWrapper直接作为成员变量赋值给Buildee,这样可以做到单点修改 - 使用Interceptor和InterceptorChain方式,层层过滤、修改请求和结果,基本做到了开闭。但是cache等功能其实也可以使用Interceptor来做,现在直接用的直接调用代码
- 用了很多ActiveObject模式,其实大部分操作都是靠request、RealCall这些对象直接execute实现的
- 极完整的隔离了数据链路层,HttpStream
- 分层,对每层的接口都有针对性的mock和测试用例
- 对外提供唯一功能接口对象,该对象以context形式对内提供所有相关功能
- 以功能分类,有一种把函数封装成对象的既视感,其实就是把一个参数、逻辑极为复杂的函数封装成了一个对象,参数都靠成员变量,流程控制也更加灵活。保证了相关逻辑完全在同一个类中管控:
- Dispatcher:所有跟任务调度和记录相关
- OkHttpClient:对外接口
- RealCall:请求所有逻辑
- HttpEngine:一次请求的所有逻辑
- 为用途不可预测的对象加一个Object的tag,方便扩展——Request、Android中的View
- StreamAllocation作为一个中台,协调Connection、Stream和Call
技巧
- 用空interface来进行类型区分——Collections.unmodifiableList——RandomAccess接口
- 统一对外接口(OkHttpClient)不是单例,保证了调用方可以方便的扩展出多个不同的请求方法,包括分级qos等
- HttpEngine每次子请求发起前会查看是否cancel。实现多次不可中断操作构成的大操作的中断功能,大概都是这个思路
可能的问题
- static的field比较多,而且都是构造出来的对象,会不会在cinit的时候有性能问题
- Interceptor.Chain的默认实现,会对每个Interceptor new出一个新的Chain出来,没有get到好处。似乎是为了保证请求间的数据(Interceptor的成员变量)不会相互影响。但是,方法的局部变量也可以做到啊。。。
- 对于HttpEngine,一个engine仅对应着一次request,每redirect一次都会new出来一个。似乎开发者很喜欢这个用法
- 看起来okHttp对内存应该是各种不在乎的
- strategy似乎用的不太完美,对于webSocket各种if做兼容
- 对于enqueue的AsyncCall,没有找到调用Dispatcher.finished的地方。。。略尴尬。。。这些逻辑暴露出来也不太好
- OkHttpClient不是单例,但是每个client都会覆盖Internal的单例,可能有坑啊
- 貌似http2使用了一个单独的线程搞的数据传输,而请求只是在等待,没看太懂