Gzip을 사용하는 Golang HTTP 핸들러



소개



Golang은 표준 라이브러리 내에 데이터를 압축하는 데 사용할 수 있는 다양한 종류의 압축 기술을 가지고 있습니다. 데이터 크기를 줄이기 위해 압축이 필요합니다. 웹 서버에서도 압축 기술은 클라이언트와 서버 간의 통신 속도를 높이는 데 도움이 됩니다. Gzip은 Golang과 웹 모두에서 지원하는 압축 기술 중 하나입니다. 이 문서에서는 gzip 요청/응답을 압축 해제하는 golang HTTP 핸들러 생성과 gzip 압축 본문 페이로드를 송수신하는 HTTP 요청을 생성하는 방법을 다룹니다. 이 문서에서 다루는 모든 코드는 this repository에서 찾을 수 있습니다.

디렉토리 구조




$ tree .
.
├── LICENSE
├── Makefile
├── README.md
├── client
│   └── main.go
├── curl.sh
├── go.mod
├── server
│   └── main.go
└── util.go

2 directories, 8 files


서버/main.go




package main

import (
    "example"
    "log"
    "net/http"
)

func main() {
    log.Println("server listening at :8000")
    http.ListenAndServe(":8000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        body := example.MustReadCompressedBody[example.Payload](r.Body)
        body.Number++

        example.MustWriteCompressedResponse(w, body)
    }))
}


서버는 read a compressed body , increment the payload content , 그런 다음 respond with new body 가 될 단일 엔드포인트만 실행합니다.

util.go



유효 탑재량



압축을 테스트하기 위해 가장 간단한 페이로드를 사용할 수 있습니다. 예를 들면 다음과 같습니다.

type Payload struct {
    Number int `json:"number"`
}


압축된 바디 페이로드 읽기




func MustReadCompressedBody[T any](r io.Reader) *T {
    gr, err := gzip.NewReader(r)
    PanicIfErr(err)
    defer gr.Close()

    var t T
    PanicIfErr(json.NewDecoder(gr).Decode(&t))
    return &t
}


gzip으로 압축된 페이로드를 읽으려면 응답 또는 요청에서 gzip.Reader를 생성해야 합니다. 그런 다음 gzip.Reader 인스턴스를 사용하여 평소와 같이 JSON을 디코딩합니다.

압축 응답 작성




func MustWriteCompressedResponse(w http.ResponseWriter, body any) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Content-Encoding", "gzip")

    gw := gzip.NewWriter(w)
    defer gw.Close()
    PanicIfErr(json.NewEncoder(gw).Encode(body))
}


평소와 같이 고객에게 Content-TypeContent-Encoding 에 대해 알리는 것이 좋습니다. 그러나 golang의 경우 클라이언트에 Content-Encoding에 대해 알리면 golanghttp.Request이 페이로드를 자동으로 압축 해제하는 데 도움이 됩니다. reading the payload 과 마찬가지로 gzip.Writer 만 생성한 다음 콘텐츠를 정상적으로 인코딩하면 됩니다. 또한 EOF 오류를 방지하려면 gw.Close()를 호출하는 것을 잊지 마십시오.

서버 테스트



이제 서버가 요청과 응답을 완전히 압축 해제했습니다. 먼저 cURL 명령을 사용하여 테스트해 봅시다.

# run the server on another terminal
# using `go run ./server/` command
$ echo '{"number": 99}' | gzip | \
curl -iXPOST http://localhost:8000/ \
-H "Content-Type: application/json" \
-H "Content-Encoding: gzip" \
--compressed --data-binary @-


서버가 Payload.Number를 증가시키므로 100로 요청을 보내면 응답 번호가 99가 될 것으로 예상할 수 있습니다. 예상 결과는 다음과 유사합니다.

HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: application/json
Date: Thu, 26 May 2022 12:04:53 GMT
Content-Length: 39

{"number":100}


이제 서버가 완전히 제대로 작동하는지 확인한 후 golanghttp.Request을 사용하여 HTTP 요청을 전송해 보겠습니다.

클라이언트/main.go




package main

