27

ソケットに問題があるようです。以下に、サーバーとクライアントをフォークするコードをいくつか示します。サーバーは TCP ソケットを開き、クライアントはそれに接続してから閉じます。スリープは、タイミングを調整するために使用されます。クライアント側の close() の後、サーバーは TCP 接続の独自の側に write() を試みます。write(2) のマニュアル ページによると、これにより SIGPIPE および EPIPE errno が返されるはずです。しかし、私はこれを見ません。サーバーの観点からは、ローカルの閉じられたソケットへの書き込みは成功し、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;
}
4

5 に答える 5

48

これは、Linuxのマニュアルページに次のように記載されていwriteます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.)

Linux がpipeまたはを使用している場合、次の 2 つのプログラムが示すように、ペアの読み取り終了socketpairをチェックできます。

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、ソケットに関する状態はプロトコル スタックによって維持されます。あなたのテストはこの動作を示していますが、以下は上記の 2 つのテストと同様に、単一のスレッドですべてを実行するプログラムです。

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

これはwrite、ソケットがCLOSED状態に遷移するのに 1 時間を要したことを示しています。これが発生した理由を調べるには、トランザクションの 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

最初の 3 行は、3 ウェイ ハンドシェイクを表します。4 行目はFINクライアントがサーバーに送信するパケットで、5 行目はACKサーバーからの受信確認です。PUSH6 行目は、フラグを設定してクライアントに 1 バイトのデータを送信しようとしているサーバーです。最後の行はクライアントRESETパケットです。これにより、接続の TCP 状態が解放されますnetstat。これが、上記のテストで 3 番目のコマンドが出力を返さなかった理由です。

したがって、サーバーは、クライアントがデータを送信しようとするまで、クライアントが接続をリセットすることを知りません。リセットの理由は、クライアントがclose他の何かではなく を呼び出したためです。

サーバーは、クライアントが実際に発行したシステム コールを確実に知ることはできず、TCP の状態に従うことしかできません。たとえばclose、呼び出しをshutdown代わりにへの呼び出しに置き換えることができます。

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

shutdownとの違いcloseshutdown、接続の状態のみを管理し、ソケットを表すファイル記述子closeの状態も管理することです。Aはソケットではありません。shutdownclose

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が発生した場合shutdown、実際にはEPIPEエラーが発生することは自明です。

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

0代わりに、クライアントがサーバーへの即時リセットを示すようにしたい場合は、呼び出し前のリンガー タイムアウトで linger オプションを有効にすることにより、ほとんどの TCP スタックで強制的に発生させることができますclose

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場合、 はすぐにエラーになりますが、そうではありませEPIPEECONNRESET。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、そのデータは消去され、データが失われます。a の送信を強制するRESETことは、通常、要求/応答スタイルのプロトコルで使用されます。要求の送信者は、その要求に対する応答全体を受信すると、データが失われる可能性がないことを知ることができます。次に、要求送信者がRESET接続で a を強制的に送信しても安全です。

于 2012-07-19T00:49:20.853 に答える
3

クライアント用とサーバー用の 2 つのソケットがあります。これは、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 であることがわかります。これが明確になることを願っています。

于 2012-07-11T16:50:08.560 に答える
2

クライアントがソケットを編集したwrite()後に(あなたの例でコード化されているように)1回(最初に)呼び出した後、write()への連続した呼び出しで期待されるものを取得します。close()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

への 2 番目の呼び出しによって発生したprintf()ためにプロセスが終了するため、最後の 2 つの の出力が欠落しています。プロセスの終了を避けるために、プロセスを無視させたい場合があります。SIGPIPEwrite()SIGPIPE

于 2012-07-12T11:16:52.233 に答える
0

何が起こっているのかは、サーバー側のソケットがまだ有効であるため、TCPセッションが閉じた状態であっても、書き込み呼び出しがファイル記述子への書き込みを有効に試みていると思われます。私が完全に間違っている場合は私に知らせてください。

于 2012-07-11T15:49:06.397 に答える
0

送信の失敗を検出して再送信を試みているTCPスタックに遭遇していると思います。後続の呼び出しはwrite()サイレントに失敗しますか?つまり、閉じたソケットに5回書き込んでみて、最終的にSIGPIPEを取得するかどうかを確認してください。そして、「成功」と書くと、3の結果が返されますか?

于 2012-07-11T16:24:10.613 に答える