HTTP
Протокол передачи гипертекста (HTTP) является неотъемлемой частью интернета. Он работает на прикладном уровне и представляет собой основу для обмена данными между клиентом и сервером. Когда пользователь вводит URL в браузере, отправляется HTTP-запрос к серверу, который затем возвращает соответствующий ответ. Этот процесс охватывает широкий спектр ресурсов, от простых HTML-документов до сложных JavaScript-файлов, изображений и стилей.
Процесс коммуникации в протоколе HTTP начинается с того, что клиент (чаще всего это веб-браузер) отправляет запрос на сервер, указывая адрес ресурса в формате URL. Сервер в ответ отсылает соответствующие данные, которые затем отображаются пользователю. Примером такого взаимодействия является ситуация, когда пользователь вводит адрес "www.google.com" в адресной строке браузера. В ответ сервер Google отправляет главную страницу поисковика, которая затем появляется на экране пользователя.
URL
URL (Uniform Resource Locator) представляет собой адрес ресурса, который используется для поиска веб-сервера и указания запрашиваемого контента. URL состоит из нескольких компонентов, каждый из которых имеет свою роль. Схема (например, http
или https
) определяет протокол, по которому будет происходить передача данных. Авторизация, хотя и является необязательной частью URL, может включать в себя имя пользователя и пароль для доступа к защищенным ресурсам. Хостнейм и порт указывают на сервер и его порт, а путь сообщает, где именно на сервере находится нужный ресурс. Запрос, который начинается с символа ?
, включает параметры, передаваемые серверу, а фрагмент, начинающийся с #
, указывает на конкретную часть документа или ресурса.
Пример URL: https://username:password@www.example.com:8080/path?query=value#section
HTTP-запросы
HTTP-запросы — это сообщения, которое клиент отправляет серверу с просьбой предоставить запрашиваемый ресурс. Структура запроса состоит из метода, целевого ресурса, заголовков и тела. Метод HTTP-запроса указывает, что нужно сделать с целевым ресурсом. Например, метод GET сообщает серверу, что клиент хочет получить данные, связанные с ресурсом, в то время как метод POST указывает на необходимость отправки данных на сервер.
Заголовки запроса содержат метаданные, такие как размер тела запроса или его формат. Например, если клиент отправляет изображение на сервер, то в теле запроса будет содержаться изображение, а в заголовке Content-Length
– размер передаваемых данных.
В языке Go для работы с HTTP-запросами используется пакет net/http, который поддерживает самые распространенные HTTP-методы, определенные в стандартах RFC 7231 и RFC 5789. Среди них можно выделить такие методы, как GET
для получения ресурса, POST
для отправки данных, PUT
для их обновления и другие.
HTTP-ответы
Когда сервер завершает обработку запроса, он отправляет клиенту HTTP-ответ, содержащий статус выполнения запроса, данные и дополнительные метаданные. Ответы могут быть классифицированы по статусным кодам, которые дают представление о результате обработки запроса.
Ответ состоит из нескольких частей: стартовой строки, заголовков и тела. Стартовая строка включает в себя HTTP-версию, статусный код и текстовое описание результата. Например, строка HTTP/1.1 200 OK
означает, что запрос был успешно обработан, и сервер вернул запрашиваемые данные.
Заголовки ответа предоставляют метаданные о содержимом ответа, такие как тип данных или время кэширования. Например, заголовок Content-Type
может указывать, что ответ содержит HTML-документ или JSON-данные. Тело ответа содержит сам контент, например, HTML-страницу или файл изображения.
Пример HTTP-ответа:
Статусные коды HTTP-ответов
Статусные коды HTTP-ответов классифицируются на пять основных категорий, каждая из которых обозначает определенный тип результата:
- Информационные ответы (100-199) информируют клиента о том, что запрос был принят, и сервер ожидает дальнейших действий.
- Успешные ответы (200-299) подтверждают успешное выполнение запроса. Например, код
200 OK
означает, что сервер успешно обработал запрос. - Перенаправления (300-399) просят клиента сделать другой запрос, как правило, по другому адресу URI.
- Ошибки клиента (400-499) указывают на ошибки со стороны клиента. Например, запрос несуществующей страницы или попытка
- Ошибки сервера (500-599) указывают на внутренние проблемы серверной стороны, такие как сбой в работе или временная недоступность ресурса.
Клиент-серверное взаимодействие
HTTP-клиент
Пакет net/http
предоставляет структуру http.Client
для создания и настройки HTTP-клиента, который используется для отправки HTTP-запросов и получения ответов от веб-серверов:
Детально рассмотрим каждое из полей структуры http.Client:
- Transport определяет механизм выполнения запросов. В большинстве случаев используется стандартный объект http.DefaultTransport, который обрабатывает все части HTTP-запроса, включая установление соединения, отправку запроса и получение ответа. Однако допускается создавать собственные реализации интерфейса
RoundTripper
для более тонкой настройки поведения клиента, например, для использования прокси-сервера. - CheckRedirect управляет поведением клиента при перенаправлении (например, при 301 или 302 статусах). Вы можете настроить ее так, чтобы определенные перенаправления были разрешены или отклонены.
- Jar используется для управления cookies, которые клиент может получать от серверов и отправлять обратно в последующих запросах. По умолчанию используется http.CookieJar, который управляет cookies в автоматическом режиме. Это позволяет клиенту сохранять состояние между запросами, что важно при работе с сессиями.
- Timeout задает максимальное время, которое клиент будет ожидать получения ответа от сервера. Если сервер не отвечает в течение этого времени, запрос будет отменен с ошибкой. Такое поведения позволяет избежать зависания клиента при плохих сетевых соединениях.
Запросы GET и POST
Самый простой способ отправить GET-запрос средствами Go – это указать URL запрашиваемого ресурса в качестве аргумента функции http.Get:
В результате сервер вернет ответ, который можно прочитать с помощью функции io.ReadAll и вывести на экран:
Ответ от сервера задается структурой http.Response, которая содержит множество полей (статус, код статуса, протокол, заголовок и тд).
Тело ответа (response.Body) представляет собой структуру io.ReadCloser, поэтому с ним можно взаимодействовать при помощи функций пакета io. При этом важно вызывать отложенное закрытие тела ответа с помощью defer для избежания утечек ресурсов.
Встроенные методы http.Get и http.Post довольны просты в использовании и справляются с базовыми задачами, но иногда их возможностей недостаточно. Для более комплексных задач, таких как добавление пользовательских заголовков, настройка прокси, установка времени ожидания и других используются два объекта: структура http.Client и функция http.NewRequest, на которых основаны методы http.Get и http.Post. Чтобы в этом убедиться, посмотрим реализацию функции http.Get в пакете net/http:
Как можно видеть, метод http.Get() создает новый объект http.Request с помощью функции http.NewRequest(), а затем использует метод http.Client.Do для его отправки.
Давайте напишем POST-запрос с использованием http.Client и функции http.NewRequest:
Для начала мы подготавливаем наш запрос, создавая структуру клиента и тело запроса в виде JSON. Далее происходит создание объекта http.Request с помощью функции http.NewRequest со следующими аргументами: HTTP-метод, URL, тело запроса типа io.Reader
. Обратите внимание, что вместо названий HTTP-методов в виде строк (например, "GET", "POST") можно использовать соответствующие им константы пакета net/http.
Следующий шаг – с помощью конструкции request.Header.Set указать в заголовке тип контента, который мы хотим отослать. В данном случае ограничимся заданием объекта JSON в виде слайса байт, но для полноценной работы с JSON предпочтительнее воспользоваться пакетом encoding/json, который детально рассматривается в статье Эффективная работа с JSON в Go.
Далее отправляем запрос серверу с помощью вызова у клиента метода Do и записываем ответ (http.Response) в переменную response, не забыв обработать ошибку и вызвать отложенное закрытие тела ответа.
В завершение считываем содержимое ответа с помощью io.ReadAll и выводим его на экран.
Отправка запросов
Поле Transport в структуре http.Client позволяет настроить механизм, используемый для выполнения HTTP-запросов. Поле представляет собой интерфейс RoundTripper
, который содержит метод RoundTrip(*http.Request) (*http.Response, error). Этот интерфейс отвечает за фактическое выполнение запроса, установление соединений, обработку прокси, настройку времени ожидания и прочие аспекты сетевого взаимодействия.
Для изменения поведения HTTP-запросов, например, добавления логирования, собственных заголовков или прокси, нужно реализовать интерфейс RoundTripper
:
В этом примере мы создали собственную реализацию CustomTransport
, которая логирует каждый запрос перед его отправкой и добавляет кастомный заголовок X-Custom-Header
в запрос.
Одной из частых задач является настройка прокси-сервера для HTTP-запросов. Для этого можно использовать структуру http.ProxyURL
, которая позволяет указать URL прокси-сервера:
Перенаправление запросов
Поле CheckRedirect
в структуре http.Client
используется для управления поведением клиента при получении ответа на запрос с перенаправлением (например, HTTP-статусы 3xx). Оно представляет собой функцию, которая вызывается каждый раз, когда происходит перенаправление. Эта функция принимает два аргумента: запрос для выполнения (*http.Request
) и срез ответов ([]*http.Response
), которые уже были получены в процессе перенаправлений.
Самый простой сценарий использования поля CheckRedirect
— это ограничение числа перенаправлений для избежания зацикливания или бесконечного выполнения запросов:
Таким образом, при достижении 5 перенаправлений клиент вернет последний полученный ответ, не следуя за новым перенаправлением. Это полезно, чтобы избежать потенциальных бесконечных циклов перенаправлений.
Таймауты
Поле Timeout
в структуре http.Client
определяет максимальное время ожидания выполнения всего HTTP-запроса, включая установление соединения, чтение и запись данных. Это полезно, в тех случаях, когда необходимо ограничить время, которое клиент будет тратить на обработку запроса, особенно если сетевое соединение нестабильно или сервер работает медленно.
Рассмотрим простой пример использования поля Timeout
для установки тайм-аута на 5 секунд:
Если значение поля Timeout
превысит заданное значение, то запрос будет прерван, и вернется ошибка context.DeadlineExceeded
.
HTTP-сервер
Классическая схема обработки HTTP-запроса
Переходим к изучению и реализации второго компонента клиент-серверной архитектуры – HTTP-сервера.
Для начала рассмотрим классическую схему обработки запроса клиента:
- Клиент посылает запросы на сервер
- Сервер распределяет (мультиплексирует) запросы на обработчики
- Запрос проходит через промежуточный слой (middleware)
- После прохождения промежуточного слоя запрос попадает конкретному обработчику, который отслылает клиенту ответ.
Можно сказать, что процесс обработки запросов в Go основывается на двух основных компонентах:
- Обработчики (handlers): они отвечают за логику приложения и формирование ответа клиенту. Обработчики в Go могут быть как простыми функциями, так и сложными структурами, реализующими интерфейс
http.Handler
. - Серверные мультиплексоры (servemuxes): это компоненты, которые сопоставляют URL-пути с соответствующими обработчиками. Мультиплексоры позволяют серверу понимать, какой обработчик следует использовать для каждого конкретного запроса.
Продемонстрируем пример простого HTTP-сервера с одним мультиплексором и обработчиком:
Чтобы протестировать работу сервера, достаточно запустить код и отправить запрос по адресу http://localhost:8080/redirect. Сделать это можно с помощью популярной программы Postman или утилиты curl с дополнительными флагами:
В результате получим следующий подробный вывод, содержащий HTTP-заголовки, информацию о подключении и другую диагностическую информацию:
Пользовательские обработчики
Когда речь заходит о разработке серверных приложений на Go, стандартных обработчиков, предоставляемых библиотекой net/http
, бывает недостаточно для реализации сложной логики. В таких случаях на помощь приходят пользовательские обработчики, которые позволяют настроить сервер под специфические требования и задачи.
Любой пользовательский обработчик должен реализовывать интерфейс http.Handler с единственным методом ServeHTTP:
Напишем собственный обработчик, который будет отсылать ответ с предоставленной информацией о пользователе:
Обработка запроса на стороне сервера происходит следующим образом: сервер получает HTTP-запрос и передает его мультиплексору, который указан вторым аргументом функции http.ListenAndServe. Следующим этапом мультиплексор ищет подходящий обработчик по указанному маршруту, в данном случае – "/user". Далее мультиплексор вызывает у подобранного обработчика метод ServeHTTP, который записывает данные в HTTP-соединение и отсылает ответ клиенту.
Объект http.ServeMux можно воспринимать как особый обработчик, который вместо предоставления ответа передает запрос другому обработчику, формируя таким образом цепочку. Стоит отметить, что подобный механизм находит применение в специальных промежуточных обработчиках – middleware, с которыми мы познакомимся в дальнейшем.
Отметим, что в качестве обработчиков могут выступать обычные функции с сигнатурой func(http.ResponseWriter, *http.Request)
, обернутые в специальный тип http.HandlerFunc, который реализует интерфейс Handler:
Для демонстрации создадим обработчик infoHandler, конвертируем его в тип HandlerFunc и передадим в мультиплексор:
Так как конвертация функции в тип http.HandlerFunc и добавление её в мультиплексор является довольно частой задачей, для нее придумали отдельный метод mux.HandleFunc. Поэтому код
можно заменить единственной строкой:
Middleware
В контексте веб-разработки middleware выполняет роль промежуточного звена между клиентом и сервером, обрабатывая HTTP-запросы до того, как они будут переданы в конечные обработчики. Middleware широко используется в Go-фреймворках Gin, Echo и Chi.
Middleware может выполнять разные задачи, например:
- Авторизация и аутентификация
- Логирование
- Валидация входных данных
- Ограничение времени медленных запросов
- Сокрытие конфиденциальных данных
Middleware работает по принципу цепочки: когда HTTP-запрос поступает в сервер, он сначала проходит через серию middleware-компонентов, и только потом передается в основной обработчик. Каждый middleware может либо завершить обработку запроса, отправив ответ клиенту, либо передать управление следующему middleware в цепочке.
В языке Go создание middleware происходит в два этапа:
- Написать функцию middleware, чтобы она удовлетворяла интерфейсу http.Handler
- Выстроить цепочку из middleware и обработчиков
Начнем с написания стандартного шаблона middleware:
Функция sampleMiddleware принимает в качестве аргумента обработчик next типа http.Handler
и возвращает новый обработчик того же типа. Внутри функции создаётся анонимный обработчик с использованием конструктора http.HandlerFunc
, который преобразует функцию в объект, соответствующий интерфейсу http.Handler
. Этот анонимный обработчик выполняет основную логику, после чего вызывается метод ServeHTTP у next, который передаёт запрос и ответ следующему обработчику в цепочке.
Рассмотренный способ задания функции позволяет создавать цепочки middleware с вложенными обработчиками и наглядно регистрировать их в мультиплексорах по следующему примеру:
Руководствуясь полученными знаниями, напишем HTTP-сервер, иллюстрирующий передачу запроса по цепочке из двух middleware:
После запуска приведенного кода и отправки запроса curl -i http://localhost:8080
получим следующий вывод:
В терминале, где был запущен код, выведутся логи, по которым можно отследить процесс передачи управления от одного middleware к другому сначала в порядке их вложенности, а затем – в обратном:
Заключение
В 18 части самоучителя мы изучили основы работы с HTTP-протоколом, разобрались в структуре запросов и ответов, а также познакомились с методами взаимодействия между клиентом и сервером. Мы подробно рассмотрели возможности пакета net/http
в Go для реализации веб-приложений и закрепили полученные знания с помощью практических примеров.
Эта глава завершает наш самоучитель по языку Go, в котором мы подробно разобрали основные подходы программирования на этом мощном и современном языке. Мы искренне благодарим вас за то, что прошли этот путь вместе с нами, и надеемся, что изученные материалы станут надежной основой для вашего дальнейшего профессионального роста.
Не бойтесь применять полученные знания на практике, развивайте свои навыки, исследуйте новые области и вдохновляйтесь успехами других. Помните, что каждая строка кода — это шаг к воплощению ваших идей и решению реальных задач. Желаем вам успехов, творческого вдохновения и удовольствия от программирования на Go!
Содержание самоучителя
- Особенности и сфера применения Go, установка, настройка
- Ресурсы для изучения Go с нуля
- Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
- Переменные. Типы данных и их преобразования. Основные операторы
- Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
- Функции и аргументы. Области видимости. Рекурсия. Defer
- Массивы и слайсы. Append и сopy. Пакет slices
- Строки, руны, байты. Пакет strings. Хеш-таблица (map)
- Структуры и методы. Интерфейсы. Указатели. Основы ООП
- Наследование, абстракция, полиморфизм, инкапсуляция
- Обработка ошибок. Паника. Восстановление. Логирование
- Обобщенное программирование. Дженерики
- Работа с датой и временем. Пакет time
- Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
- Конкурентность. Горутины. Каналы
- Тестирование кода и его виды. Table-driven подход. Параллельные тесты
- Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
- Протокол HTTP. Создание HTTP-сервера и клиента. Пакет net/http
Комментарии