脑残式网络编程入门(三):HTTP协议必知必会的一些知识

本文原作者:“竹千代”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动。

1、前言

无论是即时通讯应用还是传统的信息系统,Http协议都是我们最常打交道的网络应用层协议之一,它的重要性可能不需要再强调(有鉴于此,即时通讯网整理了大量的有关http协议的文章,如有必要可从本文的相关链接处查阅)。但是实际上很多人(包括我自己),虽然每天都会跟http的代码打交道,但对http了解的并不够深入。本文就我自己的学习心得,分享一下我认为需要知道的http常见的相关知识点。

HTTP协议所处的TCP/IP协议层级:
<ignore_js_op>脑残式网络编程入门(三):HTTP协议必知必会的一些知识
(▲ 上图来自《计算机网络通讯协议关系图(中文珍藏版)[附件下载]》,您可下载此图的完整清晰版)

2、系列文章

本文是系列文章中的第3篇,本系列大纲如下:

3、Http协议报文相关知识

3.1基础概念

首先我们来点基础的,看看http报文具体的格式。http报文可以分为请求报文和响应报文,格式大同小异。

主要分为三个部分:

  • 1)起始行;
  • 2)首部;
  • 3)主体。

请求报文格式:

1
2
3
4
<method> <request-url> <version>
<headers>
 
<entity-body>

响应报文格式:

1
2
3
4
<version> <status> <reason-phrase>
<headers>
 
<entity-body>

从请求报文格式和响应报文格式可以看出,两者主要在起始行上有差异。

这里稍微解释一下各个标签:

1
2
3
4
5
<method> 指请求方法,常用的主要是Get、 Post、Head 还有其他一些我们这里就不说了,有兴趣的可以自己查阅一下
<version> 指协议版本,现在通常都是Http/1.1了
<request-url> 请求地址
<status> 指响应状态码, 我们熟悉的200、404等等
<reason-phrase> 原因短语,200 OK 、404 Not Found 这种后面的描述就是原因短语,通常不必太关注。

3.2method

我们知道请求方法最常用的有Get 和Post两种,面试时也常常会问到这两者有什么区别,通常什么情况下使用。这里我们来简单说一说。

两个方法之间在传输形式上有一些区别,通过Get方法发起请求时,会将请求参数拼接在request-url尾部,格式是url?param1=xxx¶m2=xxx&[…]。

我们需要知道,这样传输参数会使得参数都暴露在地址栏中。并且由于url是ASCII编码的,所以参数中如果有Unicode编码的字符,例如汉字,都会编码之后传输。另外值得注意的是,虽然http协议并没有对url长度做限制,但是一些浏览器和服务器可能会有限制,所以通过GET方法发起的请求参数不能够太长。而通过POST方法发起的请求是将参数放在请求体中的,所以不会有GET参数的这些问题。

另外一点差别就是方法本身的语义上的。GET方法通常是指从服务器获取某个URL资源,其行为可以看作是一个读操作,对同一个URL进行多次GET并不会对服务器产生什么影响。而POST方法通常是对某个URL进行添加、修改,例如一个表单提交,通常会往服务器插入一条记录。多次POST请求可能导致服务器的数据库中添加了多条记录。所以从语义上来讲,两者也是不能混为一谈的。

3.3状态码

常见的状态码主要有:

  • 200 OK  请求成功,实体包含请求的资源
  • 301 Moved Permanent 请求的URL被移除了,通常会在Location首部中包含新的URL用于重定向。
  • 304 Not Modified    条件请求进行再验证,资源未改变。
  • 404 Not Found       资源不存在
  • 206 Partial Content 成功执行一个部分请求。这个在用于断点续传时会涉及到。

3.4header

在请求报文和响应报文中都可以携带一些信息,通过与其他部分配合,能够实现各种强大的功能。这些信息位于起始行之下与请求实体之间,以键值对的形式,称之为首部。每条首部以回车换行符结尾,最后一个首部额外多一个换行,与实体分隔开。

这里我们重点关注一下:

01
02
03
04
05
06
07
08
09
10
Date 
Cache-Control 
Last-Modified 
Etag 
Expires 
If-Modified-Since  
If-None-Match 
If-Unmodified-Since 
If-Range 
If-Match

Http的首部还有很多,但限于篇幅我们不一一讨论。这些首部都是Http缓存会涉及到的,在下文中我们会来说说各自的作用。

