http

Лекция 8

Максим Иванов

Что такое HTTP?

HTTP (HyperText Transfer Protocol) - это протокол для передачи данных в интернете.

Работает по принципу "запрос-ответ":
- Клиент (браузер, приложение) отправляет запрос
- Сервер обрабатывает и отправляет ответ

2

HTTP/1.x vs HTTP/2

HTTP/1.x (старая версия):
- Одно соединение = один запрос в момент времени
- Много повторяющихся заголовков → лишний трафик
- Чтобы загрузить страницу быстрее, нужно открывать много соединений

HTTP/2 (новая версия):
- Одно соединение → много параллельных запросов (мультиплексирование)
- Сжатие заголовков → экономия трафика
- Сервер может отправлять данные до запроса (server push)

3

Имеем в Go из коробки

4

net/http

Содержит в себе:
- HTTP клиент и сервер
- Константы статусов и методов HTTP
- Sentinel ошибки
- Вспомогательные функции для составления и разбора HTTP запросов

5

HTTP клиент

6

Делаем запрос

package main

import (
    "fmt"
    "net/http"
)

func main() {
    resp, err := http.Get("https://golang.org")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    fmt.Println(resp.StatusCode)
}

Доступные функции:

Get(url string) (*Response, error)
Post(url, contentType string, body io.Reader) (*Response, error)
Head(url string) (*Response, error)
PostForm(url string, form url.Values) (*Response, error)
7

Улучшаем запрос

package main

import (
	"bytes"
	"fmt"
	"log"
	"net/http"
	"net/http/httputil"
)

