Merge branch 'lecture6_http' into 'master'

lecture 6: http and context

See merge request slon/shad-go-private!23
This commit is contained in:
Fedor Korotkiy 2020-04-02 11:42:31 +00:00
commit c67224b4b2
17 changed files with 784 additions and 0 deletions

View file

@ -0,0 +1,43 @@
package cancelation
import (
"context"
"time"
)
func SimpleCancelation() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(5 * time.Second)
cancel()
}()
if err := doSlowJob(ctx); err != nil {
panic(err)
}
}
func SimpleTimeout() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := doSlowJob(ctx); err != nil {
panic(err)
}
}
func doSlowJob(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// perform a portion of slow job
time.Sleep(1 * time.Second)
}
}
}
// OMIT

View file

@ -0,0 +1,35 @@
package httpserver
import (
"bufio"
"fmt"
"net/http"
"os"
"time"
)
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())
}
}
}

View file

@ -0,0 +1,18 @@
package httpserver
import (
"net/http/httptest"
"testing"
)
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)
}
}

View file

@ -0,0 +1,36 @@
package httpserver
import (
"context"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
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()
}

View file

@ -0,0 +1,43 @@
package main
import (
"crypto/tls"
"crypto/x509"
"errors"
"net/http"
"net/http/cookiejar"
"time"
)
func main() {
// все куки записанные в этот Jar будут передаваться
// и изменяться во всех запросах
cj, _ := cookiejar.New(nil)
client := &http.Client{
Timeout: 1 * time.Second,
Jar: cj,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) > 20 {
return errors.New("too many redirects")
}
return nil
},
Transport: &http.Transport{
// резмер буферов чтения и записи (4KB по-умолчанию)
WriteBufferSize: 32 << 10,
ReadBufferSize: 32 << 10,
// конфиг работы с зашифрованными соединениями
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{},
RootCAs: &x509.CertPool{},
// только для отладки!
InsecureSkipVerify: true,
// ..
},
// ...
},
}
_ = client
}

View file

@ -0,0 +1,31 @@
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("POST", "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)
}
fmt.Println(resp.StatusCode)
}

View file

@ -0,0 +1,49 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
handler := func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("pong"))
}
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
}
}

View file

@ -0,0 +1,47 @@
package httptest
import (
"context"
"io/ioutil"
"net/http"
"strconv"
)
const (
BaseURLProd = "https://github.com/api"
)
type APIClient struct {
baseURL string
httpc *http.Client
}
func NewAPICLient(baseURL string) *APIClient {
if baseURL == "" {
baseURL = BaseURLProd
}
return &APIClient{
baseURL: baseURL,
httpc: new(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 := ioutil.ReadAll(resp.Body)
if err != nil {
return 0, err
}
return strconv.Atoi(string(body))
}
// OMIT

View file

@ -0,0 +1,30 @@
package httptest
import (
"context"
"net/http"
"net/http/httptest"
"testing"
)
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)
}
}
// OMIT

View file

@ -0,0 +1,38 @@
package main
import (
"crypto/tls"
"fmt"
"net/http"
"sync"
)
func main() {
urls := []string{"https://golang.org/doc", "https://golang.org/pkg", "https://golang.org/help"}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
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
}
fmt.Printf("%s - %d\n", url, resp.StatusCode)
}(url)
}
wg.Wait()
}

View file

@ -0,0 +1,39 @@
package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"sync"
)
func main() {
urls := []string{"https://golang.org/doc", "https://golang.org/pkg", "https://golang.org/help"}
client := &http.Client{
Transport: &http.Transport{
MaxConnsPerHost: 100,
},
}
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(ioutil.Discard, resp.Body)
fmt.Printf("%s - %d\n", url, resp.StatusCode)
}(url)
}
wg.Wait()
}

View file

@ -0,0 +1,30 @@
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()
}

View file

@ -0,0 +1,221 @@
http и context
Лекция 6
Георгий Зуйков
* Имеем из коробки
- HTTP клиент (HTTP/1.x, HTTP/2)
- HTTP сервер (с поддержкой TLS)
- Тестирование
* net/http
* net/http
Содержит в себе:
- HTTP клиент и сервер
- Константы статусов и мотодов HTTP
- Sentinel ошибки
- Вспомогательные функции для составления и разбора HTTP запросов
* HTTP клиент
* Делаем запрос
.play simpleget/simpleget.go
Доступные функции:
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)
* Делаем более лучший запрос
.play custompost/custompost.go
`http.DefaultClient` - базовый глобальный клиент с настройками по-умолчанию.
* http.Client
type Client struct {
// Определяет механизм выполнения каждого запроса
Transport RoundTripper
// Функция для кастомной проверки редиректов
// По-умолчанию - максимум 10 редиректов
CheckRedirect func(req *Request, via []*Request) error
// Хранилище кук
Jar CookieJar
// Таймаут любого запроса от клиента
// Считается все время от соединения до конца вычитывания тела
// 0 - без таймаута
Timeout time.Duration
}
* Тонкая настройка клиента
.code customclient/customclient.go /func main/,/^}/
* Keepalive
.play keepalive/naive/naive.go /func main/,/^}/
Как-то медленно
* Keepalive
.play keepalive/advanced/advanced.go /func main/,/^}/
Что-то лыжи не едут
* Keepalive
.play keepalive/correct/correct.go /func main/,/^}/
Вот теперь всё как надо!
* HTTP сервер
* Простой HTTP сервер
.code simpleserver/simpleserver.go /func RunServer/,/^}/
.code simpleserver/simpleserver.go /func RunTLSServer/,/^}/
http.Handler - интерфейс, описывающий функцию для обработки HTTP запроса.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
* Роутинг
.code simpleserver/router.go /func RunServerWithRouting/,/OMIT/
* Что нужно знать
- Запуск сервера - блокирующая операция
- Каждый входящий HTTP запрос обрабатывается в отдельной горутине (следите за дескрипторами)
- Паника внутри одного хэндлера не приводит к остановке всего сервера
- Неотловленная паника закрывает HTTP соединение
- Хедеры ответа нельзя менять после вызова `ResponseWriter.WriteHeader` или `ResponseWriter.Write`
* Middleware
.code simpleserver/middleware.go /func RunServerWithMiddleware/,/^}/
* Middleware
.code simpleserver/middleware.go /func UnifiedErrorMiddleware/,/^}/
* Graceful shutdown
.play gracefulshutdown/gracefulshutdown.go /func run()/,/^}/
* context
Контекст - инкапсулированное состояние определенной части приложения.
Позволяет оповещать операции о необходимости завершения и/или передавать контекстозависимые значения между различными частями приложения.
Контексты наследуемы, при отмене закрывается родительский и все дочерние контексты.
type Context interface {
// Возвращает время, когда операция будет оповещена о необходимости завершения
Deadline() (deadline time.Time, ok bool)
// Возвращает канал, который будет закрыт при необходимости завершить операцию
// Служит в качестве инструмента оповещения об отмене
Done() <-chan struct{}
// Если Done не закрыт - возвращает nil.
// Если Done закрыт, Err ошибку с объяснением причины:
// - Canceled - контекст был отменен
// - DeadlineExceeded - наступил дедлайн.
// После возврашения не nil ошибки Err всегда возвращает данную ошибку.
Err() error
// Позволяет получить произвольный объект из контекста
Value(key interface{}) interface{}
}
Типы контекстов:
- TODO
- Background
- Cancel
- Deadline
* Отменяем операции
.code context/cancelation/cancelation.go /func SimpleCancelation()/,/OMIT/
* context в библиотеках Go
По соглашению `Context` всегда передается первым параметром в функции, обычно именуясь `ctx`.
database/sql.(*DB).QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)
database/sql.(*DB).ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error)
net/http.NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error)
golang.org/x/sync/errgroup.WithContext(ctx context.Context) (*Group, context.Context)
...
Быстрый пример:
ctx, cancel := context.WithTimeout(context.Background(), 1 * time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "http://loremipsum.com", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
// возможно тут будет DeadlineExceeded
}
* Контекст в HTTP сервере
.code context/httpserver/httpserver.go /type ReqTimeContextKey/,/^}/
* Контекст в HTTP сервере
.code context/httpserver/handler.go /type handler/,/^}/
* httptest
* httptest
Содержит хелперы для удобного написания тестов для HTTP клиентов и серверов.
// стартует новый локальный HTTP сервер на слуйчаном свободном порту
httptest.NewServer(http.Handler)
// объект, реализующий интерфейс http.ResponseWriter и дающий доступ к результатам ответа
httptest.NewRecorder()
// возвращает объект, готовый к передаче прямо в http.Handler
httptest.NewRequest(method, target string, body io.Reader) *http.Request
* Пример тестирования клиента
.code httptest/code.go /^const \(/,/OMIT/
* Пример тестирования клиента
.code httptest/code_test.go /func TestGetReposCount/,/OMIT/
* Пример тестирования сервера
.code context/httpserver/handler_test.go /func TestHandlerServeHTTP/,/^}/
* Полезные библиотеки и фреймворки
Роутеры:
.link https://github.com/go-chi/chi
.link https://github.com/julienschmidt/httprouter
.link https://github.com/gorilla/mux
Фреймворки:
.link https://github.com/labstack/echo
.link https://github.com/gin-gonic/gin
.link https://github.com/gofiber/fiber

View file

@ -0,0 +1,14 @@
package main
import (
"fmt"
"net/http"
)
func main() {
resp, err := http.Get("https://golang.org")
if err != nil {
panic(err)
}
fmt.Println(resp.StatusCode)
}

View file

@ -0,0 +1,50 @@
package simpleserver
import (
"errors"
"net/http"
)
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)
}
}
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)
}
}

View file

@ -0,0 +1,33 @@
package simpleserver
import (
"net/http"
)
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"))
}
// OMIT

View file

@ -0,0 +1,27 @@
package simpleserver
import (
"net/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)
}
}