Недопустимая ошибка поиска при работе с потоками сокетов с непустыми буферами чтения

В настоящее время я пишу серверное приложение на Linux x86_64, используя <sys/socket.h>. После принятия соединения через accept() я использую fdopen() для переноса полученного сокета в поток FILE*.

Запись и чтение из этого потока FILE* обычно работают довольно хорошо, но сокет становится непригодным для использования, как только я пишу в него, пока у него есть непустой буфер чтения.

В демонстрационных целях я написал некоторый код, который прослушивает соединение, а затем считывает ввод, строка за строкой, в буфер чтения, используя fgetc(). Если строка слишком длинная, чтобы поместиться в буфер, она не считывается полностью, а считывается во время следующей итерации.

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

FILE* listen_on_port(unsigned short port) {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        struct sockaddr_in name;
        name.sin_family = AF_INET;
        name.sin_port = htons(port);
        name.sin_addr.s_addr = htonl(INADDR_ANY);
        if(bind(sock, (struct sockaddr*) &name, sizeof(name)) < 0)
                perror("bind failed");
        listen(sock, 5);
        int newsock = accept(sock, 0, 0);
        return fdopen(newsock, "r+");
}

int main(int argc, char** argv) {
        int bufsize = 8;
        char buf[9];
        buf[8] = 0; //ensure null termination

        int data;
        int size;

        //listen on the port specified in argv[1]
        FILE* sock = listen_on_port(atoi(argv[1]));
        puts("New connection incoming");

        while(1) {
                //read a single line
                for(size = 0; size < bufsize; size++) {
                        data = fgetc(sock);
                        if(data == EOF)
                                break;
                        if(data == '\n') {
                                buf[size] = 0;
                                break;
                        }
                        buf[size] = (char) data;
                }

                //check if the read failed due to an EOF
                if(data == EOF) {
                        perror("EOF: Connection reset by peer");
                        break;
                } else {
                        printf("Input line: '%s'\n", buf);
                }

                //try to write ack
                if(fputs("ack\n", sock) == EOF)
                        perror("sending 'ack' failed"); 

                //try to flush
                if(fflush(sock) == EOF)
                        perror("fflush failed");        
        }

        puts("Connection closed");
}

Код должен компилироваться в gcc без каких-либо специальных параметров. Запустите его с номером порта в качестве аргумента и используйте netcat для локального подключения к нему.

Теперь, если вы попытаетесь отправить строки короче 8 символов, это будет работать безупречно. Но если вы отправите строку, содержащую более 10 символов, программа завершится ошибкой. Этот образец ввода:

ab
cd
abcdefghij

Создаст этот вывод:

New connection incoming
Input line: 'ab'
Input line: 'cd'
Input line: 'abcdefgh'
fflush failed: Illegal seek
EOF: Connection reset by peer: Illegal seek
Connection closed

Как видите, (правильно) читаются только первые 8 символов abcdefgh, но когда программа пытается отправить строку 'ack' (которую клиент никогда не получает), а затем сбросить буфер вывода, мы получаем ошибку Illegal seek, и следующий вызов fgetc() возвращает EOF.

Если часть fflush() закомментирована, та же ошибка возникает, но

fflush failed: Illegal seek

строка отсутствует в выводе сервера.

Если часть fputs(ack) закомментирована, кажется, что все работает как задумано, но perror(), вызываемый вручную из gdb, по-прежнему сообщает об ошибке «Недопустимый поиск».

Если и fputs(ack), и fflush() закомментированы, все действительно работает как задумано.

К сожалению, мне не удалось найти ни хорошей документации, ни обсуждений этой проблемы в Интернете, поэтому ваша помощь будет очень признательна.

изменить

Решение, на котором я остановился, состоит в том, чтобы не использовать fdopen() и FILE*, поскольку, похоже, не существует чистого способа преобразования сокета fd в FILE*, который можно было бы надежно использовать в режиме r+. Вместо этого я работал непосредственно над сокетом fd, написав собственный код замены для fputs и fprintf.

Если кому нужно, вот код.


person mic_e    schedule 21.04.2012    source источник


Ответы (4)


Очевидно, что режим «r+» (чтение/запись) не работает с сокетами в этой реализации, без сомнения, потому что базовый код предполагает, что он должен выполнять поиск для переключения между чтением и записью. Это общий случай с потоками stdio (вы должны выполнить какую-то операцию синхронизации), потому что еще в Dim Time фактические реализации stdio имели только один счетчик на поток, и это был либо счетчик «количество оставшихся символов для чтения из буфера потока с помощью макроса getc" (в режиме чтения) или "количество символов, которое можно безопасно записать в буфер потока с помощью макроса putc (в режиме записи). операция поискового типа.

