首页 文章

写入封闭的本地TCP套接字不会失败

提问于
浏览
21

我的套接字似乎有问题 . 下面,您将看到一些分叉服务器和客户端的代码 . 服务器打开TCP套接字,客户端连接到它,然后关闭它 . 睡眠用于协调时间 . 在客户端关闭()之后,服务器尝试将write()写入其自己的TCP连接端 . 根据write(2)手册页,这应该给我一个SIGPIPE和一个EPIPE错误 . 但是,我没有_179645_的观点,写入本地,关闭套接字成功,并且没有EPIPE我看不到服务器应该如何检测客户端已关闭套接字 .

在关闭其结束的客户端和尝试写入的服务器之间的间隙中,对netstat的调用将显示连接处于CLOSE_WAIT / FIN_WAIT2状态,因此服务器端应该绝对能够拒绝写入 .

作为参考,我在Debian Squeeze上,uname -r是2.6.39-bpo.2-amd64 .

这里发生了什么?


#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/tcp.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>

#include <netdb.h>

#define SERVER_ADDRESS "127.0.0.7"
#define SERVER_PORT 4777


#define myfail_if( test, msg ) do { if((test)){ fprintf(stderr, msg "\n"); exit(1); } } while (0)
#define myfail_unless( test, msg ) myfail_if( !(test), msg )

int connect_client( char *addr, int actual_port )
{
    int client_fd;

    struct addrinfo hint;
    struct addrinfo *ailist, *aip;


    memset( &hint, '\0', sizeof( struct addrinfo ) );
    hint.ai_socktype = SOCK_STREAM;

    myfail_if( getaddrinfo( addr, NULL, &hint, &ailist ) != 0, "getaddrinfo failed." );

    int connected = 0;
    for( aip = ailist; aip; aip = aip->ai_next ) {
        ((struct sockaddr_in *)aip->ai_addr)->sin_port = htons( actual_port );
        client_fd = socket( aip->ai_family, aip->ai_socktype, aip->ai_protocol );

        if( client_fd == -1) { continue; }
        if( connect( client_fd, aip->ai_addr, aip->ai_addrlen) == 0 ) {
            connected = 1;
            break;
        }
        close( client_fd );
    }

    freeaddrinfo( ailist );

    myfail_unless( connected, "Didn't connect." );
    return client_fd;
}


void client(){
    sleep(1);
    int client_fd = connect_client( SERVER_ADDRESS, SERVER_PORT );

    printf("Client closing its fd... ");
    myfail_unless( 0 == close( client_fd ), "close failed" );
    fprintf(stdout, "Client exiting.\n");
    exit(0);
}


int init_server( struct sockaddr * saddr, socklen_t saddr_len )
{
    int sock_fd;

    sock_fd = socket( saddr->sa_family, SOCK_STREAM, 0 );
    if ( sock_fd < 0 ){
        return sock_fd;
    }

    myfail_unless( bind( sock_fd, saddr, saddr_len ) == 0, "Failed to bind." );
    return sock_fd;
}

int start_server( const char * addr, int port )
{
    struct addrinfo *ailist, *aip;
    struct addrinfo hint;
    int sock_fd;

    memset( &hint, '\0', sizeof( struct addrinfo ) );
    hint.ai_socktype = SOCK_STREAM;
    myfail_if( getaddrinfo( addr, NULL, &hint, &ailist ) != 0, "getaddrinfo failed." );

    for( aip = ailist; aip; aip = aip->ai_next ){
        ((struct sockaddr_in *)aip->ai_addr)->sin_port = htons( port );
        sock_fd = init_server( aip->ai_addr, aip->ai_addrlen );
        if ( sock_fd > 0 ){
            break;
        } 
    }
    freeaddrinfo( aip );

    myfail_unless( listen( sock_fd, 2 ) == 0, "Failed to listen" );
    return sock_fd;
}


