golangの日記

Go言語を中心にプログラミングについてのブログ

HTTPリクエストのレスポンスをキャッシュする

golang.png


HTTPキャッシュ(cache)は、ウェブページなど一度目のリクエストで取得したコンテンツを保存しておき、
次回以降のリクエスト送信時にコンテンツに変更がなければ再利用するというもの。

リクエストを受け取るサーバー側は処理が軽減され負荷が減り、
リクエストを送るクライアント側も通信量が減るので受け取るまでの時間短縮になる。





以下のコードは、 1回目のレスポンスヘッダーの ETag と Last-Modified を それぞれ2回目のリクエストヘッダーの If-None-Match と If-Modified-Since に設定して キャッシュを再利用しても良いかサーバーに確認するもの

この他にも「そもそもキャッシュしてもいいコンテンツなのか」「キャッシュの有効期限はいつまでか」など
考慮すべきことはたくさんあるけどETagだけでもセットしておくと 304 Not Modified(変更がない) が返ってくる。

HTTPキャッシュの仕組みは HTTP キャッシュに、HTTPヘッダーにつては HTTP ヘッダー - HTTP | MDN に載ってる


package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)

type Cache struct {
    Resp *http.Response
    Body []byte
}

var (
    // キャッシュを保存しておくためのマップ
    // データベースを使うのが現実的
    cacheMap = make(map[string]*Cache)
)

func sendRequest(rawurl string) ([]byte, *http.Response, error) {
    req, err := http.NewRequest(http.MethodGet, rawurl, nil)
    if err != nil {
        return nil, nil, err
    }

    cache := cacheMap[rawurl]

    if cache != nil {
        // rawurlでキャッシュが存在していたら
        // 検証するためのヘッダーをリクエストヘッダーにセットする
        eTag := cache.Resp.Header.Get("etag")
        req.Header.Set("if-none-match", eTag)

        lastModified := cache.Resp.Header.Get("last-modified")
        req.Header.Set("if-modified-since", lastModified)
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, nil, err
    }
    body, _ := ioutil.ReadAll(resp.Body)


    fmt.Println("Status code:", resp.StatusCode)

    // ステータスコードが 304 (Not modified) だったらキャッシュを返す
    if resp.StatusCode == http.StatusNotModified {
        return cache.Body, cache.Resp, nil
    } else {
        cacheMap[rawurl] = &Cache{
            Resp: resp,
            Body: body,
        }
    }

    return body, resp, nil
}

func main() {
    // 画像はキャッシュに対応してる場合が殆どなので試すのにいい
    rawurl := "https://cdn-ak.f.st-hatena.com/images/fotolife/g/golang/20181009/20181009042416.png"

    for i := 0; i < 2; i++ {
        _, _, err := sendRequest(rawurl)
        if err != nil {
            log.Fatal(err)
        }
        time.Sleep(1000 * time.Millisecond)
    }
}



LevelDBを使ったキャッシュの保存(いろいろ不十分だけど、こんな感じで保存できる)

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "net/http/httputil"

    "github.com/syndtr/goleveldb/leveldb"
)

func main() {
    rawurl := "https://example.com"

    db, err := leveldb.OpenFile("cache", nil)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    req, err := http.NewRequest(http.MethodGet, rawurl, nil)
    if err != nil {
        log.Fatal(err)
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        log.Fatal(err)
    }

    // *http.Response を []byte 化する
    cache, err := httputil.DumpResponse(resp, true)
    if err != nil {
        log.Fatal(err)
    }

        // データベースに保存する
    if err := db.Put([]byte(rawurl), cache, nil); err != nil {
        log.Fatal(err)
    }

    // データベースから取り出す
    if data, err := db.Get([]byte(rawurl), nil); err != nil {
        log.Fatal(err)
    } else {
        // 保存した []byte を *http.Response に戻す
        cache, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(data)), req)
        if err != nil {
            log.Fatal(err)
        }
        body, _ := ioutil.ReadAll(cache.Body)
        fmt.Printf("%#v\n%s\n", cache, string(body))
    }
}