3.5实体

请求发送的资源,或是响应返回的资源。

3.6更多资料

因为篇幅原因,本文只是对http的相关知识做出简要介绍,如果需要详细了解,则建议阅下以下文章:

4、Http缓存相关的知识

当我们发起一个http请求后,服务器返回所请求的资源,这时我们可以将该资源的副本存储在本地,这样当再次对该url资源发起请求时,我们能快速的从本地存储设备中获取到该url资源,这就是所谓的缓存。缓存既可以节约不必要的网络带宽,又能迅速对http请求做出响应。

先摆出几个概念:

  • 新鲜度检测;
  • 再验证;
  • 再验证命中。

我们知道,有些url所对应的资源并不是一成不变的,服务器中该url的资源可能在一定时间之后会被修改。这时本地缓存中的资源将与服务器一侧的资源有差异。

既然在一定时间之后可能资源会改变,那么在某个时间之前我们可以认为这个资源没有改变,从而放心大胆的使用缓存资源,当请求时间超过来该时间,我们认为这个缓存资源可能不再与服务器端一致了。所以当我们发起一个请求时,我们需要先对缓存的资源进行判断,看看究竟我们是否可以直接使用该缓存资源,这个就叫做新鲜度检测。即每个资源就像一个食品一样,拥有一个过期时间,我们吃之前需要先看看有没有过期。

如果发现该缓存资源已经超过了一定的时间,我们再次发起请求时不会直接将缓存资源返回,而是先去服务器查看该资源是否已经改变,这个就叫做再验证。如果服务器发现对应的url资源并没有发生变化,则会返回304 Not Modified,并且不再返回对应的实体。这称之为再验证命中。相反如果再验证未命中,则返回200 OK,并将改变后的url资源返回,此时缓存可以更新以待之后请求。

我们看看具体的实现方式。

新鲜度检测:
我们需要通过检测资源是否超过一定的时间,来判断缓存资源是否新鲜可用。那么这个一定的时间怎么决定呢?其实是由服务器通过在响应报文中增加Cache-Control:max-age,或是Expire这两个首部来实现的。值得注意的是Cache-Control是http1.1的协议规范,通常是接相对的时间,即多少秒以后,需要结合last-modified这个首部计算出绝对时间。而Expire是http1.0的规范,后面接一个绝对时间。

再验证:
如果通过新鲜度检测发现需要请求服务器进行再验证,那么我们至少需要告诉服务器,我们已经缓存了一个什么样的资源了,然后服务器来判断这个缓存资源到底是不是与当前的资源一致。逻辑是这样没错。那怎么告诉服务器我当前已经有一个备用的缓存资源了呢?我们可以采用一种称之为条件请求的方式实现再验证。

Http定义了5个首部用于条件请求:

  • If-Modified-Since
  • If-None-Match
  • If-Unmodified-Since
  • If-Range
  • If-Match

If-Modified-Since 可以结合Last-Modified这个服务器返回的响应首部使用,当我们发起条件请求时,将Last-Modified首部的值作为If-Modified-Since首部的值传递到服务器,意思是查询服务器的资源自从我们上一次缓存之后是否有修改。

If-None-Match 需要结合另一个Etag的服务器返回的响应首部使用。Etag首部实际上可以认为是服务器对文档资源定义的一个版本号。有时候一个文档被修改了,可能所做的修改极为微小,并不需要所有的缓存都重新下载数据。或者说某一个文档的修改周期极为频繁,以至于以秒为时间粒度的判断已经无法满足需求。这个时候可能就需要Etag这个首部来表明这个文档的版号了。发起条件请求时可将缓存时保存下来的Etag的值作为If-None-Match首部的值发送至服务器,如果服务器的资源的Etag与当前条件请求的Etag一致,表明这次再验证命中。

其他三个与断点续传涉及到的相关知识有关,本文暂时不讨论。待我之后写一篇文章来讲讲断点续传。

5、以Android端的OkHttp库为例来讲解http缓存的客户端具体实现

缓存的Http理论知识大致就是这么些。我们从OkHttp的源码来看看(iOS端可以读一读著名的AFNetworking库的代码),这些知名的开源库是如何利用Http协议实现缓存的。这里我们假设读者对OkHttp的请求执行流程有了大致的了解,并且只讨论缓存相关的部分。对于OkHttp代码不熟悉的同学,建议先看看相关代码或是其他文章。

