Запрос на веб-программирование

Go

Обзор

Ранее мы узнали об обработчиках и функциях-обработчиках, как писать и регистрировать обработчики. В этой статье мы узнаем, как получить информацию из запроса.

структура запроса

Из предыдущего исследования мы знаем, что функция обработчика должна соответствовать следующей сигнатуре:

func (w http.ResponseWriter, r *http.Request)

в,http.Requestэто тип запроса. Данные, переданные клиентом, могут быть получены через эту структуру. структураRequestОпределяется в пакете net/http:

// src/net/http/request.go

type Request struct {
    Method          string    
    URL             *url.URL
    Proto           string
    ProtoMajor      int
    ProtoMinor      int
    Header          Header
    Body            io.ReadCloser
    ContentLength   int
    // 省略一些字段...
}

Рассмотрим несколько важных полей.

Method

в запросеMethodПоле указывает, какой метод сервера хочет вызвать клиент. существуетпервая статья, мы упомянули метод протокола HTTP. Его значениеGET/POST/PUT/DELETEЖдать. Сервер будет обрабатывать по-разному в зависимости от метода запроса, напримерGETМетод заключается в том, чтобы просто получить информацию (основную информацию о пользователе, информацию о товарах и т. д.),POSTМетоды создания новых ресурсов (регистрация новых пользователей, перечисление новых продуктов и т. д.).

URL

Создавая Всемирную паутину, Тим Бернерс-Ли также представил концепцию использования строк для представления интернет-ресурсов. Он называет строку какЕдиный идентификатор ресурса(URI, универсальный идентификатор ресурса). URI состоит из двух частей. часть представляет имя ресурса, т.е.Единое имя ресурса(URN, универсальное имя ресурса). Другая часть представляет собой местоположение ресурса, т.е.Единый указатель ресурсов(URL, единое расположение ресурса).

В HTTP-запросе используйте URL-адрес для описания расположения ресурса, с которым нужно работать. Общий формат URL:

[scheme:][//[userinfo@]host][/]path[?query][#fragment]
  • scheme: Имя протокола, общиеhttphttpsftp;
  • userInfo: Если есть, это означает, что можно записать информацию о пользователе, такую ​​как имя пользователя и пароль.dj:password;
  • host: Указывает доменное имя или адрес хоста и дополнительную информацию о порте. Если порт не указан, по умолчанию используется 80. Напримерwww.example.com,www.example.com:8080,127.0.0.1:8080;
  • path: путь ресурса на хосте, начиная с/разделены, например/posts;
  • query: необязательная строка запроса, параметры пары ключ-значение, передаваемые клиентом, ключ-значение используется напрямую=, используется между несколькими парами ключ-значение&Соединение, напримерpage=1&count=10;
  • fragment: Фрагмент, также известный как якорь. Представляет информацию о местоположении на странице. Эта часть информации обычно отсутствует в URL-адресе запроса, инициированном браузером. но черезajaxОтправьте эти данные в виде кода;

Давайте посмотрим на полный URL:

http://dj:password@www.example.com/posts?page=1&count=10#fmt

Структура URL определена в Gonet/urlв сумке:

// net/url/url.go
type URL struct {
    Scheme      string
    Opaque      string
    User        *Userinfo
    Host        string
    Path        string
    RawPath     string
    RawQuery    string
    Fragment    string
}

через объект запросаURLполе для получения этой информации. Далее давайте напишем программу, чтобы взглянуть на нее (используя базовую структуру веб-программы, описанную в предыдущей статье, нам нужно только добавить функции процессора и регистрацию):

func urlHandler(w http.ResponseWriter, r *http.Request) {
    URL := r.URL
    
    fmt.Fprintf(w, "Scheme: %s\n", URL.Scheme)
    fmt.Fprintf(w, "Host: %s\n", URL.Host)
    fmt.Fprintf(w, "Path: %s\n", URL.Path)
    fmt.Fprintf(w, "RawPath: %s\n", URL.RawPath)
    fmt.Fprintf(w, "RawQuery: %s\n", URL.RawQuery)
    fmt.Fprintf(w, "Fragment: %s\n", URL.Fragment)
}

// 注册
mux.HandleFunc("/url", urlHandler)

Запустите сервер и получите к нему доступ через браузерlocalhost:8080/url/posts?page=1&count=10#main:

Scheme: 
Host: 
Path: /url/posts
RawPath: 
RawQuery: page=1&count=10
Fragment:

Почему пустые поля? Обратите внимание на исходный кодRequestв структуреURLНа поле есть комментарий:

// URL specifies either the URI being requested (for server
// requests) or the URL to access (for client requests).
//
// For server requests, the URL is parsed from the URI
// supplied on the Request-Line as stored in RequestURI.  For
// most requests, fields other than Path and RawQuery will be
// empty. (See RFC 7230, Section 5.3)
//
// For client requests, the URL's Host specifies the server to
// connect to, while the Request's Host field optionally
// specifies the Host header value to send in the HTTP
// request.

В том смысле, что когда запрос получен как сервер,URLКромеPathиRawQuery, остальные поля в основном пусты. Для этой проблемы в репозитории Go GithubIssue 28940Были обсуждения.

Мы также можем пройтиURLСтруктура получает строку URL:

URL := &net.URL {
    Scheme:     "http",
    Host:       "example.com",
    Path:       "/posts",
    RawQuery:   "page=1&count=10",
    Fragment:   "main",
}
fmt.Println(URL.String())

Приведенная выше программа запускает выходную строку:

http://example.com/posts?page=1&count=10#main

Proto/ProtoMajor/ProtoMinor

ProtoУказывает версию протокола HTTP, напримерHTTP/1.1,ProtoMajorуказывает на большую версию,ProtoMinorОбозначает минорную версию.

func protoFunc(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Proto: %s\n", r.Proto)
    fmt.Fprintf(w, "ProtoMajor: %d\n", r.ProtoMajor)
    fmt.Fprintf(w, "ProtoMinor: %d\n", r.ProtoMinor)
}

mux.HandleFunc("/proto", protoFunc)

Запустите сервер, запросы браузераlocalhost:8080возвращение:

Proto: HTTP/1.1
ProtoMajor: 1
ProtoMinor: 1

В настоящее время HTTP/1.1 является основной версией.

Header

HeaderИнформация заголовка, отправленная клиентом, хранится в , в виде пар ключ-значение.HeaderБазовый тип на самом делеmap[string][]string:

// src/net/http/header.go
type Header map[string][]string

Ключ и значение каждого заголовка являются строками, и можно задать несколько одинаковых ключей. уведомлениеHeaderзначение[]stringТип, который содержит несколько значений для одного и того же ключа. Когда браузер инициирует HTTP-запрос, он автоматически добавляет некоторые заголовки. Напишем программу для просмотра:

func headerHandler(w http.ResponseWriter, r *http.Request) {
    for key, value := range r.Header {
        fmt.Fprintf(w, "%s: %v\n", key, value)
    }
}

mux.HandleFunc("/header", headerHandler)

Запускаем сервер, браузер запрашиваетlocalhost:8080/headerвозвращение:

Accept-Encoding: [gzip, deflate, br]
Sec-Fetch-Site: [none]
Sec-Fetch-Mode: [navigate]
Connection: [keep-alive]
Upgrade-Insecure-Requests: [1]
User-Agent: [Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36]
Sec-Fetch-User: [?1]
Accept: [text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3]
Accept-Language: [zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7]

Я использую браузер Chrome, и разные браузеры добавляют разные заголовки.

Общие заголовки:

  • Accept: тип контента, который клиент хочет, чтобы сервер отправил;
  • Accept-Charset: указывает кодировку символов, которую может принять клиент;
  • Content-Length: тела запросабайтДлина, как правило, больше в запросах POST/PUT;
  • Content-Type: при включении тела запроса этот заголовок используется для записи типа содержимого тела. При отправке запроса POST или PUT тип содержимого по умолчанию равенx-www-form-urlecoded. Но при загрузке файла вы должны установить типmultipart/form-data.
  • User-Agent: Используется для описания информации о клиенте, который инициировал запрос, например, какой браузер.

Content-Length/Body

Content-Lengthпредставляющий тело запросабайтдлина, содержимое тела запроса может быть изменено сBodyчитать в поле. Осторожные друзья, возможно, нашлиBodyполе представляет собойio.ReadCloserинтерфейс.Закройте его после прочтения, иначе будут утечки ресурсов. можно использоватьdeferУпрощение кодирования.

func bodyHandler(w http.ResponseWriter, r *http.Request) {
    data := make([]byte, r.ContentLength)
    r.Body.Read(data) // 忽略错误处理
    defer r.Body.Close()
    
    fmt.Fprintln(w, string(data))
}

mux.HandleFunc("/body", bodyHandler)

Приведенный выше код возвращает содержимое тела запроса от клиента к клиенту. также можно использоватьio/ioutilПакет упрощает операции чтения:

data, _ := ioutil.ReadAll(r.Body)

Ввод URL-адреса непосредственно в браузере инициируетGETЗапрос не может содержать тело запроса. Есть много способов инициировать запрос с телом запроса, два из которых описаны ниже:

использовать форму

Через форма HTML мы можем отправить запрос на почту на сервер и отправить содержимое формы в качестве тела запроса.

func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, `
<html>
    <head>
        <title>Go Web 编程之 request</title>
    </head>
    <body>
        <form method="post" action="/body">
            <label for="username">用户名:</label>
            <input type="text" id="username" name="username">
            <label for="email">邮箱:</label>
            <input type="text" id="email" name="email">
            <button type="submit">提交</button>
        </form>
    </body>
</html>
`)
}

mux.HandleFunc("/", indexHandler)

использовать в HTMLformдля отображения формы. После нажатия кнопки отправки браузер отправит POST-запрос на путь/body, используйте имя пользователя и адрес электронной почты в качестве тела запроса.

Запустите сервер и войдите на домашнюю страницуlocalhost:8080/, чтобы отобразить форму. Заполните информацию и нажмите отправить:

Браузер отправляет запрос на почту на сервер с URL/body,bodyHandlerПосле завершения обработки тело пакета отправляется обратно клиенту. Наконец клиент показывает:

Данные выше используютx-www-form-urlencodedкодировка, которая является кодировкой по умолчанию для формы. Подробности позже.

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

PostmanЭто очень мощный инструмент для тестирования API.

  • Поддерживаются все запросы методов протокола HTTP (GET/POST/PUT/DELETE).
  • Информация заголовка и содержимое тела запроса могут переноситься в запросе;
  • служба поддержкиjson/xml/httpконтент в различных форматах;
  • Дружественный интерфейс.

Далее давайте посмотрим, как использовать PostMan для проверки нашегоbodyHandler.

  • Черный: метод выбора протокола HTTP, поэтому запрос Pick POST может содержать тело;
  • Зеленая часть: запрошенный URL;
  • Синяя часть: вы можете установить заголовок запроса и тело запроса;
  • Светло-красная часть: тело запроса поддерживает несколько форматов, и здесь выбирается исходный формат;
  • Серая часть: конкретное содержание тела запроса;
  • Красная часть: информация об ответе, отображаемая после отправки, вы можете просмотреть заголовок ответа, файл cookie, тело ответа и т. д. Видно, что возвращается как есть.

Получить параметры запроса

Выше мы проанализировали общие поля HTTP-запросов в Go. В реальной разработке клиенту обычно необходимо передать некоторые параметры в запросе. Обычно существует два способа передачи параметров:

  • Пары ключ-значение URL, также известные как строка запроса, строка запроса;
  • форма.

Далее мы представим их по очереди.

Пара "ключ-значение" URL

Как упоминалось ранее в общем формате URL-адресов, за URL-адресом может следовать необязательная строка запроса для?отделенный от пути, какkey1=value1&key2=value2.

Структура URL имеетRawQueryполе. Это поле является строкой запроса.

func queryHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, r.URL.RawQuery)
}

mux.HandleFunc("/query", queryHandler)

если мы начнем сlocalhost:8080/query?name=dj&age=20запрос, строка запросаname=dj&age=20будет передан обратно клиенту как есть. ноRawQueryЭто строковый тип, и его также можно разобрать с помощью строкового метода, но это слишком хлопотно! ! !

форма

В узком смысле форму состоит в том, чтобы отправить запрос через форму, и в широком смысле данные могут быть отправлены на сервер в тело запроса. Далее мы просто пишем HTML-страницу и отправить HTTP-запрос через форму страницы:

<html>
    <head>
        <title>Go Web 编程之 request</title>
    </head>
    
    <body>
        <form action="/form?lang=cpp&name=dj" method="post" enctype="application/x-www-form-urlencoded">
            <label>Form:</label>
            <input type="text" name="lang" />
            <input type="text" name="age" />
            <button type="submit">提交</button>
        </form>
    </body>
</html>
  • actionпредставляет URL-адрес, запрошенный при отправке формы,methodПредставляет метод запроса.При использованииGETпросьба, из-заGETУ метода нет тела запроса, параметры будут вставлены в конец URL;
  • enctypeУказывает метод кодирования тела запроса, по умолчаниюapplication/x-www-form-urlencoded. Если файл необходимо отправить, он должен быть указан какmultipart/form-data;

Давайте представим, что такоеurlencodedкодирование. Зарезервированные и незарезервированные слова в URL-адресах определены в RFC 3986, и все зарезервированные символы должны быть закодированы в URL-адресе. Кодирование URL-адреса преобразует символ в его значение байта в кодировке ASCII, затем представляет значение байта как двузначное шестнадцатеричное число и, наконец, добавляет к числу префикс знака процента (% ). Например, кодировка ASCII пробела — 32, а шестнадцатеричная — 20, поэтому кодировка URL%20.

Formполе

использоватьx-www-form-urlencodedЗакодированное тело запроса, которое вызывается первым при обработке запроса.ParseFormразбор метода, затем изFormПолучить данные с поля:

func formHandler(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    fmt.Fprintln(w, r.Form)
}

mux.HandleFunc("/form", formHandler)

Запустите программу и проверьте результат:

Formтип поляurl.ValuesНижний слой на самом делеmap[string][]string. перечислитьParseFormметод, вы можете использоватьurl.Valuesспособ манипулирования данными.

использоватьParseFormОн также анализирует строку запроса, изменяя приведенную выше форму на:

<html>
    <head>
        <title>Go Web 编程之 request</title>
    </head>
    
    <body>
        <form action="/form?lang=cpp&name=dj" method="post" enctype="application/x-www-form-urlencoded">
            <label>Form:</label>
            <input type="text" name="lang" />
            <input type="text" name="age" />
            <button type="submit">提交</button>
        </form>
    </body>
</html>

Результат запроса:

Видно, что пары ключ-значение в строке запроса объединяются с обработкой синтаксического анализа в форме. Под одним и тем же ключом значение формы всегда идет первым, например[golang cpp].

PostFormполе

Если запрос содержит как пары "ключ-значение" URL, так и данные формы, а пользователь хочет получить только данные формы, вы можете использоватьPostFormполе.

использоватьPostFormБудут возвращены только данные формы, за исключением ключей URL. Если поставить вышеуказанную программу,r.Formизменить наr.PostForm, то программа выдаст следующие результаты:

MultipartFormполе

Если вы хотите обработать загруженные файлы, вы должны использоватьmultipart/form-dataкодирование. с предыдущимForm/PostFormаналог, обработкаmultipart/form-dataПри кодировании запроса его также необходимо разобрать перед использованием. Но метод используется другой, анализ используетParseMultipartForm, то изMultipartFormЗначение поля.

<form action="/multipartform?lang=cpp&name=dj" method="post" enctype="multipart/form-data">
	<label>MultipartForm:</label>
    <input type="text" name="lang" />
    <input type="text" name="age" />
    <input type="file" name="uploaded" />
    <button type="submit">提交</button>
</form>
func multipartFormHandler(w http.ResponseWriter, r *http.Request) {
    r.ParseMultipartForm(1024)
    fmt.Fprintln(w, r.MultipartForm)
    
    fileHeader := r.MultipartForm.File["uploaded"][0]
	file, err := fileHeader.Open()
	if err != nil {
		fmt.Println("Open failed: ", err)
		return
	}

	data, err := ioutil.ReadAll(file)
	if err == nil {
		fmt.Fprintln(w, string(data))
	}
}

mux.HandleFunc("/multipartform", multipartFormHandler)

Запустите программу:

MultipartFormсодержит дваmapПоля типа: одно представляет пару ключ-значение формы, а другое — информацию о загруженном файле.

Используйте имя элемента управления файлом в форме, чтобы получитьMultipartForm.FileПолучите файлы, загруженные через этот элемент управления, которых может быть несколько. получить этоmultipart.FileHeaderТип, через который можно получить различные атрибуты файла.

Следует отметить, что этот метод используется для обработки файлов. для безопасности,ParseMultipartFormМетод должен передавать параметр, указывающий максимальное использование памяти, чтобы загружаемый файл не занимал слишком много места.

FormValue/PostFormValue

Чтобы легко получить значение,net/httpпакет предоставляетFormValue/PostFormValueметод. Они автоматически вызываются при необходимостиParseForm/ParseMultipartFormметод.

FormValueметод возвращает запрошенныйFormЗначение указанного ключа в поле.Если для одного и того же ключа есть несколько значений, вернуть первое. Если вам нужно получить все значения, используйте напрямуюFormполе. Код ниже вернетhelloСоответствующее первое значение:

fmt.Fprintln(w, r.FormValue("hello"))

PostFormValueметод возвращает запрошенныйPostFormЗначение указанного ключа в поле.Если для одного и того же ключа есть несколько значений, вернуть первое. Если вам нужно получить все значения, используйте напрямуюPostFormполе

Уведомление: Когда кодировка указана какmultipart/form-dataчас,FormValue/PostFormValueне вернет значения, они читаютForm/PostFormполе, покаParseMultipartFormзаписать данныеMultipartFormполе.

Другие форматы

Другие форматы данных могут быть отправлены с помощью таких методов, как AJAX, таких какapplication/jsonЖдать. В этой ситуации:

  • сначала через шапкуContent-Typeузнать, какой это формат;
  • пройти черезr.Bodyчтение потока байтов;
  • Использование декодирования.

Суммировать

В этой статье описываетсяnet/httpАспекты запроса в пакете. отRequestСтруктура того, как передавать параметры, и, наконец, введение в обработку различных закодированных запросов.