it-swarm.com.de

Schreiben an ein geschlossenes Lokal TCP Socket schlägt nicht fehl

Ich habe scheinbar ein Problem mit meinen Steckdosen. Unten sehen Sie Code, der einen Server und einen Client enthält. Der Server öffnet einen TCP-Socket, und der Client stellt eine Verbindung zu ihm her und schließt ihn dann. Sleeps werden verwendet, um das Timing zu koordinieren. Nach dem clientseitigen close () versucht der Server, write () an sein eigenes Ende der Verbindung TCP zu schreiben. Laut der write (2) -Manpage geben diese sollte mir ein SIGPIPE und ein EPIPE-Errno an. Ich sehe das aber nicht. Aus der Sicht des Servers, das Schreiben in ein lokales, geschlossenes Socket gelingt und ohne EPIPE Ich kann nicht erkennen, wie der Server erkennen soll, dass der Client den Socket geschlossen hat.

In der Lücke zwischen dem Schließen des Clients und dem Versuch des Servers, zu schreiben, zeigt ein Aufruf von netstat, dass sich die Verbindung im Zustand CLOSE_WAIT/FIN_WAIT2 befindet, sodass das Serverende das Schreiben auf jeden Fall ablehnen kann.

Als Referenz, ich bin auf Debian Squeeze, uname -r ist 2.6.39-bpo.2-AMD64.

Was ist denn hier los?


#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;
}
21
regularfry

Das sagt die Linux-Manpage über write und EPIPE:

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

Wenn Linux eine pipe oder eine socketpair verwendet, kann und wird das Leseende des Paares überprüft, wie diese beiden Programme zeigen würden:

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 ist dazu in der Lage, da der Kernel angeborene Kenntnisse über das andere Ende der Pipe oder das verbundene Paar besitzt. Bei Verwendung von connect wird der Status des Sockets jedoch vom Protokollstapel aufrechterhalten. Ihr Test demonstriert dieses Verhalten, aber unten ist ein Programm, das alles in einem einzigen Thread ausführt, ähnlich den beiden obigen Tests:

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

Wenn Sie das obige Programm ausführen, erhalten Sie eine Ausgabe ähnlich der folgenden:

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

Dies zeigt, dass eine write für die Sockets benötigt wurde, um in die CLOSED-Zustände zu wechseln. Um herauszufinden, warum dies der Fall war, kann ein TCP -Dump der Transaktion nützlich sein:

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

Die ersten drei Zeilen stehen für den 3-Wege-Handshake. Die vierte Zeile ist das FIN-Paket, das der Client an den Server sendet, und die fünfte Zeile ist die ACK vom Server, die den Empfang bestätigt. Die sechste Zeile ist der Server, der versucht, 1 Byte Daten mit dem gesetzten Flag Push an den Client zu senden. Die letzte Zeile ist das RESET-Paket des Clients, wodurch der TCP -Zustand für die Verbindung freigegeben wird. Aus diesem Grund hat der dritte netstat-Befehl zu keiner Ausgabe im obigen Test geführt.

Der Server weiß also nicht, dass der Client die Verbindung zurücksetzt, bis er versucht, Daten an ihn zu senden. Der Grund für das Zurücksetzen liegt darin, dass der Client close anstelle von etwas anderem aufgerufen hat.

Der Server kann nicht genau wissen, welchen Systemaufruf der Client tatsächlich ausgegeben hat, er kann nur dem Status TCP folgen. Beispielsweise könnten wir den Aufruf close durch einen Aufruf von shutdown ersetzen.

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

Der Unterschied zwischen shutdown und close besteht darin, dass shutdown nur den Status der Verbindung regelt, während close auch den Status des Datei-Deskriptors regelt, der den Socket darstellt. Eine shutdown wird keine close Socket.

Die Ausgabe ändert sich mit der shutdown-Änderung:

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

Der TCP -Dump wird auch etwas anderes zeigen:

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>

Beachten Sie, dass das Zurücksetzen am Ende 5 Sekunden nach dem letzten ACK-Paket erfolgt. Dieses Zurücksetzen ist darauf zurückzuführen, dass das Programm heruntergefahren wird, ohne die Steckdosen ordnungsgemäß zu schließen. Das ACK-Paket vom Client zum Server vor dem Zurücksetzen ist anders als zuvor. Dies ist der Hinweis, dass der Client close nicht verwendet hat. In TCP ist die Angabe FIN wirklich ein Hinweis darauf, dass keine Daten mehr gesendet werden müssen. Da eine TCP-Verbindung jedoch bidirektional ist, geht der Server, der die Variable FIN empfängt, davon aus, dass der Client noch Daten empfangen kann. Im obigen Fall akzeptiert der Kunde die Daten tatsächlich.

