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

от автора

Привет, любители 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».

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


ссылка на оригинал статьи https://habr.com/ru/articles/857070/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *