サーバのキャッシュ情報を利用してキャッシュする実装(Ruby)

# 参考URL

Working with HTTP cache
https://www.brianstorti.com/working-with-http-cache/

# はじめに

httpのレスポンスのヘッダに `last-modified` や `Etag` 等のキャッシュに関する情報が載っていることがあります．
Webサーバ側がせっかくキャッシュ情報を設定しているんだから，クライアント側も実装しよう．

# 基本

以降，Ruby 2.4.3(Windows10 64bit)，2.5.1(Windows Subsystem for Linux)で説明します．

次のコードは，httpsで`docs.ruby-lang.org` から `/` をダウンロードします．
httpのヘッダやステータスコードも出力させています．

```ruby
require 'net/https'

https = Net::HTTP.new("docs.ruby-lang.org", 443)
https.use_ssl = true
https.verify_mode = OpenSSL::SSL::VERIFY_NONE
https.start do |http|
    response = http.get("/")
    p response.code
    puts response.each.map{|k,v| k+"\t => "+v }.to_a
    puts response.body[0..200] if response.body
end
```

結果はこんな感じでした．

```
"200"
server   => nginx/1.10.3
content-type     => text/html
last-modified    => Thu, 27 Dec 2018 00:19:32 GMT
etag     => W/"5c241a94-394"
cache-control    => public, max-age=43200, s-maxage=172800, stale-while-revalidate=86400, stale-if-error=604800
via      => 1.1 varnish, 1.1 varnish
fastly-debug-digest      => 33272ba139e924bc977ca4803fe9bfe699d76ef3818ac8b578bbfe42b3afa6cc
content-length   => 579
accept-ranges    => bytes
date     => Sat, 02 Mar 2019 01:49:34 GMT
age      => 102622
connection       => keep-alive
x-served-by      => cache-nrt6147-NRT, cache-itm18822-ITM
x-cache  => HIT, HIT
x-cache-hits     => 1, 3
x-timer  => S1551491375.978827,VS0,VE5
vary     => Accept-Encoding
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<!-- Global Site Tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-620926-3"></script
```


# キャッシュ

## last-modified を使う

ヘッダに含まれている`last-modified`は，「最後にそのページが更新された時間」です．
`last-modified`の値が前回アクセスした時と同じならば，
更新が無かったとみなすことができるので，bodyを取得する必要はありません．

```ruby
require 'net/https'
require 'time'

@cache = {}

def get(path)
    https = Net::HTTP.new("docs.ruby-lang.org", 443)
    https.use_ssl = true
    https.verify_mode = OpenSSL::SSL::VERIFY_NONE
    https.start do |http|
        # ヘッダのみを取得
        response = http.head(path)
        # そのパスのキャッシュが無い OR キャッシュが古いならば
        if @cache[path].nil? || Time.parse(@cache[path][:last_modified]) < Time.parse(response["last-modified"])
            STDERR.puts "get"
            # bodyも含めて取得
            response = http.get(path)
            # 取得結果を記憶
            @cache[path] = {
                last_modified: response["last-modified"],
                body: response.body
            }
            @cache[path][:body]
        else
            STDERR.puts "cached"
            # 記憶済みの取得結果を返す
            @cache[path][:body]
        end
    end
end

puts get("/")[0..50]
sleep 1
puts get("/")[0..50]
sleep 1
puts get("/")[0..50]

```


## If-Modified-Sinceを使う

クライアント側で更新時刻を比較するのではなく，サーバ側で比較してもらう方法です．
`If-Modified-Since` ヘッダを付けて，クエリを投げます．
更新がなかった場合，304が返ってきます．

```ruby
require 'net/https'
require 'time'

@cache = {}

def get(path)
    https = Net::HTTP.new("docs.ruby-lang.org", 443)
    https.use_ssl = true
    https.verify_mode = OpenSSL::SSL::VERIFY_NONE
    https.start do |http|
        # 最後に取得した日付
        since = @cache[path] ? @cache[path][:last_modified] : ""
        # サーバから取得する
        response = http.get(path, {"If-Modified-Since"=>since})
        if (response.code == "200") # OK(変更がある)
            STDERR.puts "get"
            # 取得結果を記憶
            @cache[path] = {
                last_modified: response["last-modified"],
                body: response.body
            }
            @cache[path][:body]
        elsif (response.code == "304") # Not Modified
            STDERR.puts "cached"
            # 記憶済みの取得結果を返す
            @cache[path][:body]
        else
            "🤔"
        end
    end
end

puts get("/")[0..50]
sleep 1
puts get("/")[0..50]
sleep 1
puts get("/")[0..50]
```

## Etag

ヘッダに`Etag`が記載されている場合があります．
ウェブページのハッシュ値のような識別子で，前回のクエリとEtag値が同じならば，
ページに変化がない，とみなすことが出来ます．
`If-Modified-Since`同様に，`If-None-Match` ヘッダを付けてクエリを投げることで，
サーバ側でEtag値の比較をしてくれるはずです．

```ruby
require 'net/https'
require 'time'

@cache = {}

def get(path)
    https = Net::HTTP.new("docs.ruby-lang.org", 443)
    https.use_ssl = true
    https.verify_mode = OpenSSL::SSL::VERIFY_NONE
    https.start do |http|
        # 最後に取得した日付
        etag = @cache[path] ? @cache[path][:etag] : ""
        # サーバから取得する
        response = http.get(path, {"If-None-Match"=>etag})
        if (response.code == "200") # OK(変更がある)
            STDERR.puts "get"
            # 取得結果を記憶
            @cache[path] = {
                etag: response["Etag"],
                body: response.body
            }
            @cache[path][:body]
        elsif (response.code == "304") # Not Modified
            STDERR.puts "cached"
            # 記憶済みの取得結果を返す
            @cache[path][:body]
        else
            "🍣"
        end
    end
end

puts get("/")[0..50]
sleep 1
puts get("/")[0..50]
sleep 1
puts get("/")[0..50]
```

ETagには「強いETag値」「弱いETag値」の2種類があるようです．
詳細は日本語版Wikipediaに記載されています．
https://ja.wikipedia.org/wiki/HTTP_ETag