http
Лекция 8
Максим Иванов
Максим Иванов
HTTP (HyperText Transfer Protocol) - это протокол для передачи данных в интернете.
Работает по принципу "запрос-ответ":
- Клиент (браузер, приложение) отправляет запрос
- Сервер обрабатывает и отправляет ответ
HTTP/1.x (старая версия):
- Одно соединение = один запрос в момент времени
- Много повторяющихся заголовков → лишний трафик
- Чтобы загрузить страницу быстрее, нужно открывать много соединений
HTTP/2 (новая версия):
- Одно соединение → много параллельных запросов (мультиплексирование)
- Сжатие заголовков → экономия трафика
- Сервер может отправлять данные до запроса (server push)
Содержит в себе:
- HTTP клиент и сервер
- Константы статусов и методов HTTP
- Sentinel ошибки
- Вспомогательные функции для составления и разбора HTTP запросов
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)
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 - базовый глобальный клиент с настройками по-умолчанию.
type Client struct {
// Определяет механизм выполнения каждого запроса
Transport RoundTripper
// Функция для кастомной проверки редиректов
// По-умолчанию - максимум 10 редиректов
CheckRedirect func(req *Request, via []*Request) error
// Хранилище кук
Jar CookieJar
// Таймаут любого запроса от клиента
// Считается все время от соединения до конца вычитывания тела
// 0 - без таймаута
Timeout time.Duration
}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 }
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() }
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() }
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() }
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) } }
http.Handler - интерфейс, описывающий функцию для обработки HTTP запроса.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type ResponseWriter interface {
Header() Header
WriteHeader(statusCode int)
Write([]byte) (int, error)
}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")) }
ResponseWriter.WriteHeader или ResponseWriter.Writefunc 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) } }
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) } }
type User struct { Name string }
type userKey struct{} func GetUser(ctx context.Context) *User { u, _ := ctx.Value(userKey{}).(*User) return u }
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)) }) }
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)) }
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 } }
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() }
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()) } } }
Содержит хелперы для удобного написания тестов для HTTP клиентов и серверов.
// стартует новый локальный HTTP сервер на слуйчаном свободном порту httptest.NewServer(http.Handler) // объект, реализующий интерфейс http.ResponseWriter и дающий доступ к результатам ответа httptest.NewRecorder() // возвращает объект, готовый к передаче прямо в http.Handler httptest.NewRequest(method, target string, body io.Reader) *http.Request
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)) }
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) } }
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) } }
Клиент:
Роутеры:
github.com/julienschmidt/httprouter
Фреймворки:
32Максим Иванов