golang

Middleware на уровне сетевого стэка в Go

  • понедельник, 18 ноября 2024 г. в 00:00:07
https://habr.com/ru/companies/otus/articles/857070/

Привет, любители Go! Сегодня мы рассмотрим, как создать middleware на уровне сетевого стэка в Go. Middleware позволяет добавлять полезные функции к HTTP-запросам и ответам: логирование, аутентификация, обработка ошибок и многое другое.

Простой пример Middleware

Начнем с классики – middleware для логирования запросов:

package main

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

// loggingMiddleware логирует начало и конец обработки запроса.
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("🚀 Старт обработки %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        log.Printf("🏁 Завершено за %v", time.Since(start))
    })
}

// helloHandler – простой обработчик, который приветствует мир.
func helloHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Привет, мир! 🌍"))
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/hello", helloHandler)

    // Оборачиваем mux в middleware
    loggedMux := loggingMiddleware(mux)

    log.Println("🛡️ Сервер запущен на :8080")
    if err := http.ListenAndServe(":8080", loggedMux); err != nil {
        log.Fatalf("❌ Ошибка запуска сервера: %v", err)
    }
}

loggingMiddleware: принимает http.Handler, оборачивает его и добавляет логирование до и после обработки запроса.

helloHandler: просто отвечает строкой «Привет, мир! 🌍».

main: создаем ServeMux, регистрируем обработчик, оборачиваем его в loggingMiddleware и запускаем сервер.

Чтобы проверить, запустим сервер:

go run main.go

Затем переходим в браузере по адресу http://localhost:8080/hello. В терминале вы увидите что-то вроде:

🚀 Старт обработки GET /hello
🏁 Завершено за 150µs

Супер. Теперь каждый запрос к /hello будет логироваться.

Кастомные Middleware на уровне транспорта

Пора подняться на следующий уровень и поработать с транспортным уровнем. Здесь будем использовать интерфейс http.RoundTripper, который позволяет вмешиваться в процесс отправки и получения HTTP-запросов.

Создадим middleware, который добавляет кастомный заголовок ко всем исходящим запросам.

package main

import (
    "log"
    "net/http"
)

// CustomTransport – кастомный транспорт, добавляющий заголовок
type CustomTransport struct {
    Transport http.RoundTripper
}

// RoundTrip – метод, который добавляет заголовок и выполняет запрос
func (t *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    // Клонируем запрос, чтобы не мутировать оригинал
    clonedReq := req.Clone(req.Context())
    clonedReq.Header.Set("X-Custom-Header", "GoMiddleware")

    log.Printf("🛠️ Добавлен заголовок X-Custom-Header для %s %s", clonedReq.Method, clonedReq.URL)

    return t.Transport.RoundTrip(clonedReq)
}

func main() {
    client := &http.Client{
        Transport: &CustomTransport{
            Transport: http.DefaultTransport,
        },
    }

    resp, err := client.Get("https://httpbin.org/get")
    if err != nil {
        log.Fatalf("❌ Ошибка выполнения запроса: %v", err)
    }
    defer resp.Body.Close()

    log.Printf("✅ Статус ответа: %s", resp.Status)
}

CustomTransport: реализует интерфейс http.RoundTripper и добавляет заголовок X-Custom-Header ко всем запросам.

RoundTrip: клонирует запрос, добавляет заголовок и выполняет запрос через базовый транспорт.

main: создает HTTP-клиент с нашим кастомным транспортом и выполняет GET-запрос.

Запускаем клиентский код и проверяем логи:

🛠️ Добавлен заголовок X-Custom-Header для GET https://httpbin.org/get
✅ Статус ответа: 200 OK

Если зайти на http://httpbin.org/get, вы увидите, что заголовок действительно добавлен.

Комбинируем Middleware

Почему ограничиваться одним middleware, когда можно создать целую цепочку? Давайте создадим функцию ChainRoundTripper, которая позволит комбинировать несколько middleware.

Функция ChainRoundTripper:

// ChainRoundTripper – функция для объединения нескольких RoundTripper
func ChainRoundTripper(rt http.RoundTripper, middlewares ...func(http.RoundTripper) http.RoundTripper) http.RoundTripper {
    for _, m := range middlewares {
        rt = m(rt)
    }
    return rt
}

Создадим еще одно middleware для логирования запросов и объединим его с нашим CustomTransport.

package main

import (
    "log"
    "net/http"
)

// LoggingTransport – логирует каждый запрос
type LoggingTransport struct {
    Transport http.RoundTripper
}

func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    log.Printf("🔍 Запрос: %s %s", req.Method, req.URL)
    return t.Transport.RoundTrip(req)
}

