Привет, Хабр!
Сегодня будем писать HTTP‑сервер на C. Если когда‑нибудь вас заставят написать сервер без libcurl, boost::asio или microhttpd, вы будете готовы.
Сокеты
Начнём с азов. Чтобы принимать соединения нужен сокет. Через который к нам будут подключаться браузеры, Postman-ы и другие.
Создадим TCP‑сервер:
#include <stdio.h> // printf, perror
#include <stdlib.h> // exit
#include <string.h> // memset, strlen
#include <unistd.h> // close
#include <sys/types.h> // типы данных для сокетов
#include <sys/socket.h> // сами сокеты
#include <netinet/in.h> // sockaddr_in
#include <arpa/inet.h> // htons
#define PORT 8080 // Будем слушать этот порт
#define BACKLOG 10 // Очередь входящих соединений
int main() {
int server_fd;
struct sockaddr_in server_addr;
// 1. Создаём сокет: IPv4, TCP
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("Ошибка создания сокета");
exit(EXIT_FAILURE);
}
// 2. Привязываем сокет к адресу и порту
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("Ошибка привязки");
exit(EXIT_FAILURE);
}
// 3. Переключаем в режим прослушивания
if (listen(server_fd, BACKLOG) < 0) {
perror("Ошибка listen()");
exit(EXIT_FAILURE);
}
printf("Сервер запущен на порту %d\n", PORT);
close(server_fd);
return 0;
}
Создаём сокет socket(AF_INET, SOCK_STREAM, 0)
. Привязываем к порту: bind()
, указываем INADDR_ANY
, чтобы слушать все интерфейсы. Запускаем прослушку: listen()
, ставим BACKLOG = 10
, чтобы не дропать входящие соединения
Этот код пока только запускает сервер. Никаких клиентов, но зато уже можно увидеть терминале:
Сервер запущен на порту 8080
Клиент подключился. Что делать?
Теперь принимаем соединения. Дописываем accept()
и читаем HTTP‑запрос:
int client_fd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
while (1) {
printf("\nОжидание нового соединения...\n");
// 4. Принимаем входящее соединение
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("Ошибка accept()");
continue;
}
printf("Подключился клиент: %s\n", inet_ntoa(client_addr.sin_addr));
// 5. Читаем HTTP-запрос
char buffer[1024] = {0};
read(client_fd, buffer, sizeof(buffer) - 1);
printf("Запрос:\n%s\n", buffer);
close(client_fd);
}
accept()
создаёт новый сокет для клиента. Читаем read()
первый килобайт запроса. Выводим его в консоль
Если запустить и подключиться через curl, то можно увидеть запрос:
curl -v http://localhost:8080/
На сервере появится что‑то типа:
Запрос:
GET / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.68.0
Accept: */*
Формируем HTTP-ответ
Теперь серверу надо ответить. Это делается через write()
.
Минимальный ответ:
char response[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Content-Length: 13\r\n"
"\r\n"
"Hello, Habr!";
write(client_fd, response, strlen(response));
HTTP/1.1 200 OK — браузер поймёт, что всё хорошо. Content‑Type: text/html — отправляем HTML. Content‑Length: 13 — указываем длину тела ответа
Теперь клиент увидит в браузере Hello, Habr!.
Финальный код
Объединяем всё:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[1024];
server_fd = socket(AF_INET, SOCK_STREAM, 0);
bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(server_fd, 10);
while (1) {
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
read(client_fd, buffer, sizeof(buffer) - 1);
char response[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Content-Length: 13\r\n"
"\r\n"
"Hello, Habr!";
write(client_fd, response, strlen(response));
close(client_fd);
}
close(server_fd);
return 0;
}
Запускаем сервер, открываем http://localhost:8080 в браузере, и — ваш HTTP‑сервер работает.
Итог
Можно доработать:
Поддержку HTTP/1.1 с Keep‑Alive
Обработку разных GET и POST запросов
Поддержку статических файлов
А если есть идеи для улучшения — пишите в комментариях.
Рекомендую обратить внимание на открытые уроки по C, которые скоро пройдут в Otus: