Uncategorized

Klien HTTP streaming pertama untuk Go


Streaming tanpa buffering yang efisien untuk muatan HTTP besar – dibangun di atasnya net/http.

go get github.com/nativebpm/httpstream

httpstream menyediakan API minimal yang berorientasi streaming untuk membuat permintaan HTTP tanpa melakukan buffering terhadap seluruh payload di memori.
Ideal untuk badan JSON besar, unggahan multibagian, arsip yang dihasilkan, atau umpan data berkelanjutan.

Fitur Utama

  • Streaming data secara langsung melalui io.Pipe—tidak ada buffer perantara

  • Penggunaan memori yang konstan (O(1)), berapa pun ukuran muatannya

  • Tekanan balik alami (blok tulis ketika penerima lambat)

  • tipisâ net/http pembungkus—sepenuhnya kompatibel

  • Dukungan middleware:Â func(http.RoundTripper) http.RoundTripper

  • API yang lancar agar mudah dibaca (GET,A POST,A Multipartdll.)

  • Tidak ada kebocoran goroutine, tidak ada kebocoran global

Cara Kerjanya

httpstream menghubungkan penulis Anda langsung ke transportasi HTTP. Data dikirimkan saat diproduksi, sehingga server dapat segera memulai pemrosesan—tanpa menunggu seluruh data di-buffer.

Mengapa Streaming Penting

  • Klien HTTP tradisional menyangga seluruh isi permintaan sebelum mengirim. Untuk muatan yang besar atau dihasilkan secara dinamis, hal ini dapat menyebabkan:

  • Penggunaan memori yang tinggi (O(n)dimana n = ukuran muatan)

  • Permulaan transmisi yang lambat (server menunggu unggahan penuh)

  • Kesalahan kehabisan memori di lingkungan terbatas

httpstream menghilangkan masalah ini dengan sengaja.

Contoh Sederhana Dengan Logger

package main

import (
 "context"
 "log"
 "log/slog"
 "net/http"
 "os"
 "time"

 "github.com/nativebpm/httpstream"
)

func main() {

 logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
  Level: slog.LevelInfo,
 }))

 loggingMiddleware := httpstream.LoggingMiddleware(logger)

 client, err := httpstream.NewClient(&http.Client{Timeout: 10 * time.Second}, "https://httpbin.org")
 if err != nil {
  log.Fatal(err)
 }

 resp, err := client.GET(context.Background(), "/get").
  Use(loggingMiddleware).
  Send()
 if err != nil {
  log.Fatal(err)
 }
 resp.Body.Close()
}

Contoh Streaming Multibagian

Contoh ini menunjukkan streaming data antara dua server menggunakan httpstream.

Server 1 (:8080) – Menghasilkan File Besar

package main

import (
 "fmt"
 "io"
 "net/http"
 "strings"
)

func main() {
 http.HandleFunc("/file", func(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/plain")
  w.Header().Set("Content-Disposition", "attachment; filename=large.txt")

  var builder strings.Builder
  for i := 1; i <= 10000000; i++ {
   builder.WriteString(fmt.Sprintf("Line %d: This is a line in the large file.\n", i))
  }
  reader := strings.NewReader(builder.String())
  _, err := io.Copy(w, reader)
  if err != nil {
   http.Error(w, "Failed to generate file", http.StatusInternalServerError)
  }
 })

 fmt.Println("Server 1 running on :8080")
 http.ListenAndServe(":8080", nil)
}

Server 2 (:8081) – Menerima Unggahan Multibagian

package main

import (
 "fmt"
 "io"
 "log/slog"
 "net/http"
 "os"
)

func main() {
 logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))

 http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
  err := r.ParseMultipartForm(32 << 20) // 32MB max
  if err != nil {
   logger.Error("Failed to parse multipart", "error", err)
   http.Error(w, err.Error(), http.StatusBadRequest)
   return
  }

  file, header, err := r.FormFile("file")
  if err != nil {
   logger.Error("Failed to get form file", "error", err)
   http.Error(w, err.Error(), http.StatusBadRequest)
   return
  }
  defer file.Close()

  dst, err := os.Create(header.Filename)
  if err != nil {
   logger.Error("Failed to create file", "error", err)
   http.Error(w, err.Error(), http.StatusInternalServerError)
   return
  }
  defer dst.Close()

  _, err = io.Copy(dst, file)
  if err != nil {
   logger.Error("Failed to copy file", "error", err)
   http.Error(w, err.Error(), http.StatusInternalServerError)
   return
  }

  logger.Info("File saved successfully", "filename", header.Filename)
  fmt.Fprintf(w, "File %s uploaded and saved", header.Filename)
 })

 fmt.Println("Server 2 running on :8081")
 http.ListenAndServe(":8081", nil)
}