我们知道OkHttp的请求在发送到服务器之前会经过一系列的Interceptor,其中有一个CacheInterceptor即是我们需要分析的代码。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
final InternalCache cache;
 
@Override public Response intercept(Chain chain) throws IOException {
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;
 
    long now = System.currentTimeMillis();
 
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
 
    ......
 
 }

方法首先通过InternalCache 获取到对应请求的缓存。这里我们不展开讨论这个类的具体实现,只需要知道,如果之前缓存了该请求url的资源,那么通过request对象可以查找到这个缓存响应。

将获取到的缓存响应,当前时间戳和请求传入CacheStrategy,然后通过执行get方法执行一些逻辑最终可以获取到strategy.networkRequest,strategy.cacheResponse。如果通过CacheStrategy的判断之后,我们发现这次请求无法直接使用缓存数据,需要向服务器发起请求,那么我们就通过CacheStrategy为我们构造的networkRequest来发起这次请求。我们先来看看CacheStrategy做了哪些事情。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
CacheStrategy.Factory.java
 
public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;
 
      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }
    }

CacheStrategy.Factory的构造方法首先保存了传入的参数,并将缓存响应的相关首部解析保存下来。之后调用的get方法如下

01
02
03
04
05
06
07
08
09
10
public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();
 
  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    // We're forbidden from using the network and the cache is insufficient.
    return new CacheStrategy(null, null);
  }
 
  return candidate;
}

get方法很简单,主要逻辑在getCandidate中,这里的逻辑是如果返回的candidate所持有的networkRequest不为空,表示我们这次请求需要发到服务器,此时如果请求的cacheControl要求本次请求只使用缓存数据。那么这次请求恐怕只能以失败告终了,这点我们等会儿回到CacheInterceptor中可以看到。接着我们看看主要getCandidate的主要逻辑。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private CacheStrategy getCandidate() {
  // No cached response.
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }
 
  // Drop the cached response if it's missing a required handshake.
  if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
  }
 
  // If this response shouldn't have been stored, it should never be used
  // as a response source. This check should be redundant as long as the
  // persistence store is well-behaved and the rules are constant.
  if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
  }
 
  CacheControl requestCaching = request.cacheControl();
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }
    ......
}

上面这段代码主要列出四种情况下需要忽略缓存,直接想服务器发起请求的情况:

  • 1)缓存本身不存在;
  • 2)请求是采用https 并且缓存没有进行握手的数据;
  • 3)缓存本身不应该不保存下来。可能是缓存本身实现有问题,把一些不应该缓存的数据保留了下来;
  • 4)如果请求本身添加了 Cache-Control: No-Cache,或是一些条件请求首部,说明请求不希望使用缓存数据。

这些情况下直接构造一个包含networkRequest,但是cacheResponse为空的CacheStrategy对象返回。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private CacheStrategy getCandidate() {
  ......
 
  CacheControl responseCaching = cacheResponse.cacheControl();
  if (responseCaching.immutable()) {
    return new CacheStrategy(null, cacheResponse);
  }
 
  long ageMillis = cacheResponseAge();
  long freshMillis = computeFreshnessLifetime();
 
  if (requestCaching.maxAgeSeconds() != -1) {
    freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
  }
 
  long minFreshMillis = 0;
  if (requestCaching.minFreshSeconds() != -1) {
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
  }
 
  long maxStaleMillis = 0;
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }
 
  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());
  }
 
    ......    
}

如果缓存响应的Cache-Control首部包含immutable,那么说明该资源不会改变。客户端可以直接使用缓存结果。值得注意的是immutable并不属于http协议的一部分,而是由facebook提出的扩展属性。

之后分别计算ageMills、freshMills、minFreshMills、maxStaleMills这四个值。

如果响应缓存没有通过Cache-Control:No-Cache 来禁止客户端使用缓存,并且:

ageMillis + minFreshMillis < freshMillis + maxStaleMillis

这个不等式成立,那么我们进入条件代码块之后最终会返回networkRequest为空,并且使用当前缓存值构造的CacheStrtegy。

