Android网络通信的一些见解

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 readyAsyncCalls = new ArrayDeque<>();
Deque runningAsyncCalls = new ArrayDeque<>();
Deque 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操作。

Android网络通信的一些见解_第1张图片


6. okio的看法理解

概述

okio是对Javaio的优化封装,解决一些Javaio的弊端,对于io来讲无非就是围绕着输入输出来讲的。在okio中,我们用sink、source分别代表写、读。当我们层层去查看sink、source的源码时,我们可以知道,它的简单结构是这样的。
Android网络通信的一些见解_第2张图片

Android网络通信的一些见解_第3张图片

我们进行的数据操作实际上来讲是在Buffer类上进行操作的,Buffer里面包含了什么呢?okio把数据拆成若干个segment数据,然后通过双向链表的方式将这些所有的数据连接起来,那么这样,Buffer只需要记住一个segment和它的上一个、下一个segment位置,Buffer就能获取所有的数据了。

对Segment而言,它才是真真正正存放数据的类,并且它是双向链表的结构,所以我们看一下segment的这些操作。

Segment

  • pop(),移除当前的segment;
  • push(),添加一个segment到当前节点之后;
  • writeTo(),满足一定的条件后正常写入,条件比如它是自己的数据且不是共享数据,当然,数据是自己的时必定不是共享数据。
  • compact(),压缩机制,一个优化的手段,主题意思就是说,如果当前节点的前一个节点有足够的空间来存放当前节点的数据,那么我们就可以将当前节点的数据通过writeTo()方法写入到前一个节点中,并且将当前节点回收;
  • split(),共享机制,它是优化复制数据操作的,如果我们想要复制当前节点的前10位数数据,我需要新创建一个segment,再将数据写入到新的segment上去。共享机制用了这样的方法,我要辅助节点上的一段数据,那我就把该节点一分为二,需要复制的数据拿出来单独创建一个节点,剩下的数据还作为这个节点存在,这样的话,在复制数据时我们相当于用移动数据来替代写入数据,这样可以提高效率。

SegmentPool

是一个Segment组成的单链表,SegmentPool的作用就是管理多余的Segment,不直接丢弃废弃的Segment,等客户需要Segment的时候直接从池中获取一个对象,避免了重复创建新兑现,提高资源利用率。

BufferString

它实际上就是一个对String进行封装的类。

Okio的超时机制

同步超时机制、异步超时机制。

okio的优雅之处

  • 它对数据进行了分块处理(Segment),这样在大数据IO的时候可以以块为单位进行IO,这可以提高IO的吞吐率;
  • 它对这些数据块使用链表来进行管理,这可以仅通过移动指针就进行数据的管理,而不用真正的处理数据,而且对扩容来说十分方便;
  • 闲置的块进行管理,通过一个块池(SegmentPool)的管理,避免系统GC和申请byte时的zero-fill。其他的还有一些小细节上的优化,比如如果你把一个UTF-8的String转化为ByteString,ByteString会保留一份对原来String的引用,这样当你下次需要decode这个String时,程序通过保留的引用直接返回对应的String,从而避免了转码过程;
  • 他为所有的Source、Sink提供了超时操作,这是在Java原生IO操作是没有的。
  • okio它对数据的读写都进行了封装,调用者可以十分方便的进行各种值(Stringg,short,int,hex,utf-8,base64等)的转化。

你可能感兴趣的:(Android)