web-dev-qa-db-fra.com

Écrire à un local fermé TCP prise ne manque pas

Je semble avoir un problème avec mes prises. Ci-dessous, vous verrez du code qui forge un serveur et un client. Le serveur ouvre un socket TCP. Le client s'y connecte puis le ferme. Les couchages sont utilisés pour coordonner le timing. Après la fermeture côté client, le serveur essaie d'écrire () à sa propre extrémité de la connexion TCP. Selon la page de manuel write (2), this devrait me donne un SIGPIPE et un EPIPE errno. Cependant, je ne vois pas cela. Du point de vue du serveur, l'écriture sur un socket local fermé réussit et sans l'EPIPE, je ne vois pas comment le serveur devrait détecter que le client a fermé le socket.

Dans l’espace entre le client qui ferme son extrémité et le serveur qui tente d’écrire, un appel à netstat indiquera que la connexion est dans un état CLOSE_WAIT/FIN_WAIT2. L’extrémité du serveur devrait donc pouvoir refuser l’écriture.

Pour référence, je suis sur Debian Squeeze, uname -r est 2.6.39-bpo.2-AMD64.

Que se passe t-il ici?


#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

Voici ce que dit la page de manuel Linux à propos de write et 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.)

Lorsque Linux utilise une pipe ou une socketpair, il peut et va vérifier le fin de lecture de la paire, comme le montrent ces deux programmes:

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 est capable de le faire, car le noyau a une connaissance innée de l’autre extrémité du tuyau ou de la paire connectée. Cependant, lorsque vous utilisez connect, l'état de la socket est maintenu par la pile de protocoles. Votre test démontre ce comportement, mais vous trouverez ci-dessous un programme qui le fait en un seul thread, similaire aux deux tests ci-dessus:

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

Si vous exécutez le programme ci-dessus, vous obtiendrez une sortie semblable à celle-ci:

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

Cela montre qu'il a fallu une write pour que les sockets passent aux états CLOSED. Pour savoir pourquoi cela s’est produit, un vidage de la transaction TCP peut être utile:

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

Les trois premières lignes représentent la poignée de main à trois voies. La quatrième ligne est le paquet FIN que le client envoie au serveur et la cinquième ligne est la ACK du serveur, accusant réception. La sixième ligne est le serveur qui tente d'envoyer un octet de données au client avec l'indicateur Push. La dernière ligne est le paquet client RESET, qui provoque la libération de la connexion sur l'état TCP. C'est pourquoi la troisième commande netstat n'a généré aucune sortie lors du test ci-dessus.

Ainsi, le serveur ignore que le client réinitialisera la connexion jusqu'à ce qu'il essaie de lui envoyer des données. La raison de la réinitialisation est que le client a appelé close, au lieu d’autre chose.

Le serveur ne peut pas savoir avec certitude quel appel système le client a réellement émis, il ne peut que suivre l'état TCP. Par exemple, nous pourrions remplacer l'appel close par un appel à shutdown.

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

La différence entre shutdown et close est que shutdown ne gouverne que l'état de la connexion, alors que close gouverne également l'état du descripteur de fichier représentant le socket. shutdown ne sera pas close un socket.

La sortie sera différente avec le changement 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

Le vidage TCP montrera également quelque chose de différent:

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>

Notez que la réinitialisation à la fin intervient 5 secondes après le dernier paquet ACK. Cette réinitialisation est due à l’arrêt du programme sans fermer correctement les sockets. C'est le paquet ACK du client au serveur avant la réinitialisation qui est différent du précédent. Ceci indique que le client n'a pas utilisé close. En TCP, l'indication FIN est en réalité une indication qu'il n'y a plus de données à envoyer. Mais comme une connexion TCP est bidirectionnelle, le serveur qui reçoit la variable FIN suppose que le client peut toujours recevoir des données. Dans le cas ci-dessus, le client accepte effectivement les données.

