Вказівники, низькорівневе програмування, компіляція, керування пам'яттю... Чудово! Тепер, коли ми успішно відлякали розробників на Python, давайте розпочнемо!😄
У цій статті ми будемо працювати над популярним викликом CodeCrafters Створення власного HTTP-сервера за допомогою C. Ми зосередимося лише на останньому розділі виклику, який стосується стиснення HTTP. Цей розділ включає вправи Заголовки стиснення, Кілька схем стиснення та Стиснення Gzip. Мій огляд буде обробляти всі ці вправи як одне завдання.
Завдання
Наш сервер отримає запит: curl -v -H "Accept-Encoding: gzip" http://localhost:4221/echo/abc
. Нас особливо цікавить секція заголовка Accept-Encoding, через яку запит вказує на підтримку схеми стиснення.
Заголовок Accept-Encoding
може мати такі формати:
Accept-Encoding: gzip
Accept-Encoding: encoding-1, encoding-2, encoding-3
Accept-Encoding: invalid-encoding-1, gzip, invalid-encoding-2
Ми повинні приймати лише стиснення gzip і повертати стиснуте тіло з заголовком Content-Encoding: gzip
, або повернути відповідь без стиснення, якщо gzip не вказано.
Приклад запиту
> GET /echo/foo HTTP/1.1
> Host: localhost:4221
> User-Agent: curl/7.81.0
> Accept: */*
> Accept-Encoding: gzip // Клієнт вказує, що підтримує схему стиснення gzip.
Приклад відповіді
< HTTP/1.1 200 OK
< Content-Encoding: gzip // Сервер вказує, що тіло відповіді стиснуте за допомогою gzip.
< Content-Type: text/plain // Оригінальний медіа-тип тіла.
< Content-Length: 23 // Розмір стиснутого тіла.
< ... // Стиснуте тіло.
Цей огляд припускає, що ви вже завершили попередні вправи і маєте налаштований сокет для прийому з’єднань.
Ми почнемо з того, що передамо вказівник на клієнтський дескриптор файлу як пустий (void) вказівник у функції handle_connection.
#include
#include
#include
#include
void *handle_connection(void *client_fd_ptr) {
int client_fd = *((int *)client_fd_ptr);
free(client_fd_ptr);
if (client_fd == -1) {
return NULL;
}
printf("Клієнт підключено\n");
char *response_not_found = "HTTP/1.1 404 Not Found\r\n\r\n";
char readBuffer[1024];
int bytesReceived = recv(client_fd, readBuffer, sizeof(readBuffer), 0);
if (bytesReceived == -1) {
printf("Не вдалося отримати дані: %s \n", strerror(errno));
close(client_fd);
return NULL;
}
int bytesSent;
}
У цій функції ми:
- Перетворюємо наш вказівник
client_fd_ptr
, який є пустим (void), на вказівник на ціле число. - Звільняємо пам'ять, яка була раніше виділена для нашого
client_fd_ptr
. - Налаштовуємо буфер для читання, в якому буде зберігатися вхідні дані запиту.
- Ініціалізуємо змінну
bytesSent
, яка буде використовуватися для перевірки помилок відповіді сервера.
Наступним кроком у функції обробки з'єднання ми дублюємо наш readBuffer, щоб обробити шлях запиту.
char *reqPath = strdup(readBuffer);
reqPath = strtok(reqPath, " ");
reqPath = strtok(NULL, " ");
printf("Шлях запиту: %s \n", reqPath);
Шлях запиту має вивести щось на зразок /echo/abc
.
Далі ми перевіряємо, чи починається запит з /echo/
і обробляємо його відповідно.
if (strncmp(reqPath, "/echo/", 6) == 0) {
reqPath = strtok(reqPath, "/"); // Отримуємо "echo"
reqPath = strtok(NULL, "/"); // Отримуємо вміст (наприклад, "abc")
printf("Вміст запиту: %s\n", reqPath);
int contentLength = strlen(reqPath);
if (strncmp(reqPath, "abc", 3) == 0) {
int encoded = 0;
char *headerLine = strdup(readBuffer);
headerLine = strtok(headerLine, "\r\n");
while (headerLine != NULL) {
if (strncmp(headerLine, "Accept-Encoding", 15) == 0) {
char *encoding = strtok(headerLine + 15, " ");
while (encoding != NULL) {
if (strncmp(encoding, "gzip", 4) == 0) {
// Стиснути і відповісти
} else {
encoding = strtok(NULL, " ");
}
}
}
headerLine = strtok(NULL, "\r\n");
}
}
}
else if {
bytesSent = send(client_fd, response_not_found, strlen(response_not_found), 0);
}
if (bytesSent < 0) {
printf("Не вдалося надіслати дані\n");
} else if (bytesSent == 0) {
printf("Клієнт закрив з'єднання\n");
}
У наведеній функції ми:
- Порівнюємо перші 6 літер шляху запиту з очікуваним шляхом
/echo/
. Якщо це не echo, ми повертаємо відповідь "не знайдено". - Використовуємо
strtok
, щоб витягти “echo” за допомогою роздільника “/”. Це дозволяє отримати вказівник на початок підрядка “echo”, фактично токенізуючи оригінальний рядок. - Продовжуємо токенізувати reqPath, передаючи NULL в
strtok
. Передаючи NULL вstrtok
, ми відновлюємо токенізацію там, де зупинилася попередня викликана функція, отримуючи наступний токен з рядка reqPath і дозволяючи обробити всю послідовність токенів. - Перевіряємо, чи перші 3 символи шляху запиту є “abc”. Це можна замінити на будь-який рядок, який ви очікуєте в запиті. Для чогось більш динамічного, ви можете приймати будь-який рядок, який буде стиснутий пізніше і повернутий у тілі відповіді.
- Створюємо змінну
encoded
, яку ми використовуємо пізніше для позначення того, чи було закодовано відповідь. - Дублюємо знову readBuffer, щоб обробити заголовки. Використовуючи той самий
strtok
для headerLine, ми пересуваємо вказівник на наступний символ після першогоcarriage return \r\n
.
Це ефективно переміщає нас до наступної частини заголовка, якою будеHTTP/1.1.
Але ми хочемо продовжити, поки не знайдемо секціюAccept-Encoding
. - Налаштуємо цикл while, щоб продовжувати токенr\n
, поки headerLine не стане NULL, доки не знайдемо секцію
Accept-Encoding`. - Далі будемо токенізувати за допомогою роздільника " " для визначення підтримуваної схеми стиснення або поки не досягнемо NULL. На кожній ітерації перевіряємо наявність
gzip
. Як тільки ми знаходимоgzip
, ми стискаємо тіло, що буде реалізовано пізніше.
Тепер давайте перейдемо до реалізації стиснення Gzip. Для цього ми створимо окрему функцію. Функція стиснення з бібліотеки zlib генерує сирий потік DEFLATE, що є лише стисненими даними без додаткових метаданих. Для цієї вправи ми хочемо отримати повні стиснуті дані, щоб пізніше порівняти та перевірити їх за допомогою команди Gzip у CLI. Тому ми реалізуємо більш комплексний метод.
void gzip_compress(const char *input, size_t inputLength, unsigned char *output, size_t *outputLength) {
z_stream stream = {0};
deflateInit2(&stream, Z_BEST_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
stream.next_in = (unsigned char *)input;
stream.avail_in = inputLength;
stream.next_out = output;
stream.avail_out = *outputLength;
deflate(&stream, Z_FINISH);
*outputLength = stream.total_out;
deflateEnd(&stream);
}
У цій функції ми:
- Приймаємо вказівник на тіло для стиснення, розмір тіла, вказівник на наш вихід та розмір нашого виходу.
- Ініціалізуємо структуру
z_stream
для керування процесом стиснення. - Ініціалізуємо потік з нашими заголовками gzip.
Z_BEST_COMPRESSION
(рівень стиснення),Z_DEFLATED
(вказуємо алгоритм стиснення DEFLATE),15 + 16
(розмір вікна 15 біт і +16 дозволяє додати заголовок/футер gzip для метаданих),8
(рівень виділення пам'яті) іZ_DEFAULT_STRATEGY
(стратегія стиснення за замовчуванням). - Передаємо заголовки в
deflateInit2
. - Встановлюємо вказівник на буфер вихідних даних (де будуть записані стиснуті дані).
- Встановлюємо розмір буфера виходу (кількість доступних байтів для запису).
- Виконуємо стиснення.
Z_FINISH
вказує, що це останній фрагмент даних для стиснення. - Оновлюємо розмір вихідних даних з загальною кількістю байтів, записаних у буфер.
- Очищаємо та звільняємо ресурси, використані
deflateInit2
.
Наступний крок — стиснути відповідь і надіслати з відповідними заголовками.
unsigned char compressedBody[1024];
uLong compressedLength = sizeof(compressedBody);
gzip_compress(reqPath, contentLength, compressedBody, &compressedLength);
char response[2048];
int headersLength = snprintf(response, 2048,
"HTTP/1.1 200 OK\r\nContent-Encoding: gzip\r\nContent-Type: text/plain\r\nContent-Length: %lu\r\n\r\n",
compressedLength);
memcpy(response + headersLength, compressedBody, compressedLength);
bytesSent = send(client_fd, response, headersLength + compressedLength, 0);
У цьому кроці ми:
- Виділяємо масив на 1024 байти для зберігання нашого стиснутого тіла.
- Стискаємо нашу відповідь, передаючи дані шляху запиту, довжину шляху запиту, наш вихід і довжину стиснених даних.
- Виділяємо масив для відповіді розміром 2048 байт для побудови HTTP відповіді: ми виділяємо буфер розміром 2048 байт для побудови HTTP відповіді.
Цей буфер буде містити HTTP заголовки та стиснуте тіло відповіді.
- Створюємо HTTP заголовки відповіді з урахуванням розміру стиснутого тіла: ми використовуємо snprintf для створення HTTP заголовків відповіді, включаючи заголовки Content-Encoding: gzip і Content-Length, які вказують на довжину стиснутого тіла відповіді.
- Копіюємо стиснуте тіло в буфер відповіді: ми використовуємо memcpy для копіювання стиснутого тіла відповіді в буфер відповіді, безпосередньо після HTTP заголовків.
- Надсилаємо відповідь клієнту: ми використовуємо функцію send для відправлення побудованої HTTP відповіді клієнту.
На останньому кроці ми обробляємо нестижені відповіді. Якщо gzip не зазначено в заголовку Accept-Encoding
, сервер надсилає відповідь без стиснення.
if (encoded == 0) {
char response[512];
sprintf(response, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n%s", contentLength, reqPath);
printf("Sending response: %s\n", response);
bytesSent = send(client_fd, response, strlen(response), 0);
}
Ми також можемо вивести вміст наших заголовків відповіді та стиснутого тіла для налагодження.
printf("Response Headers:\n%.*s", headersLength, response);
// Вивести стиснуте тіло як шістнадцяткові числа
printf("Compressed Body (hex):\n");
for (uLong i = 0; i < compressedLength; i++) {
printf("%02x ", compressedBody[i]);
if ((i + 1) % 16 == 0) { // Перехід на новий рядок після кожних 16 байтів
printf("\n");
}
}
printf("\n");
Давайте скомпілюємо та запустимо наш код. Не забудьте підключити бібліотеку zlib. gcc -o server server.c -lz; ./server
Ми можемо протестувати наш сервер за допомогою curl з відповідним запитом. Це має вивести відповідні заголовки відповіді та шістнадцяткове представлення стиснутого тіла.
curl -v -H "Accept-Encoding: invalid-encoding-1, gzip, invalid-encoding-2" http://localhost:4221/echo/abc
Щоб перевірити стиснені дані, ми можемо порівняти їх з результатом команди gzip
за допомогою hexdump
.
echo -n "abc" | gzip | hexdump -C
Це має дати нам відповідь, подібну до наведеного нижче.
Ми отримали схожі стиснуті дані, як показано вище.
У цьому посібнику ми реалізували HTTP стиснення в HTTP сервері на основі C, зосередившись на стисненні Gzip. Ми розглянули парсинг заголовка Accept-Encoding
, щоб визначити підтримувані схеми стиснення, стиснення тіл відповідей за допомогою бібліотеки zlib та відправлення стиснутих відповідей з відповідними заголовками.
Спочатку ми передаємо void вказівник на наш вказівник файлового дескриптора клієнта в функцію handle_connection.
#include
#include
#include
#include
void gzip_compress(const char *input, size_t inputLength, unsigned char *output, size_t *outputLength) {
z_stream stream = {0};
deflateInit2(&stream, Z_BEST_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
stream.next_in = (unsigned char *)input;
stream.avail_in = inputLength;
stream.next_out = output;
stream.avail_out = *outputLength;
deflate(&stream, Z_FINISH);
*outputLength = stream.total_out;
deflateEnd(&stream);
}
void *handle_connection(void *client_fd_ptr) {
int client_fd = *((int *)client_fd_ptr);
free(client_fd_ptr);
if (client_fd == -1) {
return NULL;
}
printf("Client connected\n");
char *response_not_found = "HTTP/1.1 404 Not Found\r\n\r\n";
char readBuffer[1024];
int bytesReceived = recv(client_fd, readBuffer, sizeof(readBuffer), 0);
if (bytesReceived == -1) {
printf("Receiving failed: %s\n", strerror(errno));
close(client_fd);
return NULL;
}
int bytesSent;
char *reqPath = strdup(readBuffer);
reqPath = strtok(reqPath, " ");
reqPath = strtok(NULL, " ");
printf("Request path: %s\n", reqPath);
if (strncmp(reqPath, "/echo/", 6) == 0) {
reqPath = strtok(reqPath, "/");
reqPath = strtok(NULL, "/");
int contentLength = strlen(reqPath);
if (strncmp(reqPath, "abc", 3) == 0) {
int encoded = 0;
char *headerLine = strdup(readBuffer);
headerLine = strtok(headerLine, "\r\n");
while (headerLine != NULL) {
if (strncmp(headerLine, "Accept-Encoding", 15) == 0) {
char *encoding = strtok(headerLine + 15, " ");
while (encoding != NULL) {
if (strncmp(encoding, "gzip", 4) == 0) {
unsigned char compressedBody[1024];
uLong compressedLength = sizeof(compressedBody);
gzip_compress(reqPath, contentLength, compressedBody, &compressedLength);
char response[2048];
int headersLength = snprintf(response, 2048,
"HTTP/1.1 200 OK\r\nContent-Encoding: gzip\r\nContent-Type: text/plain\r\nContent-Length: %lu\r\n\r\n",
compressedLength);
memcpy(response + headersLength, compressedBody, compressedLength);
bytesSent = send(client_fd, response, headersLength + compressedLength, 0);
encoded = 1;
break;
}
encoding = strtok(NULL, " ");
}
break;
}
headerLine = strtok(NULL, "\r\n");
}
if (encoded == 0) {
char response[512];
sprintf(response, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n%s", contentLength, reqPath);
bytesSent = send(client_fd, response, strlen(response), 0);
}
}
} else {
bytesSent = send(client_fd, response_not_found, strlen(response_not_found), 0);
}
if (bytesSent < 0) {
printf("Sending failed\n");
} else if (bytesSent == 0) {
printf("Client closed the connection\n");
}
free(reqPath);
close(client_fd);
return NULL;
}
Перекладено з: [Implementing HTTP Compression (Gzip) in a C HTTP Server: A CodeCrafters Challenge Walkthrough](https://innocentanyaele.medium.com/implementing-http-compression-gzip-in-a-c-http-server-a-codecrafters-challenge-walkthrough-cd9fdf9c30a6)