Ошибка "незаконного поиска" при работе с потоками сокетов с непустыми буферами чтения

Я сейчас пишу серверное приложение на 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,

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

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. (Существуют способы реализации).

Да, вы можете использовать один файловый поток для обработки вашего сокета, по крайней мере, в 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);
}

Не использовать 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);

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

#define BUFSIZE 88

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

int main(int argc, char** argv) {
    int bufsize = BUFSIZE;
    char buf[ BUFSIZE ];
Другие вопросы по тегам