Поиски не разрешены для каналов и сокетов (поскольку «смещение файла» здесь не имеет смысла).

Одно из решений — вообще не оборачивать сокет в stdio. Другой, вероятно, проще/лучше для ваших целей, это обернуть его не одним, а двумя потоками stdio:

FILE *in = fdopen(newsock, "r");
FILE *out = fdopen(newsock, "w");

Однако здесь есть еще один недостаток, потому что когда вы переходите к fclose одному потоку, он закрывает дескриптор другого файла. Чтобы обойти это, вам нужно один раз dup дескриптор сокета (в любом из двух вызовов выше, не имеет значения, в каком именно).

Если вы собираетесь использовать select или poll или что-то подобное в сокете в какой-то момент, вам обычно следует использовать решение «не обертывать с помощью stdio», поскольку нет хорошего чистого портативного способа отслеживать буферизацию stdio. (Есть способы, специфичные для реализации).

person torek    schedule 21.04.2012
comment
Итак, нет чистого способа использования FILE* streams, особенно одиночного FILE* stream? Прежде чем завернуть дескриптор сокета в FILE* stream, у меня были мои собственные оболочки для printf() и puts(), но я нашел быть некрасивым. Предположим, что это самое «чистое» решение. - person mic_e; 21.04.2012
comment
да. Возможно, по иронии судьбы ваш основной код asprintf — это именно то, что я имел в виду, когда определял возвращаемое значение snprintf так, как я это сделал. :-) - person torek; 21.04.2012

Не используйте fflush() в сетевых сокетах. Это небуферизованные потоки.

Кроме того, этот код:

//read a single line
for(size = 0; size < bufsize; size++) {
    data = fgetc(sock);
    if(data == EOF)
        break;
    if(data == '\n') {
        buf[size] = 0;
        break;
    }
    buf[size] = (char) data;
}

не читает ни строчки. Он считывает только до размера буфера, который вы определили как 8. sock по-прежнему будет иметь данные для получения, которые вы должны получить до записи в поток с помощью fputs. Кстати, вы можете заменить весь этот блок на

fgets(buf, bufsize, sock);
person smocking    schedule 21.04.2012
comment
@gcbenison: Да, fgets() читает не более n-1 символов (поскольку nth или bufsizeth в данном случае зарезервированы для завершающего нулевого байта), но останавливается на первой новой строке, если она возникает до того, как закончится место. - person torek; 21.04.2012
comment
Простая очистка буфера чтения перед записью, конечно же, решит эту проблему. Но что, если обработка пользовательского ввода занимает значительное время, в течение которого приходят новые сообщения и снова заполняется буфер чтения, а после завершения обработки я отправляю ответ, не зная о том, что за это время появились новые данные? приехал? Я получил бы другую ошибку Illegal seek. - person mic_e; 21.04.2012
comment
На самом деле для ввода-вывода сокетов вам может быть лучше использовать POSIX read() и write(), а не fget* и fput*. Они лучше подходят для небуферизованных потоков и, я думаю, позволят вам отвечать, пока в сетевом стеке есть входящие данные. - person smocking; 21.04.2012
comment
Ты не прав. Без fflush() работать не будет. Вывод буферизируется glibc. - person Jakub Nowak; 28.04.2020

Да, вы можете использовать один файловый поток для обработки вашего сокета, по крайней мере, в Linux. Но вы должны быть осторожны с этим: вы должны использовать ferror() только для проверки ошибок. У меня есть код, который использует это и безупречно работает на крупном французском сайте.

Если вы используете errno или perror(), вы поймаете любую внутреннюю ошибку, с которой столкнется поток, даже если он захочет скрыть ее от вас. И «Незаконный поиск» — один из них.

Кроме того, для проверки реальных условий EOF вы должны использовать feof(), поскольку при возврате true это взаимоисключающее действие, а ferror() возвращает ненулевое значение. Это потому, что при использовании fgetc() у вас нет возможности отличить ошибку от реальных условий EOF. Так что вам, вероятно, лучше использовать fgets(), как указал другой пользователь.

Итак, ваш тест:

if(data == EOF) {
    perror("EOF: Connection reset by peer");
    break;
} else {
    printf("Input line: '%s'\n", buf);
}

Должно быть написано как:

int sock_error = ferror(sock);
if (sock_error) {
    fprintf(stderr, "Error while reading: %s", strerror(sock_error));
} else {
    printf("Input line: '%s'\n", buf);
}
person Bunch    schedule 21.04.2015

Попробуй это :

#define BUFSIZE 88

FILE* listen_on_port(unsigned short port) {
 ... 
}

int main(int argc, char** argv) {
    int bufsize = BUFSIZE;
    char buf[ BUFSIZE ];
person bill    schedule 15.12.2014