网络体系
-
主要关注TCP/IP模型,了解OSI模型即可
image.png
TCP/IP协议族
TCP/IP概念层模型 | 功能 | TCP/IP协议族 |
---|---|---|
文件传输,电子邮件,文件服务 | FTP、HTTP、DNS | |
应用层 | 数据格式化,代码格式化,数据加密 | 无 |
解除或建立与别的接点的联系 | 无 | |
传输层 | 提供端对端的接口 | TCP/UDP |
网络层 | 为数据包选择路由 | IP、ICMP、RIP |
链路层 | 传输有地址的帧以及错误检测功能 | PPP、ARP、RARP |
以二进制数据形式在物理媒体上传输数据 | ISO2110、IEEE802 |
- TCP:面向连接的、可靠的流协议
- UDP:面向无连接的通讯协议
- IP:源地址和目的地址之间传送的数据包
- ICMP:控制报文协议
- IGMP:Internet组管理协议
- ARP:地址解析协议
- RARP:反向地址转化协议
端口号
端口号规定为16位,即允许一个IP主机有2的16次方65535个不同的端口。其中
- 0~1023:分配给系统的端口号
- 1024~49151:登记端口号,主要是让第三方应用使用
- 49152~65535:短暂端口号,是留给客户进程选择暂时使用,一个进程使用完就可以供其他进程使用。
Socket使用时,可以用1024~65535的端口号
三次握手和四次挥手
三次握手
-
为什么是三次握手
TCP是面向连接的,所以需要双方都确认连接的建立。三次握手目的是交换tcp通信的初始序号,如果是两次握手,只能交换一方的tcp通信序号,四次又属于浪费
image.png - 第一次握手
客户端请求建立连接,发送SYN包,并进入SYN_SENT状态,等待服务器确认 - 第二次握手
服务端应答客户端,并请求建立连接,服务器进入SYN_RECV状态 - 第三次握手
客户端进入ESTABLISHED,并针对服务端请求确认应答,发送完毕,服务器进入ESTABLISHED
三次握手的漏洞
syn洪泛攻击
- 定义
通过网络服务所在的端口发送大量伪造原地址的攻击报文,发送到服务端,造成服务端上的半开连接队列被占满,从而阻止其他用户进行访问 - 原理
攻击者客户端利用伪造的IP地址向服务端发出请求(第一次握手),而服务端的响应(第二次握手)的报文将永远发送不到真实的客户端,服务端在等待客户端的第三次握手(永远都不会有的),服务端在等待这种半开的连接过程中消耗了资源,如果有成千上万的这种连接,主机资源将被耗尽,从而达到攻击的目的 - 解决方案
1.无效连接监控释放
2.延缓TCB分配方法
3.防火墙
四次挥手
- 断开一个TCP连接时,需要客户端和服务端总共发送4个以确认连接的断开
-
为什么需要四次挥手
TCP是双全工(客户端和服务器端可以相互发送和接收请求),所以需要双方都确认关闭连接
image.png - 第一次挥手:客户端发送关闭请求
- 第二次挥手:服务器响应客户端关闭请求
- 第三次挥手:服务端发送关闭请求
- 第四次挥手:客户端发送关闭请求
TCP/IP中的数据包
滑动窗口
- 发送方和接收方都会维护一个数据帧的序列,这个序列被称为窗口
- 发送方的窗口大小由接收方确认
目的
- 确保数据不丢失
如果发送的数据丢失了可以重新发送 - 控制发送速度
防止接收方的缓存不够大导致溢出,同时控制流量也可以避免网络堵塞
HTTP协议
- HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议
Http的工作方式
1、浏览器
用户输入地址后回车或者点击链接->浏览器拼装Http报文并发送请求给服务器->服务器处理请求后发送响应报文给浏览器->浏览器解析响应报文并使用渲染引擎显示到界面
2、手机App
用户点击或界面自动触发联网需求->android代码调用拼装HTTP报文并发送请求到服务器->服务器处理请求后发送响应报文给手机->Android代码处理响应报文并作出响应处理(如存储数据、加工数据、显示数据到界面)
URL
URL格式
- schema协议: http/https/ftp.
- host: web服务器的ip地址或者域名
- port: 服务端端口, http默认访问的端口是80
- path: 资源访问路径
- query-string: 查询参数
public static void main(String[] args) throws MalformedURLException {
URL url = new URL("https://www.baidu.com:443");
URL relativeUrl = new URL(url, "/s?wd=peakmain&rsv_spt=1#name=peakmain1");
System.out.println("协议:" + relativeUrl.getProtocol());
System.out.println("主机:" + relativeUrl.getHost());
System.out.println("端口:" + relativeUrl.getPort());
System.out.println("文件路径:" + relativeUrl.getPath());
System.out.println("相对路径:" + relativeUrl.getRef());
System.out.println("查询的字符串:" + relativeUrl.getQuery());
}
HTTP请求的传输过程
image.png一次完整http请求过程
首先进行DNS域名解析
- 三次握手建立TCP连接
- 客户端向服务器发送请求命令:get/www.baidu.com/http/1.1
- 客户端发送请求头信息
- 服务器应答Http/1.1 200 ok
- 返回响应头信息
- 服务器向客户端发送数据
- 服务器关闭TCP连接
报文格式
请求报文
image.png
响应报文
image.png
请求方式
1、GET
- 用于获取资源
- 对服务器数据不进行修改
- 不发送body,
GET /article/list/0/json HTTP/1.1
Host:www.wanandroid.com
对应的retortfit
@GET("/article/list/{page}/json")
fun getArticleList(@Path("page") page: Int): Observable<BaseEntity<Any>>
2、POST
- 用于增加或修改资源
- 发送给服务器内容写在body
POST /user/login
Host: www.wanandroid.com
content-type: application/json;charset=UTF-8
name=peakmain&password=123456
对应的retorfit
@POST("/user/login")
@FormUrlEncoded
fun login(
@Field("username") username: String?,
@Field("password") password: String?
): Observable<BaseEntity<Any>>
3、PUT
- 用于修改资源
- 发送服务器的内容写在body内容
PUT /user/login
Host: www.wanandroid.com
content-type: application/json;charset=UTF-8
name=peakmain&password=123456
对应的retorfit
@PUT("/user/login")
@FormUrlEncoded
fun login(
@Field("username") username: String?,
@Field("password") password: String?
): Observable<BaseEntity<Any>>
4、DELETE
- 用于删除资源
- 不发送body
5、HEAD
- 和GET使用方法完全相同
- 和GET唯一区别在于,返回的响应中没有body
这里需要注意,只有post请求不是幂等,其他请求都是幂等
Status Code状态码
三位数字,⽤于对响应结果做出类型化描述(如「获取成功」「内容未找到」)
- 1xx:临时性消息。如:100 (继续发送)、101(正在切换协议)
- 2xx:成功。最典型的是 200(OK)、201(创建成功)
- 3xx:重定向。如 301(永久移动)、302(暂时移动)、304(内容未改变)
- 4xx:客户端错误。如 400(客户端请求错误)、401(认证失败)、403(被禁⽌)、404(找不到内容)
- 5xx:服务器错误。如 500(服务器内部错误)
Headers
-
Cache-Control:指定请求和响应遵循缓存机制
- max-age:缓存的内容将在xx秒后失效
- private:客户端可以缓存
- public:客户端和代理服务器都可缓存
- no-cache:指示请求不能缓存
-
Connection:表示是否需要持久连接,Http 1.1默认是持久连接
-
Accept-Encoding: 客户端接受的压缩编码类型。如 gzip
-
Range:可以请求实体的一个或者多个子范围
- 表示头500个字节:bytes=0-499
- 表示第二个500字节:bytes=500-999
- 表示最后500个字节:bytes=-500
- 表示500字节以后的范围:bytes=500-
- 第一个字节和最后一个字节:bytes=0-0,-1
-
Accept-Charset: 客户端接受的字符集。如 utf-8
-
Content-Encoding:压缩类型。如 gzip
-
Content-Type:指定Body的类型。主要类型有四类
- text/html
- x-www-form-urlencoded
- multitype/form-data
- application/json,image/jpeg,application/zip...
-
Content-Length:指定body的长度字节
http缓存机制和原理
image.png-
对比缓存:缓存标识(Last-Modified/If-Modified-Since)发送到服务器进行比较,如果缓存一样,返回304,如果变了就返回数据
- 服务器返回数据的时候,会返回Last-Modified,下次客户端请求,服务器通过If-Modified-Since进行时间对比(If-Modified-Since是否大于Last-Modified,大于等于则表示资源修改过,没有则返回304)
-
Etag 主要为了解决 Last-Modified 无法解决的一些问题。当前资源在服务器的唯一标识UUID(生成规则由服务器规定)
-
客户端第一次请求的时候,服务器会返回Etag,下次请求客户端会发送一个if-None-Match,服务器会比较Etag和If-None-Match,如果两个相同,则返回304,否则返回200
-
强制缓存:
- Expires:服务器返回给客户端的到期时间,Http1.0
- Cache-Control:Http1.1 弥补Expires的缺陷而设计的
Http/Https
Http | Https |
---|---|
数据都是未加密的,也就是明文 | 对Http协议传输的数据进行加密(SSL),保证会话过程中的安全性 |
TCP端口默认为:80 | TCP端口默认为443 |
SSL协议加密方式
SSL协议用到对称加密和非对称加密(公钥加密),在建立传输链路时,SSL首先对对称加密的秘钥使用公钥进行非对称加密,链路建立好之后,SSL对传输内容使用对称加密
对称加密 | 非对称加密 |
---|---|
使⽤公钥对数据进⾏加密得到密⽂;使⽤私钥对数据进⾏解密得到原数据 | 通信双⽅使⽤同⼀个密钥,使⽤加密算法配合上密钥来加密,解密时使⽤加密过程的完全逆过程配合密钥来进⾏解 |
速度快,可加密内容较大 | 加密速度慢,能提供更好的身份认证技术 |
DES、AES | RSA、背包算法 |
窃听风险/篡改风险/冒充风险 |
Https单向认证
image.pngHttps双向认证
image.pngOKHttp
优点
- 支持HTTP/2并允许对同一主机的所有请求共享一个套接字
- 通过连接池,减少了请求延迟
- 默认通过GZip压缩数据
- 响应缓存,避免了重复请求的网络
- 请求失败自动重试主机的其他ip,自动重定向
调用流程
image.png- 分发器:内部维护队列与线程池,完成请求调配;
- 拦截器:五大默认拦截器完成整个请求过程
分发器
异步请求工作流程
什么条件加入到runningAsyncCalls
void enqueue(AsyncCall call) {
synchronized (this) {
readyAsyncCalls.add(call);
}
promoteAndExecute();
}
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.
i.remove();
executableCalls.add(asyncCall);
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}
return isRunning;
}
- 首先会将AsyncCall加入到readyAsyncCalls中
- 遍历所有readyAsyncCalls,当runingAsyncCalls大于64或者统一域名正在请求的个数大于5就直接返回
- 当满足条件,将AsyncTask加入到runningAsyncCalls并加入executableCalls集合中
- 将任务放到线程池里面进行执行
Ready什么条件加入running
@Override protected void execute() {
boolean signalledCallback = false;
timeout.enter();
try {
Response response = getResponseWithInterceptorChain();
} finally {
client.dispatcher().finished(this);
}
}
}
void finished(AsyncCall call) {
finished(runningAsyncCalls, call);
}
private <T> void finished(Deque<T> calls, T call) {
Runnable idleCallback;
synchronized (this) {
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
idleCallback = this.idleCallback;
}
boolean isRunning = promoteAndExecute();
if (!isRunning && idleCallback != null) {
idleCallback.run();
}
}
- 首先从runningAsyncCalls中移除AsyncCall
- 之后又走到promoteAndExecute方法,继续从readyAsyncCalls取出数据进行处理
线程池的execute
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务加入队列
- 如果队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还要创建线程运行这个任务
- 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常
OkHttp的线程池
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
- 核心线程数为0,最大核心线程数为Integer.MAX_VALUE,代表永不会缓存线程
- 60, TimeUnit.SECONDS代表当一个线程无事可做,超过一定的时间(60s),线程会判断,如果当前运行的线程数大于corePoolSiz,那么这个线程就会被停掉,所以线程池所有的任务完成后,它最终会收缩到corePoolSize的大小
- Okhttp的线程池工作行为:无等待,最大并发
队列 | 特点 |
---|---|
ArrayBlockingQueue | 基于数组的阻塞队列,初始化需要指定固定的大小 |
LinkedBlockingQueue | 基于链表实现的阻塞队列,初始化可以指定大小,也可以不指定 |
SynchronousQueue | 无容量队列 |
五大拦截器
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
执行流程
- 重试拦截器判断用户是否取消了请求,在获得了结果之后,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器
- 桥接拦截器负责将HTTP协议必备的请求头加入其中并添加一些默认的行为(如:GZIP压缩),在获得结果后,调用保存cookie接口并解析GZIP数据
- 缓存拦截器,读取并判断是否使用缓存,获得结果后判断是否缓存
- 连接拦截器负责找到或者新建一个连接,并获得对应的socket流,在获得结果后不进行额外的处理
- 请求服务器拦截器进行真正的服务器的通信,向服务器发送数据,解析读取的响应数据
一、重定向与重试拦截器
1、重试的判定
@Override public Response intercept(Chain chain) throws IOException {
try {
//请求出现了异常,那么releaseConnection 依然为true
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
//连接未成功,请求还没有发出去
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getFirstConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
//请求发出去了,但是和服务器通信失败了(socket流正在读写数据的时候断开连接)
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
//
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
}
private boolean recover(IOException e, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
// 1、在配置OkHttpClient时设置了不允许重试(默认允许),则一旦发送请求失败不再重试
if (!client.retryOnConnectionFailure()) return false;
// requestSendStarted 只存在于HTTP2,暂不考虑
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
// 2、判断是不是属于重试的异常
if (!isRecoverable(e, requestSendStarted)) return false;
// 3、是否存在更多的路由,比如设置了代理,DNS解析可能存在多个IP
if (!streamAllocation.hasMoreRoutes()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
//1、是不是协议的异常
if (e instanceof ProtocolException) {
return false;
}
// 2、是不是Socket超时异常
if (e instanceof InterruptedIOException) {
return e instanceof SocketTimeoutException && !requestSendStarted;
}
// 3、是不是SSL格式异常
if (e instanceof SSLHandshakeException) {
if (e.getCause() instanceof CertificateException) {
return false;
}
}
//4、是不是SSL证书异常
if (e instanceof SSLPeerUnverifiedException) {
return false;
}
return true;
}
image.png
2、重定向的判定
@Override public Response intercept(Chain chain) throws IOException {
try {
followUp = followUpRequest(response, streamAllocation.route());
} catch (IOException e) {
streamAllocation.release();
throw e;
}
}
private Request followUpRequest(Response userResponse, Route route) throws IOException {
switch (responseCode) {
case HTTP_PROXY_AUTH:
//407
Proxy selectedProxy = route != null
? route.proxy()
: client.proxy();
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
}
//加入身份验证的Request
return client.proxyAuthenticator().authenticate(route, userResponse);
case HTTP_UNAUTHORIZED:
//401
return client.authenticator().authenticate(route, userResponse);
case HTTP_PERM_REDIRECT:
case HTTP_TEMP_REDIRECT:
//307或者308,不是GET而且不是HEAD直接返回null
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
// 300、301、302、303
if (!client.followRedirects()) return null;
//获取Location
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
if (url == null) return null;
if (!sameScheme && !client.followSslRedirects()) return null;
return requestBuilder.url(url).build();
case HTTP_CLIENT_TIMEOUT:
//408是否请求超时
//设置允许自动重试
if (!client.retryOnConnectionFailure()) {
return null;
}
//是不是响应408的重试结果
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
//未响应Retry-After
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
return null;
}
//Retry-After是否大于0
if (retryAfter(userResponse, 0) > 0) {
return null;
}
return userResponse.request();
case HTTP_UNAVAILABLE:
//503
//是不是响应503的重试结果
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
return null;
}
//retryAfter是否等于0
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
return userResponse.request();
}
return null;
default:
return null;
}
}
响应码 | 说明 | 重定向条件 |
---|---|---|
407 | 代理需要授权,如付费代理,需要验证身份 | 通过proxyAuthenticator获取到Request,例: 添加 Proxy-Authorization 请求头 |
401 | 服务器需要授权(不安全) | |
300、301、302、303、307、308 | 重定向响应 | 307和308必须要为GET/HEAD请求再继续判断 1、用户允许自动重定向(默认允许) 2、能够获得Location响应头,并且值为有效url 3、如果重定向需要http和https间切换,需要允许(默认允许) |
408 | 请求超时 | 1、用户允许自动重试 2、本次请求的结果不是响应408的重试结果 3、服务器未响应Retry-After(稍后重试)或者响应Retry-After:0 |
503 | 服务不可用 | 1、本次请求的结果不是响应503的重试结果 2、服务器明确响应Retry-After:0,立即重试 |
二、桥接拦截器
补全请求头
请求头 | 说明 |
---|---|
Content-Type | 请求体类型,如:application/x-www-form-urlencoded |
Content-Length/Transfer-Encoding | 请求体解析方式 |
Host | 请求的主机站点 |
Connection:Keep-Alive | 默认保持长连接 |
Accept-Encoding:gzip | 接收机响应体使用gzip压缩 |
Cookie | Cookie身体识别 |
User-Agent | 用户信息,如操作系统,浏览器等 |
得到响应:
- 读取Set-Cookie响应头并调用接口告知用户,在下次请求则会读取对应的数据设置进入请求头,默认CookieJar无法实现
- 响应头Content-Encoding为gzip,使用GzipSource包装便于解析
三、缓存拦截器
拦截器通过CacheStrategy判断使用缓存或发起网络请求。此对象中的networkRequest与cacheResponse分别代表需要发起请求或者直接使用缓存
networkRequest | cacheResponse | 说明 |
---|---|---|
NULL | Not NULL | 直接使用缓存 |
Not NULL | NULL | 向服务器发送请求 |
NULL | NULL | 返回504 |
Not NULL | Not NULL | 发起请求,若得到304无修改,更新缓存响应并返回 |
@Override public Response intercept(Chain chain) throws IOException {
//1、通过url的md5数据,从文件缓存查找(get请求才有缓存)
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//缓存策略:根据各种条件组成请求头
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
//网络请求的Request
Request networkRequest = strategy.networkRequest;
//缓存的Response
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
//直接返回504
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// 没有网络,直接使用缓存
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
if (cacheResponse != null) {
//networkRequest和cacheResponse 都不为空
//如果networkResponse等于304则更新缓存并返回
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
return response;
}
private CacheStrategy getCandidate() {
// 1、没有缓存,进行网络请求
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 2、Https请求,但是没有握手信息,进行网络请求
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 3、主要通过响应码以及头部缓存控制字段判断响应能不能缓存,不能缓存就进行网络请求
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
//4、如果请求包含cacheControl:no-cache,需要与服务器验证缓存有效性
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//5、如果缓存响应中Cache-Control:immutable,响应内容一直不会改变,可以使用缓存
CacheControl responseCaching = cacheResponse.cacheControl();
//6、根据缓存响应的控制缓存的响应头,判断是否允许使用缓存
//6.1、获得缓存响应从创建到现在的时间
long ageMillis = cacheResponseAge();
//6.2获取这个响应有效缓存的时长
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
//如果请求指定了max-age表示指定了资源缓存的最大时间
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
//6.3请求包含Cache-Control:min-fresh表示用户认为这个缓存有效的时长,假设本身缓存的新鲜度为:100ms,而缓存的最小新鲜度为10ms,那么缓存真正有效时间为90ms
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
//6.4判断缓存的响应包含Cache-Control:must-revalidate(不可用过期资源),获得用户请求头包含Cache-Control:max-stal=[秒]缓存过期后仍有效的时长
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//6.5判断换粗是否有效
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
//7、缓存过期处理
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
image.pngnetworkReuqest存在则优先发起网络请求,否则使用cacheResponse缓存,若都为NULL则请求失败
四、连接拦截器
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
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) {
}
}
}
}
}
};
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
// Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
// If the connection is ready to be evicted, we're done.
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside
// of the synchronized block).
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we run again.
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
cleanupRunning = false;
return -1;
}
}
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
- 新建连接,放入连接池
- 启动清理任务,开始定时清理任务
- 清除无用连接
五、请求服务拦截器
一般出现上传大容量请求体或者需要验证,代表需要先询问服务器是否接受发送请求体数据
Okhttp的做法
- 如果服务器允许则返回100,客户端继续发送请求体
- 如果服务器不允许则直接返回给用户
- 同时服务器也可能会忽略此请求头,一直无法读取应答,此时抛出超时异常
网友评论