func main() {
    body := bytes.NewBufferString("All your base are belong to us")
    req, err := http.NewRequest(http.MethodPost, "https://myapi.com/create", body)
    if err != nil {
        log.Fatal(err)
    }

    req.Header.Set("X-Source", "Zero Wing")
    repr, err := httputil.DumpRequestOut(req, true)
    if err == nil {
        fmt.Println(string(repr))
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()
    fmt.Println(resp.StatusCode)
}

http.DefaultClient - базовый глобальный клиент с настройками по-умолчанию.

8

http.Client

type Client struct {
  // Определяет механизм выполнения каждого запроса
  Transport RoundTripper

  // Функция для кастомной проверки редиректов 
  // По-умолчанию - максимум 10 редиректов
  CheckRedirect func(req *Request, via []*Request) error

  // Хранилище кук
  Jar CookieJar

  // Таймаут любого запроса от клиента
  // Считается все время от соединения до конца вычитывания тела
  // 0 - без таймаута
  Timeout time.Duration
}
9

Тонкая настройка клиента

func main() {
    // все куки записанные в этот Jar будут передаваться
    // и изменяться во всех запросах
    cj, _ := cookiejar.New(nil)
    client := &http.Client{
        Timeout: 1 * time.Second,
        Jar:     cj,
        Transport: &http.Transport{
            // резмер буферов чтения и записи (4KB по-умолчанию)
            WriteBufferSize: 32 << 10,
            ReadBufferSize:  32 << 10,
            // конфиг работы с зашифрованными соединениями
            TLSClientConfig: &tls.Config{
                Certificates: []tls.Certificate{},
                RootCAs:      &x509.CertPool{},
                // только для отладки!
                InsecureSkipVerify: true,
                // ..
            },
            // ...
        },
    }
    _ = client
}
10

Keepalive

package main

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

func main() {
    urls := []string{"https://golang.org/doc", "https://golang.org/pkg", "https://golang.org/help"}

    var wg sync.WaitGroup

    for _, url := range urls {
        wg.Add(1)

        go func(url string) {
            defer wg.Done()
            // Создаем новый клиент в каждой горутине - медленно!
            var client http.Client
            resp, err := client.Get(url)
            if err != nil {
                fmt.Printf("%s: %s\n", url, err)
                return
            }
            fmt.Printf("%s - %d\n", url, resp.StatusCode)
        }(url)
    }

    wg.Wait()
}
11

Keepalive

package main

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

func main() {
    urls := []string{"https://golang.org/doc", "https://golang.org/pkg", "https://golang.org/help"}
    client := &http.Client{}

    var wg sync.WaitGroup
    for _, url := range urls {
        wg.Add(1)
        go func(url string) {
            defer wg.Done()
            resp, err := client.Get(url)
            if err != nil {
                fmt.Printf("%s: %s\n", url, err)
                return
            }
            // Body не закрываем и не читаем - keepalive не работает!
            fmt.Printf("%s - %d\n", url, resp.StatusCode)
        }(url)
    }

    wg.Wait()
}
12

Keepalive

package main

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

func main() {
    urls := []string{"https://golang.org/doc", "https://golang.org/pkg", "https://golang.org/help"}

    client := &http.Client{}

    var wg sync.WaitGroup
    for _, url := range urls {
        wg.Add(1)

        go func(url string) {
            defer wg.Done()
            resp, err := client.Get(url)
            if err != nil {
                fmt.Printf("%s: %s\n", url, err)
                return
            }
            defer resp.Body.Close()
            _, _ = io.Copy(io.Discard, resp.Body)
            fmt.Printf("%s - %d\n", url, resp.StatusCode)
        }(url)
    }

    wg.Wait()
}
13

HTTP сервер

14

Простой HTTP сервер

func RunServer() {
    handler := func(w http.ResponseWriter, r *http.Request) {
        _, _ = w.Write([]byte("pong"))
    }

    err := http.ListenAndServe(":8080", http.HandlerFunc(handler))
    if err != nil {
        panic(err)
    }
}
func RunTLSServer() {
    handler := func(w http.ResponseWriter, r *http.Request) {
        _, _ = w.Write([]byte("pong"))
    }

    err := http.ListenAndServeTLS(":8080", "cert.crt", "private.key", http.HandlerFunc(handler))
    if err != nil {
        panic(err)
    }
}
15

Простой HTTP сервер

http.Handler - интерфейс, описывающий функцию для обработки HTTP запроса.

type Handler interface {
  ServeHTTP(ResponseWriter, *Request)
}

type ResponseWriter interface {
  Header() Header
  WriteHeader(statusCode int)
  Write([]byte) (int, error)
}
16

Роутинг

func RunServerWithRouting() {
    router := func(w http.ResponseWriter, r *http.Request) {
        switch r.RequestURI {
        case "/pong":
            pongHandler(w, r)
        case "/shmong":
            shmongHandler(w, r)
        default:
            w.WriteHeader(404)
        }
    }
    err := http.ListenAndServe(":8080", http.HandlerFunc(router))
    if err != nil {
        panic(err)
    }
}

func pongHandler(w http.ResponseWriter, r *http.Request) {
    _, _ = w.Write([]byte("pong"))
}

func shmongHandler(w http.ResponseWriter, r *http.Request) {
    _, _ = w.Write([]byte("shmong"))
}
17

Что нужно знать

18

Middleware

func RunServerWithMiddleware() {
    getOnly := func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if r.Method != http.MethodGet {
                w.WriteHeader(http.StatusMethodNotAllowed)
                return
            }
            next.ServeHTTP(w, r)
        })
    }

    handler := func(w http.ResponseWriter, r *http.Request) {
        _, _ = w.Write([]byte("pong"))
    }

    err := http.ListenAndServe(":8080", getOnly(http.HandlerFunc(handler)))
    if err != nil {
        panic(err)
    }
}
19

Middleware

func UnifiedErrorMiddleware() {
    wrapErrorReply := func(h func(w http.ResponseWriter, r *http.Request) error) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if err := h(w, r); err != nil {
                w.WriteHeader(http.StatusBadRequest)
            }
        })
    }

    handler := func(w http.ResponseWriter, r *http.Request) error {
        if r.URL.Query().Get("secret") != "FtP8lu70XjWj8Stt" {
            return errors.New("secret mismatch")
        }
        _, _ = w.Write([]byte("pong"))
        return nil
    }

    err := http.ListenAndServe(":8080", wrapErrorReply(handler))
    if err != nil {
        panic(err)
    }
}
20

Middleware с контекстом

type User struct {
    Name string
}
type userKey struct{}

func GetUser(ctx context.Context) *User {
    u, _ := ctx.Value(userKey{}).(*User)
    return u
}
21

