1. TCP协议三次握手的意义
关于TCP三次握手的理解:防止已失效的连接请求突然到达。我们来举个例子,为什么需要握手三次而不是两次。
我们来想象这样一个场景,小明去饭馆吃饭,去的时间比较晚已经过了饭点了。
- 第一次握手,因为已经过了饭点,所以小明需要询问一下现在还营不营业了,所以小明问了服务员王小二,麻烦你问一下老板你们现在还营不营业了,王小二听罢转身去问老板去了,这是第一握手;
- 第二次握手,老板一听,呀这虽然过了饭点了但是有钱谁不挣啊,告诉王小二去和小明说正常营业;
- 第三次握手:王小二告诉小明正常营业,这时小明说了好我现在就开始点菜了,然后老板通知后厨等着做菜,小明说我要吃红烧鱼,土豆丝,这样的话这笔买卖就成了;
同样是上面的场景,我们来稍微变化一下细节
- 第一次握手,小明还是需要王小二去询问一下老板营不营业了,王小二在去询问老板的途中接到了个紧急电话,光顾着打电话呢一下过了一个小时才想起有个顾客小明呢,这时候才火急火燎地过去问老板;
- 第二次握手:和上面一样,老板还是觉得有钱就挣,告诉王小二去和小明说正常营业;
- 第三次握手:王小二得到答复慌忙跑过去告诉小明,谁知道时间太长了小明等不及已经走了,那好吧既然顾客走了,那大家下班吧,厨师不用等了;
这时候如果是两次握手,没有第三步,老板不管小明还在不在店里了在第二次握手时就会通知厨师等着做饭,厨师估计等个很久也没人点菜,这不浪费资源么,明明可以回家happy的,所以三次握手的关键就在于,它可以应变连接请求是否失效。
2. TCP协议四次挥手
TCP协议需要断开连接时遵循四次挥手的原则,我们抛开专业术语而只是描述四次挥手的过程,我们仍旧拿三次握手中的点菜来举例。
三次握手后,小明开始点菜了,当点完菜后出现这种场景:
- 第一次挥手,小明告诉老板,我的菜点完了,我们不需要保持沟通了;
- 第二次挥手,老板告诉小明,恩我知道你点完菜了,但是之前你点的菜我们还没有做好,所以暂时我们还要保持沟通,你安心等着呗;
- 第三次挥手,厨师把菜做完了,老板告诉小明我们把菜给你上完了,我们不需要沟通了;
- 第四次挥手,小明说那就这样吧,不需要沟通了,小明等了一会老板没有回应,小明便知道大家下班了,我开始好好吃饭了。
3. okhttp同步请求的源码过程分析
对于okhttp的同步请求,我们很清晰地知道是这样的用法:
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("http://www.baidu.com")
.build();
Response response = client.newCall(request).execute();
我们可以看到调用了execute()方法,我们来一一分析:
Step1
首先调用了OkHttpClient的newCall()方法来产生一个接口Call;
Step2
我们查看newCall的源码发现newCall实际上产生了一个RealCall,RealCall是Call的实现类,所以实际上同步请求调用了RealCall的execute()方法;
@Override
public Call newCall(Request request) {
return new RealCall(this, request, false);
}
step3
通过源码我们发现client.dispatcher().executed(this);这条语句,实际上RealCall的execute()方法最终执行了client.dispatcher()的executed()方法。client.dispatcher()是一个Dispatcher对象。
@Override
public Response execute() throws IOException {
captureCallStackTrace();
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} finally {
client.dispatcher().finished(this);
}
}
step4
Dispatcher类是什么呢?我们通过它的名字”调度员”,和一系列的参数可以看出一些端倪,它应该就是一个用来分配各个请求如何进行工作的吧。
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
private Runnable idleCallback;
ExecutorService executorService;
Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
对于同步请求来讲,它最终调用了Dispatcher类的executed()方法。
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
可以看到我们将call这个任务放入到了一个队列中去,这个队列中的任务会被外部某个地方调用来执行请求操作,当一个Call请求任务执行完之后,外部会调用Dispatcher类的finished()方法,finished()方法来看看还有没有请求需要被执行了。
4. okhttp异步请求的源码过程分析
OKhttp异步的网络请求和同步网络请求前三步基本一致,只是同步请求调用execute()方法,异步调用enqueue()方法,
Step1、Step2、Step3省略…
Step4
同步的时候我们不管如何,可以直接将任务添加到正在执行的队列中去,但是对于异步请求来讲,我们需要考虑最大并发数量,所以这时候需要用两个标准去判别是否到达可以立马执行的情况。OKhttp中用当前正在并发的请求不能超过64且同一个地址的访问不能超过5个来表示可以立即进入线程池执行。
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
Step5
在这里我们分两种情况:
- 5.1.1,通过Step4的判断我们可以直接入队,将任务加入到runningAsyncCalls中去,然后通过线程池分配线程执行,AsyncCall 中的execute()方法可以执行;
- 5.1.2,在AsyncCall的execute()方法中getResponseWithInterceptorChain(),进入拦截器链流程,然后进行请求,获取Response。
- 5.1.3,如果是正常的获取到Response,执行finished()方法,去检测等待队列中是否有任务需要执行;
- 5.2.1,通过Step4的判断我们需要等待不能入队,我们call加入到readyAsyncCalls队列中去;
- 5.2.2,如果有任务执行完毕,触发finished()方法时,调度员会重新分配等待队列中的任务,如果有空闲则进入到5.1.1步骤。
5. okhttp调度的总结
- 采用Dispacher作为调度,与线程池配合实现了高并发,低阻塞的的运行;
- 采用Deque作为集合,按照入队的顺序先进先出;
- 最精彩的就是在try/catch/finally中调用finished函数,可以主动控制队列的移动。避免了使用锁而wait/notify操作。
