使用具有非空读缓冲区的套接字流时出现"非法搜索"错误

mic*_*c_e 6 c sockets linux fdopen

我正在编写一个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");
}
Run Code Online (Sandbox Code Playgroud)

代码应该在没有任何特殊参数的gcc中编译.使用端口号作为参数运行它,并使用netcat在本地连接到它.

现在,如果你尝试发送短于8个字符的字符串,这将运行完美.但是如果发送包含10个以上字符的字符串,程序将失败.此示例输入:

ab
cd
abcdefghij
Run Code Online (Sandbox Code Playgroud)

将创建此输出:

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
Run Code Online (Sandbox Code Playgroud)

如你所见,(正确地)只读取abcdefgh的前8个字符,但是当程序试图发送'ack'字符串(客户端从不接收),然后刷新输出缓冲区时,我们收到Illegal seek错误,并且下一次调用fgetc()返回EOF.

如果fflush()部件被注释掉,仍会出现相同的错误,但是

fflush failed: Illegal seek
Run Code Online (Sandbox Code Playgroud)

服务器输出中缺少行.

如果fputs(ack)部件被注释掉,一切似乎都按预期工作,但是从gdb手动调用的perror()仍会报告"非法搜索"错误.

如果同时fputs(ack)fflush()被注释掉了,一切都没有像预期的那样.

不幸的是,我找不到任何好的文档,也没有关于这个问题的任何互联网讨论,所以非常感谢你的帮助.

编辑

该解决方案,我终于尘埃落定的是使用fdopen()FILE*,因为似乎是一个套接字fd转换成的免洗方式FILE*,可以可靠地使用r+模式.相反,我直接在socket fd上工作,为fputs和编写自己的替换代码fprintf.

如果有人需要它,这里是代码.

tor*_*rek 5

显然,在此实现中,“ r +”(读/写)模式不适用于套接字,这毫无疑问,因为底层代码假定它必须设法在读和写之间进行切换。这是stdio流的一般情况(您必须执行某种同步操作),因为回到Dim Time时,实际的stdio实现每个流只有一个计数器,并且要么是“剩余字符数”的计数器getc“ 通过宏从流缓冲区读取”(在读取模式下)或“可以通过putc宏安全地写入流缓冲区的字符数(在写模式下)。要重置该单个计数器,您必须执行一次搜索-类型操作。

不允许在管道和套接字上进行查找(因为“文件偏移”在那里没有意义)。

一种解决方案是根本不使用stdio包装套接字。为了您的目的,另一种可能(更容易/更好)的方法是将它包装成不是两个,而是两个 stdio流:

FILE *in = fdopen(newsock, "r");
FILE *out = fdopen(newsock, "w");
Run Code Online (Sandbox Code Playgroud)

但是,这里还有另一个缺陷,因为当您转到fclose一个流时,这会关闭另一个流的文件描述符。要解决此问题,您只需dup一次使用套接字描述符(在上述两个调用中的任何一个中,哪个都不重要)。

如果您打算在某个时候在套接字上使用selectpoll类似的方法,通常应该选择“不要用stdio包装”解决方案,因为没有很好的干净的便携式方法来跟踪stdio缓冲。(有特定于实现的方式)。