Klien – Streaming File dari Server 1 ke Server 2

package main

import (
 "context"
 "fmt"
 "io"
 "log/slog"
 "mime"
 "net/http"
 "runtime"
 "time"

 "github.com/nativebpm/httpstream"
)

type countingReader struct {
 reader io.Reader
 count  int64
}

func (cr *countingReader) Read(p []byte) (n int, err error) {
 n, err = cr.reader.Read(p)
 cr.count += int64(n)
 return n, err
}

func (cr *countingReader) Close() error {
 if closer, ok := cr.reader.(io.Closer); ok {
  return closer.Close()
 }
 return nil
}

func main() {
 logger := slog.Default()
 var m runtime.MemStats
 runtime.ReadMemStats(&m)
 logger.Info("Before streaming", "Alloc (KB)", m.Alloc/1024, "TotalAlloc (KB)", m.TotalAlloc/1024)

 client := &http.Client{Timeout: 60 * time.Second}

 server1Client, _ := httpstream.NewClient(client, "http://localhost:8080")
 server1Client.Use(httpstream.LoggingMiddleware(logger.WithGroup("server1")))

 server2Client, _ := httpstream.NewClient(client, "http://localhost:8081")
 server2Client.Use(httpstream.LoggingMiddleware(logger.WithGroup("server2")))

 server1Resp, _ := server1Client.GET(context.Background(), "/file").Timeout(30 * time.Second).Send()
 defer server1Resp.Body.Close()

 filename := filename(server1Resp.Header, "default_filename")
 counter := &countingReader{reader: server1Resp.Body}

 server2Resp, _ := server2Client.Multipart(context.Background(), "/upload").
  File("file", filename, counter).
  Timeout(30 * time.Second).
  Send()
 defer server2Resp.Body.Close()

 runtime.ReadMemStats(&m)
 slog.Info("After streaming", "Alloc (KB)", m.Alloc/1024, "TotalAlloc (KB)", m.TotalAlloc/1024)

 streamedMB := float64(counter.count) / (1024 * 1024)
 slog.Info("Data streamed through pipeline", "bytes", counter.count, "megabytes", fmt.Sprintf("%.2f MB", streamedMB))
}

func filename(headers http.Header, defaultName string) string {
 if v := headers.Get("Content-Disposition"); v != "" {
  _, params, err := mime.ParseMediaType(v)
  if err == nil {
   if fn, ok := params["filename"]; ok {
    return fn
   }
  }
 }
 return defaultName
}

Menjalankan Contoh

Jalankan server dan klien di terminal terpisah:

go run server1/main.go
go run server2/main.go
go run main.go

Log menunjukkan penggunaan memori sebelum dan sesudah streaming dan total data yang dialirkan.

Before streaming "Alloc (KB)"=218 "TotalAlloc (KB)"=218
Sending request server1.method=GET server1.url=http://localhost:8080/file
Response received server1.status=200 server1.duration=1.849253692s
Sending request server2.method=POST server2.url=http://localhost:8081/upload
Response received server2.status=200 server2.duration=4.204263582s
After streaming "Alloc (KB)"=694 "TotalAlloc (KB)"=694
Data streamed through pipeline bytes=478888897 megabytes="456.70 MB"
Upload successful "server2Resp response"="File large.txt uploaded and save"

Hasil

  • Server 1 menghasilkan file besar dengan baris bernomor.

  • Klien mengalirkan file dari Server 1 ke Server 2 tanpa melakukan buffering di memori.

  • Server 2 menyimpan file secara lokal.

  • Log menunjukkan konfirmasi unggahan dan penggunaan memori.

Streaming efisien, konstan dalam memori, dan siap untuk muatan besar.

Contoh Lengkap

Github: https://github.com/nativebpm/httpstream



Klien HTTP streaming pertama untuk Go

Leave a Reply

Your email address will not be published. Required fields are marked *