Webサーバのキャッシュ設定とiOS URLSessionのキャッシュの動作


# はじめに
 サーバ間からダウンロードしたデータをスマホに表示させたいと思い、キャッシュを使った効率的な方法を検索しました。しかし、いろいろな方法が書いてあり、古い情報もありそうで、どれが正しいかわからなかったので、実機で検証してみました。結論としては、サーバを正しく設定すれば、基本的には、iOSはデフォルトの`URLSession`を利用できました。例外として、HTTP応答が`byte-range requests`の場合は、キャッシュを使わないようなので、このような場合には別の方法を見つける必要がありそうです。
　今回、一番つまずいたところは、ネットワーク的には正しくキャッシュの動作(Status:304)をしているのに、アプリのログを見ると、キャッシュを使っていないように見える(Status:200)ところでした。同じように困る方もいると思うので、検証結果も最後に載せておきます。

# やりたいこと
- サーバのデータをスマホにダウンロードして表示
- できるだけ最新のデータを利用する
- 同一のデータの場合は、ダウンロードせずに、スマホ内のキャッシュを使う
- 実装はできるだけ標準のものを使い、自分で実装する部分を減らす。


# iOSでのキャッシュポリシーの実装
SWIFTでよく使うURLSessionのキャッシュポリシーは、設定しない場合のデフォルトポリシーは、`useProtocolCachePolicy`です。この場合の動作は、[Apple社の資料]
(
https://developer.apple.com/documentation/foundation/nsurlrequest/cachepolicy/useprotocolcachepolicy)に書かれています。以下は、この図の引用です。今回実現したいことは、この図に書いた1〜3を実現することなので、URLSessionをそのまま使えば実現できそうです。
しかし、例外として、`byte-range requests`の場合には、キャッシュポリシーは、`reloadIgnoringLocalCacheData`になるようです。


<img width="700" alt="cachepolicy" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404406/a622f6b5-5a33-2330-5e0e-fcf5b33c21ee.png">

URLSessionを利用すると簡単なコードでサーバからHTTP GETできます。以下は、検証に使ったコードです。

``` 検証コード.swift
func getHttp() {
  let urlString = "http://example.com/"
  guard let url = URL(string: urlString) else { return }
  let request = URLRequest(url: url)

  let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
    debugPrint(response.debugDescription) // HTTPヘッダの表示
  }
  task.resume()
}
```

# Webサーバの設定

上記のようにスマホ側にコンテンツをキャッシュさせるためには、Webサーバに設定が必要です。キャッシュが新しいかどうかを確認するためのHTTPヘッダは、RFC7232で2つ定義されています。`Last-Modified`と`ETag`。Webサーバから少なくともどちらか１つのHTTPヘッダで応答する必要があります。

- Last-Modified ([RFC7232 2.2節](https://tools.ietf.org/html/rfc7232#section-2.2))
- ETag ([RFC7232 2.3節](https://tools.ietf.org/html/rfc7232#section-2.3))

また、キャッシュが最新かを常に検証(validation)するためには、WEBサーバで以下のHTTPヘッダのどちらかを追加する必要があります。`Cache-Control: no-cache`と`Cache-Control: max-age=0`。`no-cache`は、キャッシュをしない設定に見えますが、そうではなく、キャッシュがあ最新かを検証してからキャッシュを利用するという意味です。

- `Cache-Control: no-cache` ([RFC7234 5.2.1.4](https://tools.ietf.org/html/rfc7234#section-5.2.1))
- `Cache-Control: max-age=0` ([RFC7234 5.2.1.1](https://tools.ietf.org/html/rfc7234#section-5.2.1))

# 検証環境
- Xcode : 11.2
- iOS: 13.2.2

# 検証結果概要
検証結果を以下の図で示します。構成要素は、以下の３つです。
- iOSアプリ
- iOS内のキャッシュ
- Webサーバ
検証は、上記の図で示した3つのパターンを試します。
1. キャッシュが存在しない場合、サーバから取得
2. キャッシュが新しい場合、キャッシュを利用
3. キャッシュが古い場合、サーバから取得

1と3は、サーバからの応答と同じ内容をアプリで見れます。
しかし、2のキャッシュが新しい場合は、サーバから`304 NOT MODIFIED`応答が来て、データは転送されません。このため、ネットワークの転送量は少なくなります。しかし、アプリから見る応答は、`200 OK`と見えます。サーバからデータが転送されているように見えますが、実際にはキャッシュを使っています。

<img width="700" alt="URLSession" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404406/18754195-145b-2477-54e9-4afd700a529c.png">

この図では、`Cache-Control: no-cache` `Last-Modified`を利用した例を図示していますが、`Cache-Control: max-age=0`や`ETag`を利用した場合もキャッシュの利用は同じでしたので、図は省略します。




# 参考文献

- [HTTP キャッシュ (MDN Docs)](https://developer.mozilla.org/ja/docs/Web/HTTP/Caching)
　説明がわかりやすい
- [HTTP/1.1: Caching (RFC7234)](https://tools.ietf.org/html/rfc7234) 
　キャッシュの仕様。RFC2616(旧仕様)の更新版。
- [HTTP/1.1: Conditional Requests (RFC7232)](https://tools.ietf.org/html/rfc7232) 
　条件付き要求の仕様。RFC2616(旧仕様)の更新版。
- [URLSessionのデフォルトキャッシュ動作 (Apple社)](
https://developer.apple.com/documentation/foundation/nsurlrequest/cachepolicy/useprotocolcachepolicy)

# 参考資料: 検証結果詳細
以下では、サーバのResponseに`Cache-Control: no-cache`を入れた場合を最初に載せます。動作は同じでしたが、以下の場合も載せておきます。

- `Cache-Control: no-cache` の場合
- `Cache-Control: max-age=0` の場合
- `Cache-Control: no-cache` `ETag`の場合


## HTTPヘッダ(Cache-Control: no-cache)の場合
### 初回アクセス
初回アクセス時のiOSからサーバへのHTTP Request

``` request.http
GET / HTTP/1.1
Host: example.com
```

初回アクセス時のサーバからiOSへのHTTP Response

``` response.http
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 02:59:55 GMT
Last-Modifi![20191113-URLSession.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404406/e1dbbef6-9589-4e80-a00d-babdff924194.png)
ed: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: no-cache
```
アプリ(URLSession)で取得できるHTTP Response。このResponseデータがスマホのキャッシュに保存される。

``` app.http
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 02:59:55 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: no-cache
```

### ２回目アクセス(スマホ内のキャッシュ利用)
２回目アクセス時のiOSからサーバへのHTTP Request。キャッシュに`Last-Modified`がある場合は、`If-Modified-Since`をRequestに含める。

``` request.http
GET / HTTP/1.1
Host: example.com
If-Modified-Since: Wed, 04 Sep 2019 00:00:00 GMT
```

２回目アクセス時のサーバからiOSへのHTTP Response。スマホ内のキャッシュを利用する。キャッシュからDateフィールドを更新してアプリに応答

``` response.http
![undefined]()
HTTP/1.1 304 NOT MODIFIED
Date: Tue, 12 Nov 2019 03:01:01 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: no-cache
```
アプリ(URLSession)で取得できるHTTP Response

``` app.http
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 03:01:01 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: no-cache
```

## HTTPヘッダ(Cache-Control: max-age=0)の場合
### 初回アクセス
初回アクセス時のiOSからサーバへのHTTP Request

``` request.http
GET / HTTP/1.1
Host: example.com
```

初回アクセス時のサーバからiOSへのHTTP Response

``` response.http
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 03:14:14 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: max-age=0
```
アプリ(URLSession)で取得できるHTTP Response。このResponseデータがスマホのキャッシュに保存される。

``` app.http
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 03:14:14 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: max-age=0
```

### ２回目アクセス(スマホ内のキャッシュ利用)
２回目アクセス時のiOSからサーバへのHTTP Request。キャッシュに`Last-Modified`がある場合は、`If-Modified-Since`をRequestに含める。

``` request.http
GET / HTTP/1.1
Host: example.com
If-Modified-Since: Wed, 04 Sep 2019 00:00:00 GMT
```

２回目アクセス時のサーバからiOSへのHTTP Response。スマホ内のキャッシュを利用する。キャッシュからDateフィールドを更新してアプリに応答

``` response.http
HTTP/1.1 304 NOT MODIFIED
Date: Tue, 12 Nov 2019 03:15:33 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: max-age=0
```
アプリ(URLSession)で取得できるHTTP Response

``` app.http
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 03:15:33 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: max-age=0
```

## HTTPヘッダ(ETag)の場合
### 初回アクセス
初回アクセス時のiOSからサーバへのHTTP Request

``` request.http
GET / HTTP/1.1
Host: example.com
```

初回アクセス時のサーバからiOSへのHTTP Response

``` response.http
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 06:01:11 GMT
ETag: "7703193a5f4bc6aa199d7d2121684c3f"
Cache-Control: no-cache
```
アプリ(URLSession)で取得できるHTTP Response。このResponseデータがスマホのキャッシュに保存される。

``` app.http
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 06:01:11 GMT
ETag: "7703193a5f4bc6aa199d7d2121684c3f"
Cache-Control: no-cache
```

### ２回目アクセス(スマホ内のキャッシュ利用)
２回目アクセス時のiOSからサーバへのHTTP Request。キャッシュに`ETag`がある場合は、`If-None-Match`をRequestに含める。

``` request.http
GET / HTTP/1.1
Host: example.com
If-None-Match: "7703193a5f4bc6aa199d7d2121684c3f"
```

２回目アクセス時のサーバからiOSへのHTTP Response。スマホ内のキャッシュを利用する。キャッシュからDateフィールドを更新してアプリに応答

``` response.http
HTTP/1.1 304 NOT MODIFIED
Date: Tue, 12 Nov 2019 06:02:13 GMT
ETag: "7703193a5f4bc6aa199d7d2121684c3f"
Cache-Control: no-cache
```
アプリ(URLSession)で取得できるHTTP Response

``` app.http
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 06:02:13 GMT
ETag: "7703193a5f4bc6aa199d7d2121684c3f"
Cache-Control: no-cache
```