这个不等式究竟是什么含义呢?我们看看这四个值分别代表什么:

  • ageMills 指这个缓存资源自响应报文在源服务器中产生或者过期验证的那一刻起,到现在为止所经过的时间。用食品的保质期来比喻的话,好比当前时间距离生产日期已经过去了多久了。
  • freshMills 表示这个资源在多少时间内是新鲜的。也就是假设保质期18个月,那么这个18个月就是freshMills。
  • minFreshMills 表示我希望这个缓存至少在多久之后依然是新鲜的。好比我是一个比较讲究的人,如果某个食品只有一个月就过期了,虽然并没有真的过期,但我依然觉得食品不新鲜从而不想再吃了。
  • maxStaleMills 好比我是一个不那么讲究的人,即使食品已经过期了,只要不是过期很久了,比如2个月,那我觉得问题不大,还可以吃。

minFreshMills 和maxStatleMills都是由请求首部取出的,请求可以根据自己的需要,通过设置:

Cache-Control:min-fresh=xxx、Cache-Control:max-statle=xxx

来控制缓存,以达到对缓存使用严格性的收紧与放松。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private CacheStrategy getCandidate() {
    ......
 
  // Find a condition to add to the request. If the condition is satisfied, the response body
  // will not be transmitted.
  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);
}

如果之前的条件不满足,说明我们的缓存响应已经过期了,这时我们需要通过一个条件请求对服务器进行再验证操作。接下来的代码比较清晰来,就是通过从缓存响应中取出的Last-Modified,Etag,Date首部构造一个条件请求并返回。

接下来我们返回CacheInterceptor。

01
02
03
04
05
06
07
08
09
10
11
12
// If we're forbidden from using the network and the cache is insufficient, fail.
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();
}

可以看到,如果我们返回的networkRequest和cacheResponse都为空,说明我们即没有可用的缓存,同时请求通过Cache-Control脑残式网络编程入门(三):HTTP协议必知必会的一些知识nly-if-cached只允许我们使用当前的缓存数据。这个时候我们只能返回一个504的响应。接着往下看,

1
2
3
4
5
6
// If we don't need the network, we're done.
if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

如果networkRequest为空,说明我们不需要进行再验证了,直接将cacheResponse作为请求结果返回。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }
 
    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      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();
 
        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        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);
      }
 
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }
 
    return response;

如果networkRequest存在不为空,说明这次请求是需要发到服务器的。此时有两种情况,一种cacheResponse不存在,说明我们没有一个可用的缓存,这次请求只是一个普通的请求。如果cacheResponse存在,说明我们有一个可能过期了的缓存,此时networkRequest是一个用来进行再验证的条件请求。

不管哪种情况,我们都需要通过networkResponse=chain.proceed(networkRequest)获取到服务器的一个响应。不同的只是如果有缓存数据,那么在获取到再验证的响应之后,需要cache.update(cacheResponse, response)去更新当前缓存中的数据。如果没有缓存数据,那么判断此次请求是否可以被缓存。在满足缓存的条件下,将响应缓存下来,并返回。

OkHttp缓存大致的流程就是这样,我们从中看出,整个流程是遵循了Http的缓存流程的。

最后我们总结一下缓存的流程:

  • 1)从接收到的请求中,解析出Url和各个首部;
  • 2)查询本地是否有缓存副本可以使用;
  • 3)如果有缓存,则进行新鲜度检测,如果缓存足够新鲜,则使用缓存作为响应返回,如果不够新鲜了,则构造条件请求,发往服务器再验证。如果没有缓存,就直接将请求发往服务器;
  • 4)把从服务器返回的响应,更新或是新增到缓存中。

6、有必要了解一下广泛使用的OAuth认证授权协议

6.1什么是OAuth?

OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。oAuth是Open Authorization的简写。

基本上现在主流的第3方登陆接口都是使用或者类似于OAuth的实现原理,比如:QQ开放给第3方的登陆API、微信登陆API、新浪微博账号登陆API等。

6.2OAuth的优点

  • 1)简单:不管是OAUTH服务提供者还是应用开发者,都很易于理解与使用;
  • 2)安全:没有涉及到用户密钥等信息,更安全更灵活;
  • 3)开放:任何服务提供商都可以实现OAUTH,任何软件开发商都可以使用OAUTH。

6.3OAuth授权流程原理

OAuth定义的几个角色:
<ignore_js_op>脑残式网络编程入门(三):HTTP协议必知必会的一些知识

OAuth授权流程基本流程如下:
<ignore_js_op>脑残式网络编程入门(三):HTTP协议必知必会的一些知识

授权流程以有道云笔记为例(如下图):
<ignore_js_op>脑残式网络编程入门(三):HTTP协议必知必会的一些知识