Que le client utilise close ou SHUT_WR pour émettre une FIN, dans les deux cas, vous pouvez détecter l'arrivée de la FIN en interrogeant le socket du serveur sur un événement lisible. Si, après avoir appelé read, le résultat est 0, alors vous savez que la FIN est arrivée et vous pouvez faire ce que vous voulez avec ces informations.

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

Maintenant, il est trivialement vrai que si le serveur émet SHUT_WR avec shutdown avant d'essayer d'écrire, il obtiendra en fait l'erreur EPIPE.

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

Si vous souhaitez plutôt que le client indique une réinitialisation immédiate au serveur, vous pouvez forcer cette opération sur la plupart des piles TCP en activant l'option Linger, avec un délai d'attente de 0 avant d'appeler close.

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

Avec le changement ci-dessus, la sortie du programme devient:

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

La send obtient une erreur immédiate dans ce cas, mais ce n'est pas EPIPE, c'est ECONNRESET. Le vidage TCP reflète également ceci:

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>

Le paquet RESET arrive juste après la prise de contact à trois. Cependant, cette option présente des dangers. Si l'autre extrémité a des données non lues dans le tampon de socket lorsque la RESET arrive, ces données seront purgées, ce qui entraînera leur perte. Forcer l'envoi d'une RESET est généralement utilisé dans les protocoles de style requête/réponse. L'expéditeur de la demande peut savoir qu'aucune donnée ne peut être perdue lorsqu'il reçoit la réponse complète à sa demande. Ensuite, l'expéditeur de la demande peut forcer l'envoi d'une RESET sur la connexion.

36
jxh

Après avoir appelé write() une (première) fois (comme codé dans votre exemple) après le client close()ed du socket, vous obtiendrez la EPIPE et la SIGPIPE attendues lors de tout appel ultérieur à write ().

Essayez simplement d’ajouter un autre write () pour provoquer l’erreur:

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

La sortie sera:

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

La sortie des deux derniers printf()s est manquante à la fin du processus, car SIGPIPE est généré par le deuxième appel à write(). Pour éviter la fin du processus, vous pouvez faire en sorte que le processus ignore SIGPIPE.

2
alk

Vous avez deux sockets - une pour le client et une autre pour le serveur. Maintenant, votre client effectue la fermeture active.Cela signifie que la terminaison de la connexion De TCP a été lancée par le client (un segment tcp FIN a été envoyé par le client a envoyé). 

A ce stade, vous voyez le socket client dans l'état FIN_WAIT1. Maintenant, quel est l'état du socket du serveur maintenant? Il est dans l'état CLOSE_WAIT. Le socket du serveur n'est donc pas fermé.

Le FIN du serveur n'a pas encore été envoyé. (Pourquoi - puisque l'application n'a pas fermé la socket). À ce stade, vous écrivez sur le socket du serveur afin d'éviter toute erreur.

Maintenant, si vous voulez voir l'erreur, écrivez simplement close (client_fd) avant d'écrire sur le socket.

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

Ici, le socket du serveur n’est plus dans l’état CLOSE_WAIT, vous pouvez donc voir que la valeur renvoyée de Write est -ve pour indiquer l’erreur. J'espère que cela clarifie.

2

Je suppose que vous rencontrez la pile TCP en train de détecter un échec d'envoi et de tenter une retransmission. Les appels suivants à write() échouent-ils silencieusement? En d'autres termes, essayez d'écrire cinq fois sur le socket fermé et voyez si vous obtenez éventuellement un SIGPIPE. Et lorsque vous dites que l'écriture "réussit", obtenez-vous un résultat de retour de 3?

0
David G

Je soupçonne que ce qui se passe si le socket côté serveur est toujours valide. Votre appel d'écriture tente donc correctement d'écrire dans votre descripteur de fichier, même si votre session TCP est à l'état fermé. Si je me trompe complètement, faites le moi savoir.

0
Eric Y