Введение в HTTP
Для начала разберемся, что из себя представляет HTTP. Это текстовый протокол для обмена данными между браузером и веб-сервером.
Пример HTTP-запроса:
GET /page.html HTTP/1.1
Host: site.com
Первая строка передает метод запроса, идентификатор ресурса (URI) и версию HTTP-протокола. Затем перечисляются заголовки запроса, в которых браузер передает имя хоста, поддерживаемые кодировки, cookie и другие служебные параметры. После каждого заголовка ставится символ переноса строки \r\n.
У некоторых запросов есть тело. Когда отправляется форма методом POST, в теле запроса передаются значения полей этой формы.
POST /submit HTTP/1.1
Host site.com
Content-Type: application/x-www-form-urlencoded
name=Sergey&last_name=Ivanov&birthday=1990-10-05
Тело запроса отделяется от заголовков одной пустой строкой. Заголовок «Content-Type» говорит серверу, в каком формате закодировано тело запроса. По умолчанию, в HTML-форме данные кодируются методом «application/x-www-form-urlencoded».
Иногда необходимо передать данные в другом формате. Например, при загрузке файлов на сервер, бинарные данные кодируются методом «multipart/form-data».
Сервер обрабатывает запрос клиента и возвращает ответ.
Пример ответа сервера:
HTTP/1.1 200 OK
Host: site.com
Content-Type: text/html; charset=UTF-8
Connection: close
Content-Length: 21
<h1>Test page...</h1>
В первой строке ответа передается версия протокола и статус ответа. Для успешных запросов обычно используется статус «200 OK». Если ресурс не найден на сервере, возвращается «404 Not Found».
Тело ответа так же, как и у запроса, отделяется от заголовков одной пустой строкой.
Полная спецификации протокола HTTP описывается в стандарте rfc-2068. По понятным причинам, мы не будем реализовывать все возможности протокола в рамках этого материала. Достаточно реализовать поддержку работы с заголовками запроса и ответа, получение метода запроса, версии протокола и URL-адреса.
|
Что будет делать сервер?
Сервер будет принимать запросы клиентов, парсить заголовки и тело запроса, и возвращать тестовую HTML-страничку, на которой отображены данные запроса клиента (запрошенный URL, метод запроса, cookie и другие заголовки).
О сокетах
Для работы с сетью на низком уровне традиционно используют сокеты. Сокет — это абстракция, которая позволяет работать с сетевыми ресурсами, как с файлами. Мы можем писать и читать данные из сокета почти так же, как из обычного файла.
В этом материале мы будем работать с виндовой реализацией сокетов, которая находится в заголовочном файле <WinSock2.h>. В Unix-подобных ОС принцип работы с сокетами такой же, только отличается API. Вы можете подробнее почитать о сокетах Беркли, которые используются в GNU/Linux.
Создание сокета
Создадим сокет с помощью функции socket, которая находится в заголовочном файле <WinSock2.h>. Для работы с IP-адресами нам понадобится заголовочный файл <WS2tcpip.h>.
#include <iostream>
#include <sstream>
#include <string>
// Для корректной работы freeaddrinfo в MinGW
// Подробнее: https://stackoverflow.com/a/20306451
#define _WIN32_WINNT 0x501
#include <WinSock2.h>
#include <WS2tcpip.h>
// Необходимо, чтобы линковка происходила с DLL-библиотекой
// Для работы с сокетам
#pragma comment(lib, "Ws2_32.lib")
using std::cerr;
int main ()
{
// служебная структура для хранение информации
// о реализации Windows Sockets
WSADATA wsaData;
// старт использования библиотеки сокетов процессом
// (подгружается Ws2_32.dll)
|
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
// Если произошла ошибка подгрузки библиотеки
if (result!= 0) {
cerr << "WSAStartup failed: " << result << "\n";
return result;
}
struct addrinfo* addr = NULL; // структура, хранящая информацию
// об IP-адресе слущающего сокета
// Шаблон для инициализации структуры адреса
struct addrinfo hints;
ZeroMemory(&hints, sizeof (hints));
// AF_INET определяет, что используется сеть для работы с сокетом
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM; // Задаем потоковый тип сокета
hints.ai_protocol = IPPROTO_TCP; // Используем протокол TCP
// Сокет биндится на адрес, чтобы принимать входящие соединения
hints.ai_flags = AI_PASSIVE;
// Инициализируем структуру, хранящую адрес сокета - addr.
// HTTP-сервер будет висеть на 8000-м порту локалхоста
result = getaddrinfo("127.0.0.1", "8000", &hints, &addr);
// Если инициализация структуры адреса завершилась с ошибкой,
// выведем сообщением об этом и завершим выполнение программы
if (result!= 0) {
cerr << "getaddrinfo failed: " << result << "\n";
WSACleanup(); // выгрузка библиотеки Ws2_32.dll
return 1;
}
// Создание сокета
int listen_socket = socket(addr->ai_family, addr->ai_socktype,
addr->ai_protocol);
// Если создание сокета завершилось с ошибкой, выводим сообщение,
// освобождаем память, выделенную под структуру addr,
// выгружаем dll-библиотеку и закрываем программу
if (listen_socket == INVALID_SOCKET) {
cerr << "Error at socket: " << WSAGetLastError() << "\n";
freeaddrinfo(addr);
WSACleanup();
return 1;
}
//...
Мы подготовили все данные, которые необходимо для создания сокета и создали сам сокет. Функция socket возвращает целочисленное значение файлового дескриптора, который выделен операционной системой под сокет.
|
Привязка сокета к адресу (bind)
Следующим шагом, нам необходимо привязать IP-адрес к сокету, чтобы он мог принимать входящие соединения. Для привязки конкретного адреса к сокету используется фукнция bind. Она принимает целочисленный идентификатор файлового дескриптора сокета, адрес (поле ai_addr из структуры addrinfo) и размер адреса в байтах (используется для поддержки IPv6).
// Привязываем сокет к IP-адресу
result = bind(listen_socket, addr->ai_addr, (int)addr->ai_addrlen);
// Если привязать адрес к сокету не удалось, то выводим сообщение
// об ошибке, освобождаем память, выделенную под структуру addr.
// и закрываем открытый сокет.
// Выгружаем DLL-библиотеку из памяти и закрываем программу.
if (result == SOCKET_ERROR) {
cerr << "bind failed with error: " << WSAGetLastError() << "\n";
freeaddrinfo(addr);
closesocket(listen_socket);
WSACleanup();
return 1;
}