int server_accept( int server_fd )
{
    printf("Accepting\n");
    int client_fd = accept( server_fd, NULL, NULL );
    myfail_unless( client_fd > 0, "Failed to accept" );
    return client_fd;
}


void server() {
    int server_fd = start_server(SERVER_ADDRESS, SERVER_PORT);
    int client_fd = server_accept( server_fd );

    printf("Server sleeping\n");
    sleep(60);

    printf( "Errno before: %s\n", strerror( errno ) );
    printf( "Write result: %d\n", write( client_fd, "123", 3 ) );
    printf( "Errno after:  %s\n", strerror( errno ) );

    close( client_fd );
}


int main(void){
    pid_t clientpid;
    pid_t serverpid;

    clientpid = fork();

    if ( clientpid == 0 ) {
        client();
    } else {
        serverpid = fork();

        if ( serverpid == 0 ) {
            server();
        }
        else {
            int clientstatus;
            int serverstatus;

            waitpid( clientpid, &clientstatus, 0 );
            waitpid( serverpid, &serverstatus, 0 );

            printf( "Client status is %d, server status is %d\n", 
                    clientstatus, serverstatus );
        }
    }

    return 0;
}

5 回答

  • 0

    这是Linux手册页中关于 writeEPIPE 的内容:

    EPIPE  fd is connected to a pipe or socket whose reading end is closed.
              When this happens the writing process will also receive  a  SIG-
              PIPE  signal.  (Thus, the write return value is seen only if the
              program catches, blocks or ignores this signal.)
    

    当Linux使用 pipesocketpair 时,它可以并将检查该对的读取端,因为这两个程序将演示:

    void test_socketpair () {
        int pair[2];
        socketpair(PF_LOCAL, SOCK_STREAM, 0, pair);
        close(pair[0]);
        if (send(pair[1], "a", 1, MSG_NOSIGNAL) < 0) perror("send");
    }
    
    void test_pipe () {
        int pair[2];
        pipe(pair);
        close(pair[0]);
        signal(SIGPIPE, SIG_IGN);
        if (write(pair[1], "a", 1) < 0) perror("send");
        signal(SIGPIPE, SIG_DFL);
    }
    

    Linux能够这样做,因为内核对管道或连接对的另一端有先天的了解 . 但是,使用 connect 时,协议栈会维护套接字的状态 . 您的测试演示了这种行为,但下面是一个程序,它在一个线程中完成所有操作,类似于上面的两个测试:

    int a_sock = socket(PF_INET, SOCK_STREAM, 0);
    const int one = 1;
    setsockopt(a_sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    struct sockaddr_in a_sin = {0};
    a_sin.sin_port = htons(4321);
    a_sin.sin_family = AF_INET;
    a_sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    bind(a_sock, (struct sockaddr *)&a_sin, sizeof(a_sin));
    listen(a_sock, 1);
    int c_sock = socket(PF_INET, SOCK_STREAM, 0);
    fcntl(c_sock, F_SETFL, fcntl(c_sock, F_GETFL, 0)|O_NONBLOCK);
    connect(c_sock, (struct sockaddr *)&a_sin, sizeof(a_sin));
    fcntl(c_sock, F_SETFL, fcntl(c_sock, F_GETFL, 0)&~O_NONBLOCK);
    struct sockaddr_in s_sin = {0};
    socklen_t s_sinlen = sizeof(s_sin);
    int s_sock = accept(a_sock, (struct sockaddr *)&s_sin, &s_sinlen);
    struct pollfd c_pfd = { c_sock, POLLOUT, 0 };
    if (poll(&c_pfd, 1, -1) != 1) perror("poll");
    int erropt = -1;
    socklen_t errlen = sizeof(erropt);
    getsockopt(c_sock, SOL_SOCKET, SO_ERROR, &erropt, &errlen);
    if (erropt != 0) { errno = erropt; perror("connect"); }
    puts("P|Recv-Q|Send-Q|Local Address|Foreign Address|State|");
    char cmd[256];
    snprintf(cmd, sizeof(cmd), "netstat -tn | grep ':%hu ' | sed 's/  */|/g'",
             ntohs(s_sin.sin_port));
    puts("before close on client"); system(cmd);
    close(c_sock);
    puts("after close on client"); system(cmd);
    if (send(s_sock, "a", 1, MSG_NOSIGNAL) < 0) perror("send");
    puts("after send on server"); system(cmd);
    puts("end of test");
    sleep(5);
    

    如果运行上面的程序,您将获得类似于此的输出:

    P|Recv-Q|Send-Q|Local Address|Foreign Address|State|
    before close on client
    tcp|0|0|127.0.0.1:35790|127.0.0.1:4321|ESTABLISHED|
    tcp|0|0|127.0.0.1:4321|127.0.0.1:35790|ESTABLISHED|
    after close on client
    tcp|0|0|127.0.0.1:35790|127.0.0.1:4321|FIN_WAIT2|
    tcp|1|0|127.0.0.1:4321|127.0.0.1:35790|CLOSE_WAIT|
    after send on server
    end of test
    

    这表明套接字转换到 CLOSED 状态需要一个 write . 要找出发生这种情况的原因,事务的TCP转储可能很有用:

    16:45:28 127.0.0.1 > 127.0.0.1
     .809578 IP .35790 > .4321: S 1062313174:1062313174(0) win 32792 <mss 16396,sackOK,timestamp 3915671437 0,nop,wscale 7>
     .809715 IP .4321 > .35790: S 1068622806:1068622806(0) ack 1062313175 win 32768 <mss 16396,sackOK,timestamp 3915671437 3915671437,nop,wscale 7>
     .809583 IP .35790 > .4321: . ack 1 win 257 <nop,nop,timestamp 3915671437 3915671437>
     .840364 IP .35790 > .4321: F 1:1(0) ack 1 win 257 <nop,nop,timestamp 3915671468 3915671437>
     .841170 IP .4321 > .35790: . ack 2 win 256 <nop,nop,timestamp 3915671469 3915671468>
     .865792 IP .4321 > .35790: P 1:2(1) ack 2 win 256 <nop,nop,timestamp 3915671493 3915671468>
     .865809 IP .35790 > .4321: R 1062313176:1062313176(0) win 0
    

    前三行代表三次握手 . 第四行是客户端发送到服务器的 FIN 数据包,第五行是来自服务器的 ACK ,确认收到 . 第六行是服务器尝试将1字节数据发送到设置了 PUSH 标志的客户端 . 最后一行是客户端 RESET 数据包,它导致连接的TCP状态被释放,这就是为什么第三个 netstat 命令在上面的测试中没有产生任何输出的原因 .

    因此,服务器不知道客户端将重置连接,直到它尝试向其发送一些数据 . 重置的原因是因为客户端调用 close 而不是其他东西 .

    服务器无法确定客户端实际发出了什么系统调用,它只能遵循TCP状态 . 例如,我们可以通过调用 shutdown 替换 close 调用 .

    //close(c_sock);
    shutdown(c_sock, SHUT_WR);
    

    shutdownclose 之间的区别在于 shutdown 仅控制连接的状态,而 close 也控制表示套接字的文件描述符的状态 . shutdown 不会 close 一个套接字 .

    输出将与 shutdown 更改不同:

    P|Recv-Q|Send-Q|Local Address|Foreign Address|State|
    before close on client
    tcp|0|0|127.0.0.1:4321|127.0.0.1:56355|ESTABLISHED|
    tcp|0|0|127.0.0.1:56355|127.0.0.1:4321|ESTABLISHED|
    after close on client
    tcp|1|0|127.0.0.1:4321|127.0.0.1:56355|CLOSE_WAIT|
    tcp|0|0|127.0.0.1:56355|127.0.0.1:4321|FIN_WAIT2|
    after send on server
    tcp|1|0|127.0.0.1:4321|127.0.0.1:56355|CLOSE_WAIT|
    tcp|1|0|127.0.0.1:56355|127.0.0.1:4321|FIN_WAIT2|
    end of test
    

    TCP转储也将显示不同的内容:

    17:09:18 127.0.0.1 > 127.0.0.1
     .722520 IP .56355 > .4321: S 2558095134:2558095134(0) win 32792 <mss 16396,sackOK,timestamp 3917101399 0,nop,wscale 7>
     .722594 IP .4321 > .56355: S 2563862019:2563862019(0) ack 2558095135 win 32768 <mss 16396,sackOK,timestamp 3917101399 3917101399,nop,wscale 7>
     .722615 IP .56355 > .4321: . ack 1 win 257 <nop,nop,timestamp 3917101399 3917101399>
     .748838 IP .56355 > .4321: F 1:1(0) ack 1 win 257 <nop,nop,timestamp 3917101425 3917101399>
     .748956 IP .4321 > .56355: . ack 2 win 256 <nop,nop,timestamp 3917101426 3917101425>
     .764894 IP .4321 > .56355: P 1:2(1) ack 2 win 256 <nop,nop,timestamp 3917101442 3917101425>
     .764903 IP .56355 > .4321: . ack 2 win 257 <nop,nop,timestamp 3917101442 3917101442>
    17:09:23
     .786921 IP .56355 > .4321: R 2:2(0) ack 2 win 257 <nop,nop,timestamp 3917106464 3917101442>
    

    注意最后的复位是在最后一个 ACK 数据包后5秒 . 此重置是由于程序关闭而未正确关闭套接字 . 它是在重置之前从客户端到服务器的 ACK 数据包与之前不同的数据包 . 这表明客户端未使用 close . 在TCP中, FIN 指示实际上表明没有更多数据要发送 . 但由于TCP连接是双向的,因此接收 FIN 的服务器假定客户端仍然可以接收数据 . 在上面的例子中,客户端实际上确实接受了数据 .

    无论客户端使用 close 还是 SHUT_WR 发出 FIN ,在任何一种情况下,您都可以通过在服务器套接字上轮询可读事件来检测 FIN 的到达 . 如果在调用 read 后结果为 0 ,那么您知道 FIN 已经到达,您可以使用该信息执行所需操作 .

    struct pollfd s_pfd = { s_sock, POLLIN|POLLOUT, 0 };
    if (poll(&s_pfd, 1, -1) != 1) perror("poll");
    if (s_pfd.revents|POLLIN) {
        char c;
        int r;
        while ((r = recv(s_sock, &c, 1, MSG_DONTWAIT)) == 1) {}
        if (r == 0) { /*...FIN received...*/ }
        else if (errno == EAGAIN) { /*...no more data to read for now...*/ }
        else { /*...some other error...*/ perror("recv"); }
    }
    

    现在,如果服务器在尝试执行写操作之前发出 SHUT_WR ,那么它实际上会得到 EPIPE 错误 .

    shutdown(s_sock, SHUT_WR);
    if (send(s_sock, "a", 1, MSG_NOSIGNAL) < 0) perror("send");
    

    相反,如果您希望客户端指示立即重置服务器,则可以通过启用逗留来强制在大多数TCP堆栈上执行此操作选项,在调用 close 之前,延迟超时为 0 .

    struct linger lo = { 1, 0 };
    setsockopt(c_sock, SOL_SOCKET, SO_LINGER, &lo, sizeof(lo));
    close(c_sock);
    

    通过上述更改,程序的输出变为:

    P|Recv-Q|Send-Q|Local Address|Foreign Address|State|
    before close on client
    tcp|0|0|127.0.0.1:35043|127.0.0.1:4321|ESTABLISHED|
    tcp|0|0|127.0.0.1:4321|127.0.0.1:35043|ESTABLISHED|
    after close on client
    send: Connection reset by peer
    after send on server
    end of test
    

    在这种情况下, send 会立即收到错误,但它不是 EPIPE ,而是 ECONNRESET . TCP转储也反映了这一点:

    17:44:21 127.0.0.1 > 127.0.0.1
     .662163 IP .35043 > .4321: S 498617888:498617888(0) win 32792 <mss 16396,sackOK,timestamp 3919204411 0,nop,wscale 7>
     .662176 IP .4321 > .35043: S 497680435:497680435(0) ack 498617889 win 32768 <mss 16396,sackOK,timestamp 3919204411 3919204411,nop,wscale 7>
     .662184 IP .35043 > .4321: . ack 1 win 257 <nop,nop,timestamp 3919204411 3919204411>
     .691207 IP .35043 > .4321: R 1:1(0) ack 1 win 257 <nop,nop,timestamp 3919204440 3919204411>
    

    RESET 数据包在3次握手完成后立即出现 . 但是,使用此选项有其危险性 . 如果 RESET 到达时另一端在套接字缓冲区中有未读数据,则该数据将被清除,从而导致数据丢失 . 强制发送 RESET 通常用于请求/响应样式协议 . 请求的发送方可以知道在收到对其请求的整个响应时不会丢失数据 . 然后,请求发送方可以安全地强制在连接上发送 RESET .

  • 0

    在客户端 close() 编辑套接字后调用 write() 一(第一)次(在您的示例中编码)后,您将在任何连续调用write()时获得预期的 EPIPESIGPIPE .

    只是尝试添加另一个write()来激发错误:

    ...
    printf( "Errno before: %s\n", strerror( errno ) );
    printf( "Write result: %d\n", write( client_fd, "123", 3 ) );
    printf( "Errno after:  %s\n", strerror( errno ) );
    
    printf( "Errno before: %s\n", strerror( errno ) );
    printf( "Write result: %d\n", write( client_fd, "A", 1 ) );
    printf( "Errno after:  %s\n", strerror( errno ) );
    ...
    

    输出将是:

    Accepting
    Server sleeping
    Client closing its fd... Client exiting.
    Errno before: Success
    Write result: 3
    Errno after:  Success
    Errno before: Success
    Client status is 0, server status is 13
    

    由于第二次调用 write() 引发了 SIGPIPE ,因此进程终止,最后两个 printf() 的输出丢失 . 为了避免终止进程,您可能希望使进程忽略 SIGPIPE .

  • 1

    你有两个套接字 - 一个用于客户端,另一个用于服务器 . 现在您的客户端正在进行主动关闭 . 这意味着TCP的连接终止已由客户端启动(已从客户端发送发送了tcp FIN段) .

    在此阶段,您将看到FIN_WAIT1状态的客户端套接字 . 现在服务器套接字的状态是什么?它处于CLOSE_WAIT状态 . 因此服务器套接字未关闭 .

    来自服务器的FIN尚未发送 . (为什么 - 因为应用程序没有关闭套接字) . 在此阶段,您正在编写服务器套接字,因此您不会收到错误 .

    现在,如果你想看到错误,只需在写入套接字之前写入close(client_fd) .

    close(client_fd);
    printf( "Write result: %d\n", write( client_fd, "123", 3 ) );
    

    这里服务器套接字不再处于CLOSE_WAIT状态,因此您可以看到write的返回值是-ve以指示错误 . 我希望这澄清一下 .

  • 36

    我怀疑正在发生的事情是服务器端套接字仍然有效,因此即使TCP会话处于关闭状态,您的写入调用也会有效地尝试写入文件描述符 . 如果我完全错了,请告诉我 .

  • 2

    我猜你正在运行TCP堆栈检测到发送失败并尝试重新传输 . 对 write() 的后续调用是否会无声地失败?换句话说,尝试五次写入已关闭的套接字,看看你是否最终获得了一个SIGPIPE . 当你说写'succeeds'时,你得到3的返回结果吗?

相关问题