“Illegal seek” error when working with socket streams with non-empty read buffers

狂风中的少年 提交于 2019-12-10 13:33:48

问题


I'm currently writing a server application on Linux x86_64 using <sys/socket.h>. After accepting a connection via accept(), I use fdopen() to wrap the retrieved socket into a FILE* stream.

Writing to, and reading from, that FILE* stream usually works quite well, but the socket becomes unsusable as soon as I write to it while it has a non-empty read buffer.

For demonstration purposes, I've written some code that listens for a connection, then reads the input, line by line, into a read buffer using fgetc(). If the line is too long to fit into the buffer, it's not completely read, but instead read during the next iteration.

#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");
}

The code should compile in gcc without any special parameters. Run it with the port number as argument and use netcat to connect to it locally.

Now, if you try sending strings that are shorter than 8 characters, this will run flawlessly. But if you send a string containing more than 10 characters, the program will fail. This sample input:

ab
cd
abcdefghij

Will create this output:

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

As you see, (rightly) only the first 8 characters of abcdefgh are read, but when the program tries to send the 'ack' string (which the client never receves), and then flush the output buffer, we receive an Illegal seek error, and the next call to fgetc() returns EOF.

If the fflush() part is commented out, the same error still occurs, but the

fflush failed: Illegal seek

line is missing from the server output.

If the fputs(ack) part is commented out, everything seems to work as intended, but a perror() manually called from gdb still reports an 'Illegal seek' error.

If both fputs(ack) and fflush() are commented out, everything does work as intended.

Unfortunately, I've not been able to find any good documentation, nor any Internet discussions on this problem, so your help would be greatly appreciated.

edit

The solution i finally settled for is to not use fdopen() and FILE*, since there seems to be no clean way of converting a socket fd into a FILE* that can reliably used in r+ mode. Instead, I directly worked on the socket fd, writing my own replacement code for fputs and fprintf.

If anyone needs it, here is the code.


回答1:


Clearly "r+" (read/write) mode does not work on sockets in this implementation, no doubt because the underlying code assumes that it must do a seek to switch between reading and writing. This is the general case with stdio streams (that you must do some kind of synchronizing operation), because back in the Dim Time, actual stdio implementations had only a single counter per stream, and it was either a counter of "number of characters left to read from stream buffer via getc macro" (in read mode) or "number of characters that can safely be written to stream buffer via putc macro (in write mode). To that that single counter re-set, you had to do a seek-type operation.

Seeks are not allowed on pipes and sockets (since "file offset" is not meaningful there).

One solution is not to wrap a socket with stdio at all. Another, probably easier / better for your purposes, is to wrap it with, not one, but two stdio streams:

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

There's another flaw here though, because when you go to fclose one stream, that closes the other's file descriptor. To work around that, you need to dup the socket descriptor once (in either of the two calls above, it does not matter which one).

If you intend to use select or poll or similar on the socket at some point, you should generally go for the "don't wrap with stdio" solution, since there's no nice clean portable way to track stdio buffering. (There are implementation-specific ways).




回答2:


Don't use fflush() on network sockets. They are unbuffered streams.

Also, this code:

//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;
}

does not read a single line. It only reads up to the buffer size, which you defined as 8. sock will still have data for you to receive which you should receive before writing to the stream with fputs. BTW you can replace that whole block with

fgets(buf, bufsize, sock);



回答3:


Yes, you can use one file stream to handle your socket, at least on Linux. But you should be careful with it: you must only use ferror() to test for errors. I have some code that use this and run flawlessly in production on a major French site.

If you use errno or perror() you'll catch any internal error that the stream will encounter, even if it wants to hide it to you. And "Illegal seek" is one of them.

Also, to test for real EOF conditions, you should use feof(), since when returning true it's mutually exclusive with ferror() returning a non-zero value. It's because, when using fgetc() you don't have any mean to differentiate error from real EOF conditions. So you should probably better use fgets() as another user pointed out.

So, your test:

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

Should be written as:

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



回答4:


Try this :

#define BUFSIZE 88

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

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


来源:https://stackoverflow.com/questions/10255663/illegal-seek-error-when-working-with-socket-streams-with-non-empty-read-buffer

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!