Middleware с контекстом

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")

        if !strings.HasPrefix(token, "Bearer ") {
            w.WriteHeader(http.StatusUnauthorized)
            return
        }

        // Извлекаем имя пользователя из токена (упрощенно)
        username := strings.TrimPrefix(token, "Bearer ")

        // Создаем пользователя и добавляем в контекст
        user := &User{Name: username}
        ctx := context.WithValue(r.Context(), userKey{}, user)

        // Передаем запрос с обновленным контекстом дальше
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}
22

Использование контекста

func ProtectedHandler(w http.ResponseWriter, r *http.Request) {
    user := GetUser(r.Context())
    if user == nil {
        w.WriteHeader(http.StatusUnauthorized)
        return
    }

    w.Write([]byte("Hello, " + user.Name))
}
23

Graceful shutdown

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	_, _ = w.Write([]byte("pong"))
}

func run() error {
    srv := &http.Server{
        Addr:    ":8080",
        Handler: http.HandlerFunc(handler),
    }
    serveChan := make(chan error, 1)
    go func() {
        serveChan <- srv.ListenAndServe()
    }()

    stop := make(chan os.Signal, 1)
    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
    select {
    case <-stop:
        fmt.Println("shutting down gracefully")
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        return srv.Shutdown(ctx)

    case err := <-serveChan:
        return err
    }
}
24

Контекст в HTTP сервере

type ReqTimeContextKey struct{}

func runServer() error {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    go func() {
        stop := make(chan os.Signal, 1)
        signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
        <-stop
        cancel()
    }()

    srv := &http.Server{
        Addr:    ":8080",
        Handler: handler{},
        BaseContext: func(_ net.Listener) context.Context {
            ctx = context.WithValue(ctx, ReqTimeContextKey{}, time.Now())
            return ctx
        },
    }

    return srv.ListenAndServe()
}
25

Контекст в HTTP сервере

type handler struct{}

func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    reqTime := ctx.Value(ReqTimeContextKey{}).(time.Time)
    defer func() {
        fmt.Printf("handler finished in %s", time.Since(reqTime))
    }()

    fd, _ := os.Open("core.c")
    defer fd.Close()

    scanner := bufio.NewScanner(fd)
    for scanner.Scan() {
        select {
        case <-ctx.Done():
            fmt.Println(ctx.Err())
            return
        default:
            _, _ = w.Write(scanner.Bytes())
        }
    }
}
26

httptest

27

httptest

Содержит хелперы для удобного написания тестов для HTTP клиентов и серверов.

// стартует новый локальный HTTP сервер на слуйчаном свободном порту
httptest.NewServer(http.Handler)
// объект, реализующий интерфейс http.ResponseWriter и дающий доступ к результатам ответа
httptest.NewRecorder()
// возвращает объект, готовый к передаче прямо в http.Handler
httptest.NewRequest(method, target string, body io.Reader) *http.Request
28

Пример тестирования клиента

const (
    BaseURLProd = "https://github.com/api"
)

type APIClient struct {
    baseURL string
    httpc   *http.Client
}

func (c *APIClient) GetReposCount(ctx context.Context, userID string) (int, error) {
    url := c.baseURL + "/users/" + userID + "/repos/count"
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := c.httpc.Do(req)
    if err != nil {
        return 0, err
    }
    defer resp.Body.Close()
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return 0, err
    }

    return strconv.Atoi(string(body))
}
29

Пример тестирования клиента

func TestGetReposCount(t *testing.T) {
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        _, _ = w.Write([]byte("42"))
    }))

    defer srv.Close()

    client := NewAPICLient(srv.URL)
    count, err := client.GetReposCount(context.Background(), "007")
    if err != nil {
        t.Errorf("unexpected error: %s", err)
    }

    expectedCount := 42
    if count != expectedCount {
        t.Errorf("expected count to be: %d, got: %d", expectedCount, count)
    }
}
30

Пример тестирования сервера

func TestHandlerServeHTTP(t *testing.T) {
    w := httptest.NewRecorder()
    r := httptest.NewRequest("GET", "/", nil)

    h := handler{}
    h.ServeHTTP(w, r)

    if w.Code != 200 {
        t.Errorf("expected HTTP 200, got: %d", w.Code)
    }
}
31

Полезные библиотеки и фреймворки

Клиент:

Роутеры:

Фреймворки:

32

Thank you

Максим Иванов

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)