okHttp框架分析--GET和POST请求

概要

我们平时使用最多的请求方式莫过于GETPOST请求,它们几乎完成我们平时所需的基本任务,下面我们分析一下它们的区别以及在OkHttp中的调用流程

GET和POST区别

    1. GETPOST请求都是的HTTP协议都是基于TCP/IP的应用层协议,它们使用的是同一个传输层协议,所以在传输上相同
    1. GET请求有字符长度限制POST请求没有,浏览器通常都会限制URL长度在2K个字节,而(大多数)服务器最多处理64K大小的URL;主要原因来自于浏览器服务器,由于GET请求是明文传输,在URL中可在?后携带多个参数并以&分割,为避免URL消耗资源过多,同时也为了[性能]和[安全]考虑所以为URL长度进行了限制;而POST请求会将敏感数据封装进Request body中,所以不会造成URL过长现象
    1. GET只接受ASCII字符,而POST没有限制
    1. GET请求只能进行URL编码,而POST支持多种编码方式
    1. GETPOST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息,若想保证传输安全,可通过HTTPS进行加密
    1. GET请求产生一个TCP数据包;POST产生两个TCP数据包;对于GET方式的请求,浏览器会把HeaderData一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送Header,服务器响应100 continue,浏览器再发送Data,服务器响应200(返回数据)

GET源码解析

GET同步流程解析

GET流程图.jpg

[ 1 ]外部操作

public class GetExample {
  //声明OkHttpClick实例​
  OkHttpClient client = new OkHttpClient();
  String run(String url) throws IOException {
     //声明Request实例,组织所需元素数据​
    Request request = new Request.Builder()
        .url(url)
        .build();
    //通过​newCall得到一个RealCall实例,同时也声明了一个 Transmitter 发射器的实例
     //​调取execute()进行执行
    try (Response response = client.newCall(request).execute()) {
      return response.body().string();
    }
  }

[ 2 ] 进入RelCall.kt ,执行 @方法 execute()

override fun execute(): Response {
  synchronized(this) {
    check(!executed) { "Already Executed" }
    executed = true
  }
//设置超时​
  transmitter.timeoutEnter()
  //此方法是个‘钩子’,若在外部没有具体实现,则不执行任何操作​
  transmitter.callStart()
  try {
    //将当前的realCall存入runningSyncCalls链表​
    client.dispatcher().executed(this)
    //调取拦截链,实现数据访问,返回一个Response对象​ 
    return getResponseWithInterceptorChain()
  } finally {
    client.dispatcher().finished(this)
  }
}

[3] 调取拦截链的实现我们不在此处过多赘述,详情请见我的另一篇文章okHttp框架分析--拦截链

GET异步请求

[1]外部操作

public final class AsynchronousGet {
 //OkHttp客户端实例​
  private final OkHttpClient client = new OkHttpClient();
  public void run() throws Exception {
    //请求实例​
    Request request = new Request.Builder()
        .url("https://raw.github.com/square/okhttp/master/README.md") //url网址配置
        .build();
   //[ 2 ] client new一个RealCall,调用enqueue​函数进入队列,并实现回调操作
    client.newCall(request).enqueue(new Callback() {
     //失败回调实现​
      @Override public void onFailure(Call call, IOException e) {
        e.printStackTrace();  //打印访问服务失败的异常信息
      }
     //成功回调实现​
      @Override public void onResponse(Call call, Response response) throws IOException {
        ...
        //todo 访问服务器成果后的具体操作​
    });
  }

[ 2 ] client声明一个RealCall类型的对象,调用enqueue(...)​函数进入队列,并实现回调操作

override fun enqueue(responseCallback: Callback) {
  synchronized(this) {
    check(!executed) { "Already Executed" }
    executed = true
  }
 //发射器开始调用,此处为一个空操作,可在外部进行操作重写​
  transmitter.callStart()
//[ 3 ]client事件分发处理,获取一个Dispatcher对象,
//继续调用enqueue函数,传入回调Callback包装成AsyncCall类型
  client.dispatcher().enqueue(AsyncCall(responseCallback))
}

[ 3 ]client事件分发处理,获取一个Dispatcher类型对象,继续调用enqueue(...)函数,传入回调Callback包装成AsyncCall类型

internal fun enqueue(call: AsyncCall) {
  synchronized(this) {
     //将我们的callback回调添加进准备异步调用的队列​
    readyAsyncCalls.add(call)
    //修改AsyncCall,使其共享现有运行调用的AtomicInteger 相同的主机
    if (!call.get().forWebSocket) {
      val existingCall = findExistingCallWithHost(call.host())
      if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
    }
  }
 //[ 4  ] 将符合条件的调用从[readyAsyncCalls]提升到[runningAsyncCalls],并在线程池执行。
//​必须不同步调用,因为执行调用可以调用在用户代码​
  promoteAndExecute()
}

[ 4 ] 将符合条件的调用从readyAsyncCalls链表提升到runningAsyncCalls链表,并在线程池执行。执行方法 promoteAndExecute()

private fun promoteAndExecute(): Boolean {
 //断言 当先线程是否持有Lock锁
​  assert(!Thread.holdsLock(this)) 
  //声明私有的一个执行回调队列​
  val executableCalls = ArrayList<AsyncCall>()
  val isRunning: Boolean
  synchronized(this) {
    val i = readyAsyncCalls.iterator()
    while (i.hasNext()) {
      val asyncCall = i.next()
      if (runningAsyncCalls.size >= this.maxRequests) break // 运行异步调用最大容量
      if (asyncCall.callsPerHost().get() >= this.maxRequestsPerHost) continue // 主机调用最大容量
      i.remove() 
      //此处为原子操作,将主机的调用个数增加 1 ​
      asyncCall.callsPerHost().incrementAndGet()  
     //将我们的Callback添加到执行​回调队列
      executableCalls.add(asyncCall) 
       //将我们的Callback添加到异步运行回调队列​
      runningAsyncCalls.add(asyncCall)
    }
     //正在运行的异步回调和同步回调个数相加​是否大于 0
    isRunning = runningCallsCount() > 0
  }
 //循环​私有执行回调队列
  for (i in 0 until executableCalls.size) {
    val asyncCall = executableCalls[i]
     //[ 5 ]​ 尝试在“executorService”上注册此异步调用,获取一个线程池作为参数
    asyncCall.executeOn(executorService())
  }
  return isRunning
}

[ 5 ]​ 尝试在executorService上注册此异步调用,获取一个线程池作为参数,执行方法asyncCall.executeOn(executorService())

fun executeOn(executorService: ExecutorService) {
   //判定当前线程是否持有Lock锁​
  assert(!Thread.holdsLock(client.dispatcher()))
  var success = false
  try {
    //[ 6 ] 执行线程​
    executorService.execute(this)
    success = true
  } catch (e: RejectedExecutionException) {
    val ioException = InterruptedIOException("executor rejected")
    ioException.initCause(e)
    transmitter.noMoreExchanges(ioException)
    //执行发生异常则回调 onFailure函数​
    responseCallback.onFailure(this@RealCall, ioException)
  } finally {
    if (!success) {
      client.dispatcher().finished(this) // This call is no longer running!
    }
  }
}

[ 6 ]最终还是进入方法execute(), 执行线程操作发起对服务器的请求操作

  override fun execute() {
    var signalledCallback = false
    ​//发射器判断超时设置
    transmitter.timeoutEnter()
    try {
     //​/调取拦截链,实现数据访问,返回一个Response对象​ 
      val response = getResponseWithInterceptorChain()​
      signalledCallback = true
      //访问成功则调取回调接口 onResponse​函数
      responseCallback.onResponse(this@RealCall, response)
    } catch (e: IOException) {
      if (signalledCallback) {
        // Do not signal the callback twice!
        Platform.get().log(INFO, "Callback failure for ${toLoggableString()}", e)
      } else {
        //​访问发生异常则 回调接口 onFailure函数
        responseCallback.onFailure(this@RealCall, e)
      }
    } finally {
      //最终结束本次调用​
      client.dispatcher().finished(this)
    }
  }
}

POST源码解析

POST同步流程解析

POST流程图.jpg

[ 1 ]外部操作