import (
    "context"
    "encoding/json"
    "example"
    "log"
    "net/http"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()

    body := &example.Payload{
        Number: 100,
    }

    log.Printf("create compressed request with %#v", body)
    req := example.MustCreateCompressedRequest(ctx, http.MethodPost, "http://localhost:8000/", body)
    defer req.Body.Close()

    log.Printf("send compressed request")
    resp, err := http.DefaultClient.Do(req)
    example.PanicIfErr(err)
    defer resp.Body.Close()

    log.Println("resp.Uncompressed?", resp.Uncompressed)
    var responsePayload *example.Payload
    if resp.Uncompressed {
        err = json.NewDecoder(resp.Body).Decode(&responsePayload)
    } else {
        responsePayload = example.MustReadCompressedBody[example.Payload](resp.Body)
    }
    log.Printf("read response %#v", responsePayload)
}


일반 HTTP 요청과 마찬가지로 create an HTTP.Request , send the request 및 마지막으로 decode the response 만 있으면 됩니다. 그러나 요청 페이로드를 보내기 전에 먼저 압축하는 것을 잊지 마십시오.

압축된 요청 만들기




func MustCreateCompressedRequest(ctx context.Context, method, url string, body any) *http.Request {
    pr, pw := io.Pipe()

    go func() {
        gw := gzip.NewWriter(pw)
        err := json.NewEncoder(gw).Encode(body)
        defer PanicIfErr(gw.Close())
        defer pw.CloseWithError(err)
    }()

    r, err := http.NewRequestWithContext(ctx, method, url, pr)
    PanicIfErr(err)

    r.Header.Set("Content-Type", "application/json")
    r.Header.Set("Content-Encoding", "gzip")

    return r
}


새로운 압축 요청을 생성하는 것은 write a compressed response과 같이 매우 쉽습니다. 새 gzip.Writer를 생성해야 합니다. 위의 코드는 io.Pipe를 활용하여 본문을 메모리에 버퍼링하고 불필요한 할당을 방지합니다. 그러나 본문을 먼저 버퍼링하려는 경우 대안은 다음과 같습니다.

func MustCreateCompressedRequest(ctx context.Context, method, url string, body any) *http.Request {
    var b bytes.Buffer

    gw := gzip.NewWriter(&b)
    err := json.NewEncoder(gw).Encode(body)
    defer PanicIfErr(gw.Close())

    r, err := http.NewRequestWithContext(ctx, method, url, &b)
    PanicIfErr(err)

    r.Header.Set("Content-Type", "application/json")
    r.Header.Set("Content-Encoding", "gzip")

    return r
}


여기서도 gw.Close()가 필요합니다. gzip.Writer를 닫지 않으면 다음과 유사한 EOF 오류가 표시됩니다.

2022/05/26 19:23:58 http: panic serving [::1]:56436: unexpected EOF


마지막으로 압축 요청을 생성한 후 일반 요청처럼 요청을 전송하기만 하면 됩니다. 하지만 여기 까다로운 부분이 있습니다.

var responsePayload *example.Payload
if resp.Uncompressed {
    err = json.NewDecoder(resp.Body).Decode(&responsePayload)
} else {
    responsePayload = example.MustReadCompressedBody[example.Payload](resp.Body)
}


압축을 풀고 디코딩하려는 응답을 받은 후 resp.Uncompressed 에 유의하십시오. 서버가 Content-Encoding: gzip 헤더를 반환하면 here Golang이 decompress 페이로드를 시도하므로 example.MustReadCompressedBody[example.Payload](resp.Body) 를 사용할 필요가 없습니다. 그러나 요청에 r.Header.Set("Accept-Encoding", "gzip")를 추가하면 자동으로 압축이 풀리지 않습니다.

클라이언트 테스트




# run the server on another terminal
# using `go run ./server/` command
$ go run ./client
2022/05/26 19:41:34 create compressed request with &example.Payload{Number:100}
2022/05/26 19:41:34 send compressed request
2022/05/26 19:41:34 resp.Uncompressed? true
2022/05/26 19:41:34 read response &example.Payload{Number:101}


결론



Golang HTTP 처리기 및 요청에서 gzip 압축을 구현하면 코드가 약간 복잡해질 수 있지만 요즘 최신 브라우저/API 게이트웨이는 코드를 변경하지 않고도 이를 쉽게 구현할 수 있다고 생각합니다. 또한 NY Times에서 만든 이middleware는 이 압축을 구현하는 데 드는 노력을 최소화하는 데 도움이 될 수 있습니다. 이 문서here에서 사용된 모든 코드를 찾을 수 있습니다.

읽어 주셔서 감사합니다!

좋은 웹페이지 즐겨찾기