从上图可以看出,一个典型的OAuth授权的流程主要分为6步:

  • 1)客户端向用户申请授权;
  • 2)用户同意授权;
  • 3)客户端通过获取的授权,向认证服务器申请Access Token;
  • 4)认证服务器通过授权认证后,下发Access Token;
  • 5)客户端通过获取的到Access Token向资源服务器发起请求;
  • 6)资源服务器核对Access Token后下发请求资源。

6.4更多资料

7、必须要掌握的Https知识

7.1HTTPS基础

苹果已从去年开始强制新上线的APP必须使用HTTPS(详见《苹果即将强制实施 ATS,你的APP准备好切换到HTTPS了吗?》),谷歌的Chrome浏览器也已宣布不支持https的网络将被标记不“不安全”,做为开发者,我们能感觉到HTTPS越来越被重视,所以了解https也是必须的。

简单的说:Http + 加密 + 认证 + 完整性保护 = Https。

传统的Http协议是一种应用层的传输协议,Http直接与TCP协议通信。

其本身存在一些缺点:

  • Http协议使用明文传输,容易遭到窃听;
  • Http对于通信双方都没有进行身份验证,通信的双方无法确认对方是否是伪装的客户端或者服务端;
  • Http对于传输内容的完整性没有确认的办法,往往容易在传输过程中被劫持篡改。

因此,在一些需要保证安全性的场景下,比如涉及到银行账户的请求时,Http无法抵御这些攻击。  Https则可以通过增加的SSL\TLS,支持对于通信内容的加密,以及对通信双方的身份进行验证。

7.2Https的加密原理

近代密码学中加密的方式主要有两类:

  • 1)对称秘钥加密;
  • 2)非对称秘钥加密。

对称秘钥加密是指加密与解密过程使用同一把秘钥。这种方式的优点是处理速度快,但是如何安全的从一方将秘钥传递到通信的另一方是一个问题。

非对称秘钥加密是指加密与解密使用两把不同的秘钥。这两把秘钥,一把叫公开秘钥,可以随意对外公开。一把叫私有秘钥,只用于本身持有。得到公开秘钥的客户端可以使用公开秘钥对传输内容进行加密,而只有私有秘钥持有者本身可以对公开秘钥加密的内容进行解密。这种方式克服了秘钥交换的问题,但是相对于对称秘钥加密的方式,处理速度较慢。

SSL\TLS的加密方式则是结合了两种加密方式的优点。首先采用非对称秘钥加密,将一个对称秘钥使用公开秘钥加密后传输到对方。对方使用私有秘钥解密,得到传输的对称秘钥。之后双方再使用对称秘钥进行通信。这样即解决了对称秘钥加密的秘钥传输问题,又利用了对称秘钥的高效率来进行通信内容的加密与解密。

安全方面的文章,可以详细阅读以下几篇:

7.3Https的认证

SSL\TLS采用的混合加密的方式还是存在一个问题,即怎么样确保用于加密的公开秘钥确实是所期望的服务器所分发的呢?也许在收到公开秘钥时,这个公开秘钥已经被别人篡改了。因此,我们还需要对这个秘钥进行认证的能力,以确保我们通信的对方是我们所期望的对象。

目前的做法是使用由数字证书认证机构颁发的公开秘钥证书。服务器的运营人员可以向认证机构提出公开秘钥申请。认证机构在审核之后,会将公开秘钥与共钥证书绑定。服务器就可以将这个共钥证书下发给客户端,客户端在收到证书后,使用认证机构的公开秘钥进行验证。一旦验证成功,即可知道这个秘钥是可以信任的秘钥。

7.4Https小结

Https的通信流程:

  • 1)Client发起请求;
  • 2)Server端响应请求,并在之后将证书发送至Client;
  • 3)Client使用认证机构的共钥认证证书,并从证书中取出Server端共钥;
  • 4)Client使用共钥加密一个随机秘钥,并传到Server;
  • 5)Server使用私钥解密出随机秘钥;
  • 6)通信双方使用随机秘钥最为对称秘钥进行加密解密。

附录:相关资料汇总