func main() {
    client := &http.Client{
        Transport: ChainRoundTripper(http.DefaultTransport,
            func(rt http.RoundTripper) http.RoundTripper {
                return &CustomTransport{Transport: rt}
            },
            func(rt http.RoundTripper) http.RoundTripper {
                return &LoggingTransport{Transport: rt}
            },
        ),
    }

    resp, err := client.Get("https://httpbin.org/get")
    if err != nil {
        log.Fatalf("❌ Ошибка выполнения запроса: %v", err)
    }
    defer resp.Body.Close()

    log.Printf("✅ Статус ответа: %s", resp.Status)
}

LoggingTransport: логирует каждый запрос перед его выполнением.

main: использует ChainRoundTripper для объединения CustomTransport и LoggingTransport. Теперь каждый запрос будет логироваться и иметь добавленный заголовок.

Результат:

🔍 Запрос: GET https://httpbin.org/get
🛠️ Добавлен заголовок X-Custom-Header для GET https://httpbin.org/get
✅ Статус ответа: 200 OK

Отлично! Теперь есть мощная цепочка middleware, которая делает HTTP-клиент еще круче.

Оптимизация

Middleware – это здорово, но не будем забывать про производительность.

sync.Pool позволяет переиспользовать объекты, снижая нагрузку на сборщик мусора.

package main

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

// requestPool – пул для повторного использования объектов http.Request
var requestPool = sync.Pool{
    New: func() interface{} {
        return new(http.Request)
    },
}

// CustomTransport с использованием пула
type CustomTransport struct {
    Transport http.RoundTripper
}

func (t *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    // Получаем объект из пула
    pooledReq := requestPool.Get().(*http.Request)
    *pooledReq = *req // Копируем данные

    pooledReq.Header.Set("X-Custom-Header", "GoMiddleware")

    resp, err := t.Transport.RoundTrip(pooledReq)

    // Возвращаем объект в пул
    requestPool.Put(pooledReq)

    return resp, err
}

func main() {
    client := &http.Client{
        Transport: &CustomTransport{
            Transport: http.DefaultTransport,
        },
    }

    resp, err := client.Get("https://httpbin.org/get")
    if err != nil {
        log.Fatalf("❌ Ошибка выполнения запроса: %v", err)
    }
    defer resp.Body.Close()

    log.Printf("✅ Статус ответа: %s", resp.Status)
}

Обработка протоколов на низком уровне

Иногда хочется погрузиться глубже и управлять соединениями на уровне TCP. Создадим простой TCP-сервер, который читает данные, модифицирует их и отправляет обратно.

Простой TCP-Сервер:

package main

import (
    "bufio"
    "io"
    "log"
    "net"
    "strings"
)

// handleConnection – обрабатывает каждое подключение
func handleConnection(conn net.Conn) {
    defer conn.Close()
    log.Printf("🔗 Новое соединение с %s", conn.RemoteAddr())

    reader := bufio.NewReader(conn)
    for {
        data, err := reader.ReadString('\n')
        if err != nil {
            if err != io.EOF {
                log.Printf("❌ Ошибка чтения данных: %v", err)
            }
            break
        }
        data = strings.TrimSpace(data)
        log.Printf("📥 Получено: %s", data)

        // Модифицируем данные: делаем их заглавными
        modifiedData := strings.ToUpper(data) + "\n"

        _, err = conn.Write([]byte("💬 Echo: " + modifiedData))
        if err != nil {
            log.Printf("❌ Ошибка отправки данных: %v", err)
            break
        }
    }
}

func main() {
    ln, err := net.Listen("tcp", ":8081")
    if err != nil {
        log.Fatalf("❌ Ошибка запуска TCP-сервера: %v", err)
    }
    defer ln.Close()
    log.Println("🔧 TCP-сервер запущен на :8081")

    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Printf("❌ Ошибка принятия соединения: %v", err)
            continue
        }
        go handleConnection(conn)
    }
}

handleConnection принимает соединение, читает данные построчно, превращает их в верхний регистр и отправляет обратно, а main запускает TCP-сервер на порту 8081 и обрабатывает каждое соединение в отдельной горутине.

Запускаем сервер и в другом терминале используйте telnet или nc для подключения:

telnet localhost 8081

Вводим строку, например, hello, и получите ответ Echo: HELLO.

Безопасность

Безопасность – это не шутки. Добавим немного защиты в middleware, чтобы никто не смог подставить свои данные.

Создадим middleware, которое проверяет наличие и корректность токена авторизации.

package main

import (
    "log"
    "net/http"
)