 //请求体类型
​public static final MediaType MEDIA_TYPE_MARKDOWN
    = MediaType.get("text/x-markdown; charset=utf-8");
//声明OkHttpClient实例​
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
 //请求体文件​
  File file = new File("README.md");
//声明一个 请求实例​
  Request request = new Request.Builder()
      .url("https://api.github.com/markdown/raw")    //url连接
      .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))  //post请求方法,传入一个请求体参数
      .build();
  //new 一个 RealCall实例,调用 @方法​[ execute() ]函数,执行数据访问
  try (Response response = client.newCall(request).execute()) {​
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
    System.out.println(response.body().string());
  }
}

[ 2 ] 此处说明一下,POST同步请求流程与GET同步请求流程一般无二,最终会在真正发起服务的拦截器CallServerInterceptor.kt做具体的操作或者在自定拦截器中对不同的请求方法做特殊处理;进入 CallServerInterceptor.kt 类,执行方法 intercept(chain: Interceptor.Chain)

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
  val realChain = chain as RealInterceptorChain
  val exchange = realChain.exchange()
  //获取请求实例​
  val request = realChain.request()    
 //获取请求体​
  val requestBody = request.body()
  ...
​  //@重点 做判定,若当前请求方法不是 GET或 HEAD 并且 请求体不为 Null 则判定成立
  if (HttpMethod.permitsRequestBody(request.method()) && requestBody != null) {
    //如果请求上有一个“Expect: 100-continue”报头,则等待一个“HTTP/1.1 100”报头 
  //在发送请求体之前继续“响应”。如果我们没有收到,返回
​  //在没有传输请求体的情况下,我们确实得到了(例如4xx响应)
​
  //在[预用知识说明]标签  ==>  { “Expect: 100-continue”的来龙去脉 }  中做详细说明​
    if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
      exchange.flushRequest()
      responseHeadersStarted = true
      //开始调用响应头​
      exchange.responseHeadersStart()
       // 读取响应头​
      responseBuilder = exchange.readResponseHeaders(true)
    }
   ​
    if (responseBuilder == null) {
      if (requestBody.isDuplex()) {
        // 准备一个双重主体,以便应用程序稍后可以发送请求主体。
        exchange.flushRequest()
        //创建一个buffer​
        val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
         //请求体写入buff中​
        requestBody.writeTo(bufferedRequestBody)
      } else {
        // 如果满足“Expect: 100-continue”期望,则编写请求主体
        val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
        requestBody.writeTo(bufferedRequestBody)
        bufferedRequestBody.close()
      }
    } else {
      exchange.noRequestBody()
      if (!exchange.connection().isMultiplexed) {
        //如果没有满足“Expect: 100-continue”期望,请阻止HTTP/1连接
      ​ //避免重复使用。否则,我们仍然有义务将请求体传输到 
       //让连接保持一致的状态。
        exchange.noNewExchangesOnConnection()
      }
    }
  } else {
     //没有请求体​
    exchange.noRequestBody()
  }