Unabhängig davon, ob der Client close oder SHUT_WR für die Ausgabe einer FIN verwendet, können Sie in jedem Fall die Ankunft der FIN ermitteln, indem Sie den Server-Socket nach einem lesbaren Ereignis abfragen. Wenn nach dem Aufruf von read das Ergebnis 0 lautet, wissen Sie, dass die FIN angekommen ist, und Sie können mit diesen Informationen tun, was Sie möchten.

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

Es ist trivial wahr, dass, wenn der Server SHUT_WR mit shutdown ausgibt, bevor er versucht, einen Schreibvorgang auszuführen, der EPIPE-Fehler tatsächlich angezeigt wird.

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

Wenn Sie stattdessen möchten, dass der Client den Server umgehend zurücksetzt, können Sie dies für die meisten TCP-Stacks erzwingen, indem Sie die Option verweilen mit einem verweilenden Timeout von 0 aktivieren, bevor Sie close aufrufen.

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

Mit der obigen Änderung wird die Ausgabe des Programms:

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

Die Variable send wird in diesem Fall sofort angezeigt, ist jedoch nicht EPIPE, sondern ECONNRESET. Der TCP -Dump spiegelt dies ebenfalls wider:

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>

Das RESET-Paket wird direkt nach Abschluss des 3-Wege-Handshakes bereitgestellt. Die Verwendung dieser Option birgt jedoch Gefahren. Wenn sich am anderen Ende ungelesene Daten im Socketpuffer befinden, wenn die Variable RESET ankommt, werden diese Daten gelöscht, wodurch die Daten verloren gehen. Das Senden einer RESET wird normalerweise in Anforderungs-/Antwortprotokollen verwendet. Der Absender der Anfrage kann wissen, dass keine Daten verloren gehen können, wenn er die gesamte Antwort auf seine Anfrage erhält. Dann ist es für den Anforderungssender sicher, eine RESET für die Verbindung zu erzwingen.

36
jxh

Nachdem Sie write() ein (erstes) Mal (wie in Ihrem Beispiel codiert) nach dem Client close()ed des Sockets aufgerufen haben, erhalten Sie bei jedem nachfolgenden Aufruf von write () die erwartete EPIPE und SIGPIPE.

Fügen Sie einfach ein weiteres write () hinzu, um den Fehler zu provozieren:

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

Die Ausgabe wird sein:

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

Die Ausgabe der letzten beiden printf()s fehlt, da der Prozess beendet wird, da SIGPIPE durch den zweiten Aufruf von write() ausgelöst wird. Um die Beendigung des Prozesses zu vermeiden, möchten Sie möglicherweise, dass der Prozess SIGPIPE ignoriert.

2
alk

Sie haben zwei Sockets - einen für den Client und einen für den Server. Nun führt Ihr Client das aktive Schließen durch. Dies bedeutet, dass die TCP-Verbindung Vom Client gestartet wurde (A tcp FIN-Segment wurde von gesendet, die der Client sendet). 

Zu diesem Zeitpunkt sehen Sie den Client-Socket im Status FIN_WAIT1. Wie ist nun der Status des Server-Sockets? Es befindet sich im Zustand CLOSE_WAIT. Daher ist der Server-Socket nicht geschlossen.

Die FIN vom Server wurde noch nicht gesendet. (Warum - da die Anwendung den Socket nicht geschlossen hat). In diesem Stadium überschreiben Sie den Server-Socket, damit keine Fehlermeldung angezeigt wird.

Wenn Sie nun den Fehler sehen möchten, schreiben Sie close (client_fd), bevor Sie den Socket überschreiben.

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

Hier befindet sich der Server-Socket nicht mehr im CLOSE_WAIT-Status, sodass Sie den Rückgabewert von Write -ve anzeigen können, um den Fehler anzuzeigen. Ich hoffe das klärt.

Ich schätze, Sie laufen in den Stack von TCP, der eine fehlgeschlagene Sendung erkennt und versucht, eine erneute Übertragung durchzuführen. Scheitern nachfolgende Aufrufe von write() im Hintergrund? Mit anderen Worten, schreiben Sie fünfmal in den geschlossenen Socket und prüfen Sie, ob Sie eventuell einen SIGPIPE erhalten. Und wenn Sie sagen, dass das Schreiben erfolgreich ist, erhalten Sie ein Ergebnis von 3?

0
David G

Ich vermute, dass der serverseitige Socket immer noch gültig ist, sodass Ihr Schreibaufruf einen gültigen Versuch unternimmt, in Ihren Dateideskriptor zu schreiben, obwohl sich Ihre Sitzung TCP in einem geschlossenen Zustand befindet. Wenn ich völlig falsch bin, lass es mich wissen.

0
Eric Y