- Введення в HTTP
- Що буде робити сервер?
- Про сокетах
- створення сокета
- Прив'язка сокета до адресою (bind)
- Підготовка сокета до прийняття вхідних з'єднань (listen)
- Очікування вхідного з'єднання (accept)
- Отримання запиту і відправка відповіді
- Послідовна обробка запитів
Наша взаимовыгодная связь https://banwar.org/
Створимо HTTP-сервер, який обробляє запити браузера і повертає відповідь у вигляді HTML-сторінки.
Введення в 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 // Детальніше: http://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; }Підготовка сокета до прийняття вхідних з'єднань (listen)
Підготуємо сокет до прийняття вхідних з'єднань від клієнтів. Це робиться за допомогою функції listen. Вона приймає дескриптор слухача сокета і максимальну кількість одночасних з'єднань.
У разі помилки, функція listen вирощує значення константи SOCKET_ERROR. При успішному виконанні вона поверне 0.
// ініціалізувавши слухає сокет if (listen (listen_socket, SOMAXCONN) == SOCKET_ERROR) {cerr << "listen failed with error:" << WSAGetLastError () << "\ n"; closesocket (listen_socket); WSACleanup (); return 1; }У константі SOMAXCONN зберігається максимально можливе число одночасних TCP-з'єднань. Це обмеження працює на рівні ядра ОС.
Очікування вхідного з'єднання (accept)
Функція accept очікує запит на установку TCP-з'єднання від віддаленого хоста. Як аргумент їй передається дескриптор слухача сокета.
При успішній установці TCP-з'єднання, для нього створюється новий сокет. Функція accept повертає дескриптор цього сокета. Якщо сталася помилка з'єднання, то повертається значення INVALID_SOCKET.
// Приймаємо вхідні з'єднання int client_socket = accept (listen_socket, NULL, NULL); if (client_socket == INVALID_SOCKET) {cerr << "accept failed:" << WSAGetLastError () << "\ n"; closesocket (listen_socket); WSACleanup (); return 1; }Отримання запиту і відправка відповіді
Після установки з'єднання з сервером, браузер відправляє HTTP-запит. Ми отримуємо вміст запиту через функцію recv. Вона приймає дескриптор TCP-з'єднання (в нашому випадку це client_socket), покажчик на буфер для збереження отриманих даних, розмір буфера в байтах і додаткові прапори (які зараз нас не цікавлять).
При успішному виконанні функція recv поверне розмір отриманих даних. У разі помилки повертається значення SOCKET_ERROR. Якщо з'єднання було закрито клієнтом, то повертається 0.
Ми створимо буфер розміром 1024 байт для збереження HTTP-запиту.
const int max_client_buffer_size = 1024; char buf [max_client_buffer_size]; result = recv (client_socket, buf, max_client_buffer_size, 0); std :: stringstream response; // сюди буде записуватися відповідь клієнту std :: stringstream response_body; // тіло відповіді if (result == SOCKET_ERROR) {// помилка отримання даних cerr << "recv failed:" << result << "\ n"; closesocket (client_socket); } Else if (result == 0) {// з'єднання закрито клієнтом cerr << "connection closed ... \ n"; } Else if (result> 0) {// Ми знаємо фактичний розмір отриманих даних, тому ставимо мітку кінця рядка // У буфері запиту. buf [result] = '\ 0'; // Дані успішно отримані // формуємо тіло відповіді (HTML) response_body << "<title> Test C ++ HTTP Server </ title> \ n" << "<h1> Test page </ h1> \ n" << "< p> This is body of the test page ... </ p> \ n "<<" <h2> Request headers </ h2> \ n "<<" <pre> "<< buf <<" </ pre > \ n "<<" <em> <small> Test C ++ Http Server </ small> </ em> \ n "; // Формуємо весь відповідь разом з заголовками response << "HTTP / 1.1 200 OK \ r \ n" << "Version: HTTP / 1.1 \ r \ n" << "Content-Type: text / html; charset = utf- 8 \ r \ n "<<" Content-Length: "<< response_body.str (). length () <<" \ r \ n \ r \ n "<< response_body.str (); // Відправляємо відповідь клієнту за допомогою функції send result = send (client_socket, response.str (). C_str (), response.str (). Length (), 0); if (result == SOCKET_ERROR) {// сталася помилка при отправле даних cerr << "send failed:" << WSAGetLastError () << "\ n"; } // Закриваємо з'єднання до клієнтом closesocket (client_socket); }Після отримання запиту ми відразу ж відправили відповідь клієнту за допомогою функції send. Вона приймає дескриптор сокета, рядок з даними відповіді і розмір відповіді в байтах.
У разі помилки, функція повертає значення SOCKET_ERROR. У разі успіху - кількість переданих байт.
Спробуємо скомпілювати програму, не забувши попередньо завершити функцію main.
// Прибираємо за собою closesocket (listen_socket); freeaddrinfo (addr); WSACleanup (); return 0; }Весь вихідний код прикладу.
Якщо скомпілювати і запустити програму, то вікно консолі «підвисне» в очікуванні запиту на встановлення TCP-з'єднання. Відкрийте в браузері адресу http://127.0.0.1:8000/ . Сервер поверне відповідь, як на малюнку нижче і завершить роботу.
Послідовна обробка запитів
Щоб сервер не завершував роботу після обробки першого запиту, а продовжував обробляти нові сполуки, потрібно зробити цикл ту частину коду, яка приймає запит на установку з'єднання і повертає відповідь.
const int max_client_buffer_size = 1024; char buf [max_client_buffer_size]; int client_socket = INVALID_SOCKET; for (;;) {// Приймаємо вхідні з'єднання client_socket = accept (listen_socket, NULL, NULL); if (client_socket == INVALID_SOCKET) {cerr << "accept failed:" << WSAGetLastError () << "\ n"; closesocket (listen_socket); WSACleanup (); return 1; } Result = recv (client_socket, buf, max_client_buffer_size, 0); std :: stringstream response; // сюди буде записуватися відповідь клієнту std :: stringstream response_body; // тіло відповіді if (result == SOCKET_ERROR) {// помилка отримання даних cerr << "recv failed:" << result << "\ n"; closesocket (client_socket); } Else if (result == 0) {// з'єднання закрито клієнтом cerr << "connection closed ... \ n"; } Else if (result> 0) {// Ми знаємо розмір отриманих даних, тому ставимо мітку кінця рядка // У буфері запиту. buf [result] = '\ 0'; // Дані успішно отримані // формуємо тіло відповіді (HTML) response_body << "<title> Test C ++ HTTP Server </ title> \ n" << "<h1> Test page </ h1> \ n" << "< p> This is body of the test page ... </ p> \ n "<<" <h2> Request headers </ h2> \ n "<<" <pre> "<< buf <<" </ pre > \ n "<<" <em> <small> Test C ++ Http Server </ small> </ em> \ n "; // Формуємо весь відповідь разом з заголовками response << "HTTP / 1.1 200 OK \ r \ n" << "Version: HTTP / 1.1 \ r \ n" << "Content-Type: text / html; charset = utf- 8 \ r \ n "<<" Content-Length: "<< response_body.str (). length () <<" \ r \ n \ r \ n "<< response_body.str (); // Відправляємо відповідь клієнту за допомогою функції send result = send (client_socket, response.str (). C_str (), response.str (). Length (), 0); if (result == SOCKET_ERROR) {// сталася помилка при отправле даних cerr << "send failed:" << WSAGetLastError () << "\ n"; } // Закриваємо з'єднання до клієнтом closesocket (client_socket); }}Коли сервер закінчить обробку запиту одного клієнта, він закриє з'єднання з ним і буде очікувати нового запиту.
Вихідний код остаточної версії сервера.
У другій частині цієї статті ми напишемо парсер HTTP-заголовків і створимо нормальний API для управління HTTP-запитами і відповідями.
Примітка: якщо ви використовуєте MinGW в Windows, то бібліотеку Ws2_32.lib потрібно вручну прописати в настройках лінковщік.
Що буде робити сервер?