​​
​
  if (requestBody == null || !requestBody.isDuplex()) {
    //将所有缓冲数据写入底层接收器(如果存在的话)。然后这个sink是递归不断刷新
   //​将数据尽可能地推送到最终目的地。通常, 目标是一个网络套接字或文件。​
    exchange.finishRequest()
  }
  if (!responseHeadersStarted) {
    //仅在接收响应头之前调用。 
  //​ 连接是隐式的,通常与最后一个[connectionAcquired]事件相关。 
  //​这可以被调用超过1次为一个[Call]。​
    exchange.responseHeadersStart()
  }
  if (responseBuilder == null) {
    //从HTTP传输解析响应头的字节。​
    responseBuilder = exchange.readResponseHeaders(false)!!
  }
  //一个HTTP响应。
 //​该类的实例不是不可变的:响应体是一次性的
 //​只使用一次,然后关闭的值。所有其他属性都是不可变的。​
  var response = responseBuilder
      .request(request)
      .handshake(exchange.connection().handshake())
      .sentRequestAtMillis(sentRequestMillis)
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build()
  var code = response.code()  //获取响应 Code
  if (code == 100) {
    // 服务器发送了100-continue,即使我们没有请求。
   ​ //再读一遍实际的响应
    response = exchange.readResponseHeaders(false)!!
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build()
    code = response.code()
  }
   //在接收响应标头后立即调用​
  exchange.responseHeadersEnd(response)
  response = if (forWebSocket && code == 101) {
    //连接叠积,但我们需要确保拦截器看到非空响应体。
    response.newBuilder()
        .body(Util.EMPTY_RESPONSE)
        .build()
  } else {
    response.newBuilder()
        .body(exchange.openResponseBody(response))
        .build()
  }
 //关闭连接​
  if ("close".equals(response.request().header("Connection"), ignoreCase = true) ||
      "close".equals(response.header("Connection"), ignoreCase = true)) {
    exchange.noNewExchangesOnConnection()
  }
 //抛出连接异常​
  if ((code == 204 || code == 205) && response.body()?.contentLength() ?: -1 > 0) {
    throw ProtocolException(
        "HTTP $code had non-zero Content-Length: ${response.body()?.contentLength()}")
  }
  //返回响应数据​
  return response
}

POST 异步请求

与GET异步操作一般无二,同样是在真正发起服务的拦截器CallServerInterceptor.kt做具体的操作,这里不在赘述

预用知识说明

  • 1)“Expect: 100-continue”的来龙去脉:
    HTTP/1.1 协议 里设计 100 (Continue) HTTP 状态码的的目的是,在客户端发送 Request Message 之前,HTTP/1.1 协议允许客户端先判定服务器是否愿意接受客户端发来的消息主体(基于 Request Headers)即, ClientServerPost (较大)数据之前,允许双方“握手”,如果匹配上了,Client 才开始发送(较大)数据。这么做的原因是,如果客户端直接发送请求数据,但是服务器又将该请求拒绝的话,这种行为将带来很大的资源开销。

协议对 HTTP/1.1 clients 的要求是:
如果 client预期等待100-continue的应答,那么它发的请求必须包含一个 Expect: 100-continue的头域!

2)libcurl 发送大于1024字节 数据时启用Expect:100-continue特性:

这也就是 Laruence在 2011 年撰文所写的:
在使用 curlPOST 的时候,当要 POST 的数据大于 1024 字节的时候,curl 并不会直接就发起 POST 请求,而是会分为两步:

    1. 发送一个请求,包含一个 Expect: 100-continue 头域,询问 Server 是否愿意接收数据;
    1. 接收到 Server 返回的 100-continue 应答以后,才把数据 POSTServer;这是 libcurl 的行为。
      This ALL! Thanks EveryBody!
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容