// authMiddleware – проверяет заголовок Authorization
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token != "Bearer supersecrettoken" {
            log.Printf("🚫 Неавторизованный доступ к %s %s", r.Method, r.URL.Path)
            http.Error(w, "Forbidden", http.StatusForbidden)
            return
        }
        // Продолжаем обработку запроса
        next.ServeHTTP(w, r)
    })
}

// loggingMiddleware – уже знакомый middleware для логирования
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("🚀 Старт обработки %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        log.Printf("🏁 Завершено за %v", time.Since(start))
    })
}

// secureHandler – защищенный обработчик
func secureHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("🔒 Secure Content"))
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/secure", secureHandler)

    // Комбинируем middleware: сначала логирование, потом аутентификация
    handler := authMiddleware(loggingMiddleware(mux))

    log.Println("🛡️ Сервер с аутентификацией запущен на :8080")
    if err := http.ListenAndServe(":8080", handler); err != nil {
        log.Fatalf("❌ Ошибка запуска сервера: %v", err)
    }
}

Попробуем выполнить запросы с и без правильного токена:

# Без токена
curl -i http://localhost:8080/secure
# Ответ: 403 Forbidden

# С неверным токеном
curl -i -H "Authorization: Bearer wrongtoken" http://localhost:8080/secure
# Ответ: 403 Forbidden

# С правильным токеном
curl -i -H "Authorization: Bearer supersecrettoken" http://localhost:8080/secure
# Ответ: 200 OK
# Тело ответа: 🔒 Secure Content

Результаты:

🚫 Неавторизованный доступ к GET /secure

При правильном токене:

🚀 Старт обработки GET /secure
🏁 Завершено за 200µs

Полный пример

Теперь соберем всё вместе и создадим полноценный HTTP-сервер с несколькими middleware.

package main

import (
    "log"
    "net/http"
    "sync"
    "time"
)

// loggingMiddleware – логирует HTTP-запросы
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("🚀 Старт обработки %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        log.Printf("🏁 Завершено за %v", time.Since(start))
    })
}

// authMiddleware – проверяет заголовок Authorization
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token != "Bearer supersecrettoken" {
            log.Printf("🚫 Неавторизованный доступ к %s %s", r.Method, r.URL.Path)
            http.Error(w, "Forbidden", http.StatusForbidden)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// CustomTransport – добавляет кастомный заголовок к исходящим запросам
type CustomTransport struct {
    Transport http.RoundTripper
    Pool      *sync.Pool
}

func (t *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    // Получаем объект из пула
    pooledReq := t.Pool.Get().(*http.Request)
    *pooledReq = *req // Копируем данные

    pooledReq.Header.Set("X-Custom-Header", "GoMiddleware")

    log.Printf("🛠️ Добавлен заголовок X-Custom-Header для %s %s", pooledReq.Method, pooledReq.URL)

    resp, err := t.Transport.RoundTrip(pooledReq)

    // Возвращаем объект в пул
    t.Pool.Put(pooledReq)

    return resp, err
}

// secureHandler – защищенный обработчик
func secureHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("🔒 Secure Content"))
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/secure", secureHandler)

    // Комбинируем middleware: сначала логирование, потом аутентификация
    handler := authMiddleware(loggingMiddleware(mux))

    // Создаем пул для http.Request
    requestPool := &sync.Pool{
        New: func() interface{} {
            return new(http.Request)
        },
    }

    // Создаем HTTP-клиента с кастомным транспортом
    client := &http.Client{
        Transport: &CustomTransport{
            Transport: http.DefaultTransport,
            Pool:      requestPool,
        },
    }

    // Пример использования клиента
    go func() {
        time.Sleep(2 * time.Second) // Ждем, пока сервер запустится
        req, _ := http.NewRequest("GET", "https://httpbin.org/get", nil)
        resp, err := client.Do(req)
        if err != nil {
            log.Printf("❌ Ошибка выполнения запроса: %v", err)
            return
        }
        resp.Body.Close()
        log.Printf("✅ Клиент получил ответ: %s", resp.Status)
    }()

    log.Println("🛡️ Сервер с несколькими middleware запущен на :8080")
    if err := http.ListenAndServe(":8080", handler); err != nil {
        log.Fatalf("❌ Ошибка запуска сервера: %v", err)
    }
}

Заключение

Надеюсь, эта статья принесла вам пользу. Если у вас есть вопросы, идеи или вы хотите поделиться своими наработками, пишите в комментариях. Всегда рад обсудить и помочь!


19 ноября в Otus пройдет урок на тему «Паттерны отказоустойчивости и масштабируемости микросервисной архитектуры», записаться на него можно на странице курса "Software Architect".

А все лучшие практики, инструменты и подходы к построению архитектуры приложений можно изучить на практических курсах. Подробности в каталоге.