[1] 更多网络编程资料汇总:
TCP/IP详解 - 第11章·UDP:用户数据报协议
TCP/IP详解 - 第17章·TCP:传输控制协议
TCP/IP详解 - 第18章·TCP连接的建立与终止
TCP/IP详解 - 第21章·TCP的超时与重传
技术往事:改变世界的TCP/IP协议(珍贵多图、手机慎点)
通俗易懂-深入理解TCP协议(上):理论基础
通俗易懂-深入理解TCP协议(下):RTT、滑动窗口、拥塞处理
理论经典:TCP协议的3次握手与4次挥手过程详解
理论联系实际:Wireshark抓包分析TCP 3次握手、4次挥手过程
计算机网络通讯协议关系图(中文珍藏版)
UDP中一个包的大小最大能多大?
P2P技术详解(一):NAT详解——详细原理、P2P简介
P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解
P2P技术详解(三):P2P技术之STUN、TURN、ICE详解
通俗易懂:快速理解P2P技术中的NAT穿透原理
高性能网络编程(一):单台服务器并发TCP连接数到底可以有多少
高性能网络编程(二):上一个10年,著名的C10K并发连接问题
高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了
高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索
不为人知的网络编程(一):浅析TCP协议中的疑难杂症(上篇)
不为人知的网络编程(二):浅析TCP协议中的疑难杂症(下篇)
不为人知的网络编程(三):关闭TCP连接时为什么会TIME_WAIT、CLOSE_WAIT
不为人知的网络编程(四):深入研究分析TCP的异常关闭
不为人知的网络编程(五):UDP的连接性和负载均衡
不为人知的网络编程(六):深入地理解UDP协议并用好它
不为人知的网络编程(七):如何让不可靠的UDP变的可靠?
网络编程懒人入门(一):快速理解网络通信协议(上篇)
网络编程懒人入门(二):快速理解网络通信协议(下篇)
网络编程懒人入门(三):快速理解TCP协议一篇就够
网络编程懒人入门(四):快速理解TCP和UDP的差异
网络编程懒人入门(五):快速理解为什么说UDP有时比TCP更有优势
网络编程懒人入门(六):史上最通俗的集线器、交换机、路由器功能原理入门
网络编程懒人入门(七):深入浅出,全面理解HTTP协议
网络编程懒人入门(八):手把手教你写基于TCP的Socket长连接
技术扫盲:新一代基于UDP的低延时网络传输层协议——QUIC详解
让互联网更快:新一代QUIC协议在腾讯的技术实践分享
现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障
聊聊iOS中网络编程长连接的那些事
移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”
移动端IM开发者必读(二):史上最全移动弱网络优化方法总结
IPv6技术详解:基本概念、应用现状、技术实践(上篇)
IPv6技术详解:基本概念、应用现状、技术实践(下篇)
从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路
脑残式网络编程入门(一):跟着动画来学TCP三次握手和四次挥手
脑残式网络编程入门(二):我们在读写Socket时,究竟在读写什么?
脑残式网络编程入门(三):HTTP协议必知必会的一些知识
>> 更多同类文章 ……

[2] Web端即时通讯资料汇总:
新手入门贴:史上最全Web端即时通讯技术原理详解
Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE
SSE技术详解:一种全新的HTML5服务器推送事件技术
Comet技术详解:基于HTTP长连接的Web端实时通信技术
新手快速入门:WebSocket简明教程
WebSocket详解(一):初步认识WebSocket技术
WebSocket详解(二):技术原理、代码演示和应用案例
WebSocket详解(三):深入WebSocket通信协议细节
WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)
WebSocket详解(五):刨根问底HTTP与WebSocket的关系(下篇)
WebSocket详解(六):刨根问底WebSocket与Socket的关系
socket.io实现消息推送的一点实践及思路
LinkedIn的Web端即时通讯实践:实现单机几十万条长连接
Web端即时通讯技术的发展与WebSocket、Socket.io的技术实践
Web端即时通讯安全:跨站点WebSocket劫持漏洞详解(含示例代码)
开源框架Pomelo实践:搭建Web端高性能分布式IM聊天服务器
使用WebSocket和SSE技术实现Web端消息推送
详解Web端通信方式的演进:从Ajax、JSONP 到 SSE、Websocket
MobileIMSDK-Web的网络层框架为何使用的是Socket.io而不是Netty?
理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性
微信小程序中如何使用WebSocket实现长连接(含完整源码)
>> 更多同类文章 ……

上一篇:MUI版本升级更新程序IOS和andriod


下一篇:构造函数为什么不能为虚函数 & 基类的析构函数为什么要为虚函数