"Illegal seek" error when working with socket streams with non-empty read buffers
Asked Answered
U

4

6

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.

Umlaut answered 21/4, 2012 at 2:25 Comment(0)
L
5

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).

Lactalbumin answered 21/4, 2012 at 7:33 Comment(2)
So there is no clean way of using FILE* streams, especially a single FILE* stream? Before wrapping the socket descriptor up in a FILE* stream, I had my own wrappers for printf() and puts(), but i found it to be ugly. Suppose it's the most 'clean' solution after all.Umlaut
Yes. Possibly ironically, your "basically asprintf" code is exactly what I had in mind when I defined the return value of snprintf the way I did. :-)Lactalbumin
K
1

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);
Kutaisi answered 21/4, 2012 at 3:7 Comment(4)
@gcbenison: Yes, fgets() reads no more than n-1 characters (because the nth, or bufsizeth in this case, is reserved for the terminating zero byte) but stops at the first newline if one occurs before running out of space.Lactalbumin
Simply emptying the read buffer before writing would, of course, solve this problem. But what if the processing of the user input takes significant time, during which new messages arrive and the read buffer gets filled again, and after the processing is finished I'm sending a reply, unknowing of the fact that in the meantime new data has arrived? I'd get an other Illegal seek error.Umlaut
Actually for socket I/O you might be better off with the POSIX read() and write() functions rather than fget* and fput*. They are better suited to unbuffered streams and I think will allow you to reply while there is incoming data on the network stack.Kutaisi
You're wrong. It won't work without the fflush(). The output is buffered by glibc.Commodious
C
1

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);
}
Casablanca answered 21/4, 2015 at 4:54 Comment(0)
T
0

Try this :

#define BUFSIZE 88

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

int main(int argc, char** argv) {
    int bufsize = BUFSIZE;
    char buf[ BUFSIZE ];
Toothlike answered 15/12, 2014 at 10:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.