C Academy · Урок

Простой эхо-сервер

Обслуживайте множество клиентов одновременно.

Урок 4 из 413 шагов

«Простой эхо-сервер» — бесплатный урок C Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C Academy содержит 4 уроков всего.

Что мы создаём

Эхо-сервер принимает TCP-соединения и отправляет обратно все байты, которые присылает клиент. Это «Hello, world!» сетевого программирования.

Мы создадим его на основе epoll, чтобы один поток одновременно обслуживал множество клиентов. Компоненты таковы: сокет, принимающий соединения, цикл epoll и обработка каждого клиента по схеме «прочитать, затем записать».

Создание сокета, принимающего соединения

Начните с socket() для конечной точки IPv4 TCP. Тройка параметров такова: AF_INET, SOCK_STREAM и протокол 0.

Функция возвращает файловый дескриптор, который мы привяжем к адресу, переведём в неблокирующий режим и в итоге зарегистрируем в epoll. Проверяйте значение -1 после каждого системного вызова.

int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) { perror("socket"); exit(1); }

Повторное использование адреса

Перед привязкой установите SO_REUSEADDR с помощью setsockopt(). Это позволит серверу перезапуститься и снова привязаться к тому же порту, не дожидаясь завершения состояния TIME_WAIT.

Без этого быстрый перезапуск часто завершается ошибкой EADDRINUSE, что особенно неприятно во время разработки.

int yes = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);

Привязка к порту

Заполните struct sockaddr_in, указав семейство, порт и адрес. Для порта используйте htons(), а для прослушивания всех интерфейсов — INADDR_ANY.

Затем вызовите bind(), чтобы привязать сокет к этому адресу. htons преобразует порядок байтов хоста в сетевой порядок байтов, что необходимо.

struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8080);
bind(listen_fd, (struct sockaddr*)&addr, sizeof addr);

Прослушивание и переход в неблокирующий режим

Вызовите listen(), чтобы перевести сокет в пассивный режим и задать очередь ожидающих соединений. Значение SOMAXCONN запрашивает максимальный размер, поддерживаемый системой.

Затем переведите сокет в неблокирующий режим, чтобы accept() никогда не приостанавливал цикл обработки событий. Теперь сокет готов к регистрации в epoll.

listen(listen_fd, SOMAXCONN);
set_nonblocking(listen_fd);

Регистрация сокета, принимающего соединения

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

Мы сохраняем дескриптор в data.fd, чтобы распознать этот сокет внутри цикла.

int epfd = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = listen_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

Приём клиента

Когда сокет, принимающий соединения, готов к чтению, выполняйте accept() в цикле до получения EAGAIN. Каждый новый клиентский сокет переводится в неблокирующий режим и регистрируется для EPOLLIN.

С этого момента epoll будет сообщать нам, когда клиент отправит данные, которые можно вернуть ему обратно.

int c = accept(listen_fd, NULL, NULL);
if (c != -1) {
    set_nonblocking(c);
    struct epoll_event cev = { .events = EPOLLIN, .data.fd = c };
    epoll_ctl(epfd, EPOLL_CTL_ADD, c, &cev);
}

Логика эхо-сервера

Для готового клиентского сокета выполните recv() в буфер. Если получены байты, сразу отправьте те же байты обратно с помощью send().

Это простая схема «прочитать, затем записать» и есть эхо-ответ. В рабочем приложении следовало бы буферизовать неотправленные байты, но в демонстрационном примере мы предполагаем, что небольшой ответ помещается в буфер отправки.

char buf[4096];
ssize_t r = recv(fd, buf, sizeof buf, 0);
if (r > 0) {
    send(fd, buf, r, 0);
}

Обнаружение отключения

Возвращаемое recv() значение 0 означает, что клиент корректно завершил соединение. Отрицательное значение с любым кодом, кроме EAGAIN, означает настоящую ошибку.

В обоих случаях удалите дескриптор из epoll и вызовите для него close(), чтобы освободить ресурсы и прекратить получение дальнейших событий.

if (r == 0 || (r < 0 && errno != EAGAIN)) {
    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
    close(fd);
}

Полный цикл

Объедините компоненты: ожидайте события, определяйте, принадлежит ли дескриптор сокету, принимающему соединения, или клиенту, и выполняйте соответствующую обработку.

Этот компактный цикл представляет собой полностью работоспособный параллельный эхо-сервер. Один поток, множество клиентов, отдельные потоки для соединений не нужны.

for (;;) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < n; i++) {
        if (events[i].data.fd == listen_fd) accept_clients();
        else echo_or_close(events[i].data.fd);
    }
}

Тестирование и дальнейшие шаги

Скомпилируйте программу в Linux и подключитесь с помощью nc localhost 8080: всё, что Вы введёте, вернётся обратно. Откройте несколько терминалов, чтобы убедиться в параллельной обработке.

Чтобы сделать сервер надёжнее, буферизуйте частично отправленные данные, обрабатывайте EPOLLOUT, переключитесь на EPOLLET с полным чтением данных и обрабатывайте EPOLLRDHUP при частичном закрытии соединения.

Быстрая проверка

Проверьте, насколько хорошо Вы поняли работу эхо-сервера.

Итоги

Вы создали эхо-сервер на основе epoll: создали сокет, установили SO_REUSEADDR, привязали его, перевели в режим прослушивания и сделали неблокирующим. Зарегистрировали серверный сокет, принимали клиентов в цикле и отправляли им обратно полученные данные с помощью recv()/send().

Обрабатывайте возврат 0 как отключение и корректно закрывайте соединение. На основе этого каркаса можно создать высокопроизводительный однопоточный сетевой сервер.

Можно начать бесплатно

Изучай C с ИИ-репетитором — бесплатно

Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.

Курсы
39
Уроки
144

Часто задаваемые вопросы

Урок «Простой эхо-сервер» бесплатный?

Да — полный текст урока «Простой эхо-сервер» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C Academy, подпишись на CoddyKit PRO. Курс C Academy содержит 4 уроков всего.

Чему я научусь в уроке «Простой эхо-сервер»?

Обслуживайте множество клиентов одновременно. Ты практикуешь C Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать C Academy?

Предыдущий опыт не требуется. C Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Простой эхо-сервер»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке C Academy?

Да. Каждый урок C Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Блокирующий и неблокирующий ввод-вывод
  2. Настройка epoll
  3. Цикл обработки событий
  4. Простой эхо-сервер
← Назад к C Academy