Типичные ошибки в сетевых приложениях на C/C++

11 октября 2017

Очень многие программисты при написании сетевых приложений на C/C++ по неопытности допускают одни и те же ошибки. Даже несмотря на то, что эти ошибки, казалось бы, довольно известны. Поэтому сегодня мне хотелось бы в очередной раз поднять этот несколько баянистый топик, в надежде, что заметка поможет уменьшить hit ratio соответствующих граблей.

Итак, первая проблема заключается в обработке сигнала SIGPIPE. Этот сигнал приходит приложению, например, в случае, когда оно пытается сделать send в сокет, уже закрытый на стороне клиента. По умолчанию этот сигнал убивает приложение, что часто не является тем, чего хочет программист (если он пишет свое приложение на трэдах, а не процессах). Решить проблему можно так:

/*
 * If client will close a connection send() will just return -1
 * instead of killing a process with SIGPIPE.
 */

void HttpServer::_ignoreSigpipe() {
    sigset_t msk;
    sigemptyset(&msk);
    sigaddset(&msk, SIGPIPE);
    if(pthread_sigmask(SIG_BLOCK, &msk, nullptr) != 0)
        throw std::runtime_error("pthread_sigmask() call failed");
}

Две другие проблемы связаны с обработкой возвращаемых значений от recv и send (или read и write, смотря что вы используете).

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

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

Правильный код будет выглядеть как-то так:

void Socket::read(char* buff, size_t buffsize) {
    while(buffsize > 0) {
        ssize_t res = ::read(_fd, buff, buffsize);
        if(res == 0) {
            throw std::runtime_error("client closed connection");
        } else if(res < 0) {
            if(errno == EINTR)
                continue;
            throw std::runtime_error("read() failed");
        }
        buff += res;
        buffsize -= res;
    }
}

void Socket::write(const char* buff, size_t buffsize) {
    while(buffsize > 0) {
        ssize_t res = ::write(_fd, buff, buffsize);
        if(res <= 0) {
            if(errno == EINTR)
                continue;
            throw std::runtime_error("write() failed");
        }

        buff += res;
        buffsize -= res;
    }
}

Это, собственно, и есть те ошибки, о которых я хотел рассказать. Само собой разумеется, существуют и куда более глубоководные грабли. Например, при реализации собственных протоколов обычно не лишено смысла предусмотреть посылку пингов клиенту. Нужно это для своевременного закрытия повисших соединений, которые образуются как из-за «умного» прокси между клиентом и сервером, кэширующего соединения (часто встречается в случае HTTP), так и тупо из-за бага в клиентском коде. Еще неплохой идеей будет предусмотреть собственные контрольные суммы. Было больше одной истории с багами в маршрутизаторах — например, в редких случаях они портили принятые данные, а затем вычисляли контрольные суммы в IP-пакетах от испорченных данных (см также тынц и тынц). Однако это все уже темы для других постов.

А какие типичные ошибки вы бы добавили к приведенному списку?

Метки: .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.