| Уровень модели OSI | Протоколы |
|---|---|
| Прикладной | DNS, DHCP, FTP, HTTP, HTTPS, LDAP, NTP, IMAP, POP, SSH, SMTP, NFS |
| Представления | JPEG, MIDI, MPEG, TIFF, GIF, SSL, ASCII |
| Сеансовый | SQL, ZIP, NetBIOS, PAP |
| Транспортный | TCP, UDP |
| Сетевой | ICMP, IGMP, IPsec, IPv4, IPv6, IPX, RIP |
| Канальный | ARP, HDLC, SLIP, PPP, ATM, CDP, FDDI, MPLS, STP |
| Физический | Bluetooth, Ethernet, DSL, ISDN, 802.11, Wi-Fi |
wget и curl из мира Unix. Да и браузер — это клиент сетевого взаимодействия.net/http стандартной библиотеки, который позволяет не только создавать HTTP-сервер, но и выполнять HTTP-запросы от лица клиента.http.ListenAndServe(addr string, handler Handler) error. Она начинает слушать сетевой порт по указанному адресу, разбирает запросы и передаёт их обработчикам http.Handler. Запросы могут обрабатываться параллельно: для каждого из них создаётся отдельная горутина. addr содержит IP-адрес компьютера, на котором будет создан сервер, и номер порта. Записывается в формате IP-адрес:порт — например, 127.0.0.1:8080 или 192.168.1.101:55121. Вместо 127.0.0.1 можно указать localhost, который по умолчанию будет перенаправлять на этот IP-адрес. Поскольку у компьютера обычно несколько IP-адресов и нужно, чтобы сервер был доступен с каждого из них, лучше указать 0.0.0.0:порт или просто :порт. http.ListenAndServe() останавливает выполнение текущего потока до момента, пока не возникнет ошибка или программа не завершит свою работу. В случае неудачного запуска функция возвращает ошибку — error. Например, указанный порт уже прослушивается другим приложением.package main
import "net/http"
func main() {
err := http.ListenAndServe(`:8080`, nil)
if err != nil {
panic(err)
}
} panic: listen tcp :8080: bind: address already in use — «адрес уже используется».http://localhost:8080. Страница должна показать 404 page not found. Это значит, что сервер работает, но, так как отсутствуют обработчики запросов, он пока не может ответить ничего толкового.http. ListenAndServe("127.0.0.1:3000", nil), а другая — http. ListenAndServe("192.168.1.101:3000", nil), где 192.168.1.101 — другой IP-адрес этого же компьютера. Будут ли эти программы работать одновременно?http.Handler — это интерфейсный тип с единственной функцией ServeHTTP(...). Она будет вызвана для обработки любого HTTP-запроса.http.ResponseWriter — интерфейс потоковой записи, куда обработчик может писать ответные данные для клиента;http.Request — данные запроса.type Handler interface {
ServeHTTP(ResponseWriter, *Request)
} ResponseWriter. Здесь методы Header и WriteHеader используются для работы с заголовками, а метод Write выводит тело ответа:type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
} *Request — это указатель на структуру, которая содержит информацию о заголовках HTTP-запроса и данные, отправленные клиентом.package main
import "net/http"
type MyHandler struct{}
func (h MyHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
data := []byte("Привет!")
res.Write(data)
}
func main() {
var h MyHandler
err := http.ListenAndServe(`:8080`, h)
if err != nil {
panic(err)
}
} Привет!. Недостаток в том, что эта строка выводится в ответ на любой запрос: http://localhost:8080/, http://localhost:8080/api, http://localhost:8080/users/cabinet. А можно сделать так, чтобы ответ был разным в зависимости от пути, указанного в запросе.Handler для всех запросов, обработчик сильно разрастётся и его трудно будет поддерживать. Чтобы этого избежать, применяют маршрутизацию запросов.http.ServeMux. Метод ServeHTTP() для этой структуры прописан в стандартной библиотеке. http.ServeMux — это одновременно обработчик и мультиплексор, распределяющий задачи обработки другим http.Handler. Он смотрит на URL запроса, ищет совпадения в списке зарегистрированных URL (паттерн) и вызывает соответствующий обработчик.http.DefaultServeMux, который имеет тип *http.ServeMux. Например, в том коде, где вы запускали ListenAndServe() с параметром nil, работает именно этот маршрутизатор.http.DefaultServeMux можно функцией http.HandleFunc(pattern string, handler func(ResponseWriter, *Request)).package main
import "net/http"
func mainPage(res http.ResponseWriter, req *http.Request) {
res.Write([]byte("Привет!"))
}
func apiPage(res http.ResponseWriter, req *http.Request) {
res.Write([]byte("Это страница /api."))
}
func main() {
http.HandleFunc(`/api`, apiPage)
http.HandleFunc(`/`, mainPage)
err := http.ListenAndServe(`:8080`, nil)
if err != nil {
panic(err)
}
} http://localhost:8080/api будет обрабатывать функция apiPage(). Все остальные запросы будут приходить обработчику по умолчанию mainPage(), так как у него указан маршрут /. HandleFunc() указан маршрут /api без слеша в конце. Это значит, что запросы http://localhost:8080/api/ и http://localhost:8080/api/getid будет обрабатывать функция mainPage. /api указать маршрут /api/, то эти запросы придут функции apiPage() — из-за последнего слеша этот маршрут будет перехватывать маршруты с префиксом /api/. При этом запрос http://localhost:8080/api будет перенаправляться на http://localhost:8080/api/ и тоже обрабатываться функцией apiPage(). // иcпользование этого фрагмента приведёт к ошибке
// panic: http: multiple registrations for /api
http.HandleFunc(`/api`, apiPage)
http.HandleFunc(`/`, mainPage)
http.HandleFunc(`/api`, mainPage) HandleFunc(), нельзя использовать регулярные выражения и маски, так как маршрутизатор стандартной библиотеки не проводит их разбор. // этот вариант будет работать так же,
// как вариант в примере
http.HandleFunc(`/`, mainPage)
http.HandleFunc(`/api`, apiPage) init() стоит вызов http.HandleFunc("/backdoor", backdoorPage). Будет ли твой сервер откликаться на /backdoor?backdoorPage из импортируемого пакета. http.DefaultServeMux. Лучше создать свою переменную-маршрутизатор функцией NewServeMux() *ServeMux и вызвать для неё методы HandleFunc() с маршрутами и обработчиками.mux := http.NewServeMux()
mux.HandleFunc(`/api/auth`, authHandler)
mux.HandleFunc(`/api/`, apiHandler)
// ...
err := http.ListenAndServe(`:8080`, mux)
// ... Handler.ServeHTTP() и функции типа http.HandlerFunc() передают в параметрах переменную интерфейсного типа http.ResponseWriter и переменную типа *http.Request с информацией о запросе. Покажем, как с ними работать.http.Request. type Request struct {
// указаны некоторые поля структуры
Method string
URL *url.URL
Header Header
Body io.ReadCloser
ContentLength int64
Host string
// ...
} Method содержит метод HTTP-запроса. Чтобы избежать ошибок, лучше использовать предопределённые константы пакета net/http:const (
MethodGet = "GET"
MethodHead = "HEAD"
MethodPost = "POST"
MethodPut = "PUT"
MethodPatch = "PATCH"
MethodDelete = "DELETE"
MethodConnect = "CONNECT"
MethodOptions = "OPTIONS"
MethodTrace = "TRACE"
) func GetHandler(w http.ResponseWriter, r *http.Request) {
// этот обработчик принимает только запросы, отправленные методом GET
if r.Method != http.MethodGet {
http.Error(w, "Only GET requests are allowed!", http.StatusMethodNotAllowed)
return
}
// продолжаем обработку запроса
// ...
} Header в виде мапы map[string][]string. Так как заголовок может содержать несколько значений, то используется мапа слайсов, а не строк:(h Header) Values(key string) []string возвращает слайс значений указанного заголовка;(h Header) Get(key string) string возвращает первое значение.URL. Query() url.Values, который возвращает значение типа type Values map[string][]string. Метод (v Values) Get(key string) string возвращает первое значение. Если запрашиваемый параметр не был указан, то Get() вернёт пустую строку.package main
import (
"fmt"
"net/http"
)
func mainPage(res http.ResponseWriter, req *http.Request) {
body := fmt.Sprintf("Method: %s\r\n", req.Method)
body += "Header ===============\r\n"
for k, v := range req.Header {
body += fmt.Sprintf("%s: %v\r\n", k, v)
}
body += "Query parameters ===============\r\n"
for k, v := range req.URL.Query() {
body += fmt.Sprintf("%s: %v\r\n", k, v)
}
res.Write([]byte(body))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc(`/`, mainPage)
err := http.ListenAndServe(`:8080`, mux)
if err != nil {
panic(err)
}
} http://localhost:8080/?id=12345&name=John%20Doe&filter=town&filter=country.(r *Request) FormValue(key string) string. Чтобы получить все значения параметра, используйте поле Form, которое имеет тип url.Values. В этом случае нужно предварительно вызвать метод (r *Request) ParseForm() error.Body io.ReadCloser и читается стандартными методами — например, body, err := io.ReadAll(req.Body). Вызывать Body.Close() не нужно, так как это автоматически делает сервер.http.ResponseWriter для вывода ответа сервера. Но можно записывать не только тело ответа, но и возвращать нужные заголовки и код статуса. Интерфейсный тип http.ResponseWriter содержит следующие методы:type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
} Header() возвращает объект типа http.Header. Как вы уже знаете, он состоит из имён и значений заголовков. Для записи нужных заголовков можно использовать такие методы:(h Header) Set(key, value string) — установить заголовок;(h Header) Add(key, value string) — добавить значение заголовка;(h Header) Del(key string) — удалить заголовок.WriteHeader() записывает в ответ сервера текущие заголовки и код статуса. После вызова этой функции изменения заголовков не будут влиять на ответ сервера. Как обычно, из правил есть исключения: например, значения заголовков Trailer можно устанавливать в самом конце. Если вызов WriteHeader отсутствует, то заголовки автоматически запишутся с кодом статуса 200. net/http для кодов статуса определены соответствующие константы. Вот некоторые из них:StatusOK = 200
StatusBadRequest = 400
StatusUnauthorized = 401
StatusForbidden = 403
StatusNotFound = 404
StatusMethodNotAllowed = 405
StatusInternalServerError = 500 Write, вы уже видели. Но получается, что параметр типа http.ResponseWriter удовлетворяет интерфейсному типу io.Writer, а значит, можно выводить тело ответа и таким образом:io.WriteString(res, "Привет!")
fmt.Fprint(res, "Привет!") type Subj struct {
Product string `json:"name"`
Price int `json:"price"`
}
func JSONHandler(w http.ResponseWriter, req *http.Request) {
// собираем данные
subj := Subj{"Milk", 50}
// кодируем в JSON
resp, err := json.Marshal(subj)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// устанавливаем заголовок Content-Type
// для передачи клиенту информации, кодированной в JSON
w.Header().Set("content-type", "application/json")
// устанавливаем код 200
w.WriteHeader(http.StatusOK)
// пишем тело ответа
w.Write(resp)
} ResponseWriter. Функция Error(w ResponseWriter, error string, code int) возвращает ответ в виде текста с cообщением об ошибке и указанным кодом статуса.GET-запроса обработчик будет возвращать форму авторизации, а при передаче данных методом POST — проверять логин и пароль.package main
import (
"io"
"net/http"
)
const form = `<html>
<head>
<title></title>
</head>
<body>
<form action="/" method="post">
<label>Логин</label><input type="text" name="login">
<label>Пароль<input type="password" name="password">
<input type="submit" value="Login">
</form>
</body>
</html>`
func Auth(login, password string) bool {
return login == `guest` && password == `demo`
}
func mainPage(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
login := r.FormValue("login")
password := r.FormValue("password")
if Auth(login, password) {
io.WriteString(w, "Добро пожаловать!")
} else {
http.Error(w, "Неверный логин или пароль", http.StatusUnauthorized)
}
return
} else {
io.WriteString(w, form)
}
}
func main() {
err := http.ListenAndServe(`:8080`, http.HandlerFunc(mainPage))
if err != nil {
panic(err)
}
} mainPage приведена к типу http.HandlerFunc, который выступает адаптером и позволяет использовать функцию-обработчик как http.Handler.middleware, — об этом расскажем в следующих уроках темы. А сейчас покажем, как сделать конвейерную обработку запросов средствами стандартной библиотеки.// middleware принимает параметром Handler и возвращает тоже Handler.
func middleware(next http.Handler) http.Handler {
// получаем Handler приведением типа http.HandlerFunc
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// здесь пишем логику обработки
// например, разрешаем запросы cross-domain
// w.Header().Set("Access-Control-Allow-Origin", "*")
// ...
// замыкание: используем ServeHTTP следующего хендлера
next.ServeHTTP(w, r)
})
} middleware() принимает и возвращает значения типа http.Handler. Для связи маршрута и http.Handler используется функция Handle(pattern string, handler Handler). Соответственно, функцию-обработчик, которую будем передавать в middleware(), нужно также привести к http.Handler.func rootHandle(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Привет"))
}
func main() {
http.Handle("/", middleware(http.HandlerFunc(rootHandle)))
//...
} type Middleware func(http.Handler) http.Handler
func Conveyor(h http.Handler, middlewares ...Middleware) http.Handler {
for _, middleware := range middlewares {
h = middleware(h)
}
return h
}
func main() {
http.Handle("/", Conveyor(http.HandlerFunc(rootHandle), middleware1, middleware2, middleware3))
// ...
} http.Error(), http.NotFound(), http.Redirect() из пакета net/http. Функция http.Error() уже упоминалась в этом уроке, a NotFound(w ResponseWriter, r *Request) возвращает конкретную ошибку 404.Redirect(w ResponseWriter, r *Request, url string, code int) можно использовать для перенаправления следующим образом:func redirect(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://yandex.ru/", http.StatusMovedPermanently)
}
func main() {
http.HandleFunc("/search/", redirect)
log.Fatal(http.ListenAndServe(":8080", nil))
} http.Handler:NotFoundHandler() Handler — выдаёт ошибку 404;RedirectHandler(url string, code int) Handler — перенаправляет;TimeoutHandler(h Handler, dt time.Duration, msg string) Handler — выдаёт ошибку 503 Service Unavailable, если ответ не успел отправиться в течение указанного интервала времени.http.Handle("/dummy", http.RedirectHandler("https://google.com", http.StatusMovedPermanently)) net/http есть специальные возможности для работы с файлами.FileServer(root FileSystem) Handler принимает параметром переменную интерфейсного типа http.FileSystem:type FileSystem interface {
Open(name string) (File, error)
} http.Handler. Самый простой способ получить для директории переменную типа http.FileSystem — это привести путь к типу http.Dir, который поддерживает этот интерфейс. package main
import (
"net/http"
)
func main() {
// простейший сервер, которому доступны все файлы в поддиректории static
err := http.ListenAndServe(":8080", http.FileServer(http.Dir("./static")))
if err != nil {
panic(err)
}
} http://localhost:8080, то будет показано содержимое директории static в виде ссылок на файлы и поддиректории.http://localhost:8080/assets/images/image.png, то он будет искать файл ./static/assets/images/image.png.static, но при этом надо, чтобы они были привязаны не к корневому пути, а, например, к директории assets? StripPrefix(prefix string, h Handler) Handler. Она возвращает обработчик, который удаляет префикс из пути запроса.fs := http.FileServer(http.Dir("./static"))
http.Handle("/assets/", http.StripPrefix("/assets/", fs)) FileServer() для разных директорий. Если при определённом запросе нужно вернуть содержимое конкретного файла, стоит использовать функцию ServeFile(w ResponseWriter, r *Request, name string).http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request){
http.ServeFile(w, r, "./public/favicon.ico")
}) ~/dev/alice-skill. Внутри этой директории создадим следующую структуру:> ~/dev/alice-skill
|
|--- cmd
| |--- skill
|--- internal cmd/<name>, где <name> — название результирующего бинарного файла после компиляции. Бинарный файл с веб-сервером навыка Алисы будет называться просто skill (или skill.exe для Windows), поэтому в проекте есть директория cmd/skill.internal хранится код, доступный для использования только в данном проекте, — это гарантирует сам компилятор языка Go. В этой директории будут храниться модели данных и детали реализации навыка, о которых не стоит знать какому-либо внешнему коду.go.mod в корне проекта. В этом файле описывается название модуля, минимальная версия компилятора и внешние зависимости проекта. Файл можно создать командой $ go mod init <project_address>, где <project_address> — адрес проекта без префикса протокола. https://github.com/bluegopher/alice-skill. Тогда команда будет выглядеть так: $ go mod init github.com/bluegopher/alice-skill. А файл go.mod — так:module github.com/bluegopher/alice-skill
go 1.18 cmd/skill/main.go. По соглашению между разработчиками, файл, содержащий входную функцию main(), называют main.go. В файле напишем:// пакеты исполняемых приложений должны называться main
package main
import (
"net/http"
)
// функция main вызывается автоматически при запуске приложения
func main() {
if err := run(); err != nil {
panic(err)
}
}
// функция run будет полезна при инициализации зависимостей сервера перед запуском
func run() error {
return http.ListenAndServe(`:8080`, http.HandlerFunc(webhook))
}
// функция webhook — обработчик HTTP-запроса
func webhook(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
// разрешаем только POST-запросы
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// установим правильный заголовок для типа данных
w.Header().Set("Content-Type", "application/json")
// пока установим ответ-заглушку, без проверки ошибок
_, _ = w.Write([]byte(`
{
"response": {
"text": "Извините, я пока ничего не умею"
},
"version": "1.0"
}
`))
} cmd/skill команду:$ go run http://localhost:8080/. Попробуем отправить запрос:$ curl -v -X POST 'http://localhost:8080'
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Wed, 12 Oct 2022 20:42:50 GMT
< Content-Length: 233
<
{
"response": {
"text": "Извините, я пока ничего не умею"
},
"version": "1.0"
} net/http.beego.