// Бэкенд стенда hpnn_v2: показывает имя хоста, дату и время, а также реальный // адрес клиента. Последнее — ключевая проверка схемы DNAT-без-SNAT: если в // поле client стоит адрес из клиентской подсети, а не адрес балансировщика, // значит трансляция источника нигде не выполняется. package main import ( "fmt" "log" "net" "net/http" "os" "strconv" "sync/atomic" "time" ) var requests atomic.Uint64 func localAddr(r *http.Request) string { if a, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr); ok { return a.String() } return "unknown" } func main() { listen := os.Getenv("LISTEN") if listen == "" { listen = ":8080" } host, err := os.Hostname() if err != nil { host = "unknown" } mux := http.NewServeMux() // Одна строка на запрос: удобно читать вывод цикла из 20 curl. mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { n := requests.Add(1) w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprintf(w, "backend=%-6s time=%s client=%-21s served=%-21s req=%d\n", host, time.Now().Format("2006-01-02 15:04:05 MST"), r.RemoteAddr, localAddr(r), n) }) // Развёрнутый вид — для ручного осмотра в браузере. mux.HandleFunc("/info", func(w http.ResponseWriter, r *http.Request) { n := requests.Add(1) w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprintf(w, "Имя хоста : %s\n", host) fmt.Fprintf(w, "Дата и время : %s\n", time.Now().Format("2006-01-02 15:04:05 MST -07:00")) fmt.Fprintf(w, "Адрес клиента : %s\n", r.RemoteAddr) fmt.Fprintf(w, "Адрес назначения: %s\n", localAddr(r)) fmt.Fprintf(w, "Host-заголовок : %s\n", r.Host) fmt.Fprintf(w, "Запрос № : %d\n", n) }) // Цель health-проб балансировщика. mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprintln(w, "ok") }) // Длинный ответ: тело отдаётся по строке в секунду. Нужен, чтобы держать // открытую сессию во время смены состава пула и проверять, что уже // установленное соединение не рвётся. /slow?seconds=20 mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) { seconds := 10 if v := r.URL.Query().Get("seconds"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 120 { seconds = n } } w.Header().Set("Content-Type", "text/plain; charset=utf-8") flusher, _ := w.(http.Flusher) for i := 1; i <= seconds; i++ { fmt.Fprintf(w, "backend=%s tick=%d/%d time=%s\n", host, i, seconds, time.Now().Format("15:04:05")) if flusher != nil { flusher.Flush() } select { case <-r.Context().Done(): return case <-time.After(time.Second): } } }) srv := &http.Server{ Addr: listen, Handler: mux, ReadHeaderTimeout: 5 * time.Second, } log.Printf("backend %s слушает %s", host, listen) if err := srv.ListenAndServe(); err != nil { log.Fatal(err) } }