it-roy-ru.com

Запись на закрытый, местный TCP сокет не выходит из строя

Кажется, у меня проблема с сокетами. Ниже вы увидите код, который разветвляет сервер и клиент. Сервер открывает сокет TCP, и клиент подключается к нему, а затем закрывает его. Сны используются для координации времени. После закрытия close () на стороне клиента сервер пытается написать () собственный конец соединения TCP. Согласно справочной странице 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;
}
21
regularfry

Вот что говорится в справочной странице 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 или 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 состояние сокета поддерживается стеком протоколов. Ваш тест демонстрирует это поведение, но ниже приведена программа, которая делает все это в одном потоке, аналогично двум вышеупомянутым тестам:

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. Чтобы выяснить, почему это произошло, может быть полезен дамп транзакции 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. Например, мы могли бы вместо этого заменить вызов close вызовом shutdown.

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

Разница между shutdown и close заключается в том, что 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>

Обратите внимание, что сброс в конце происходит через 5 секунд после последнего пакета ACK. Этот сброс происходит из-за закрытия программы без правильного закрытия сокетов. Это пакет 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");

Если вместо этого вы хотите, чтобы клиент указывал немедленный сброс на сервер, вы можете принудительно сделать это в большинстве стеков TCP, включив опцию linger с задержкой на задержку 0 до вызова 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 получает немедленную ошибку, но это не 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 приходит сразу после завершения трехстороннего рукопожатия. Однако использование этой опции имеет свои опасности. Если на другом конце есть непрочитанные данные в буфере сокета, когда приходит RESET, эти данные будут удалены, что приведет к потере данных. Принудительная отправка RESET обычно используется в протоколах стиля запрос/ответ. Отправитель запроса может знать, что данные не могут быть потеряны при получении полного ответа на его запрос. Затем отправитель запроса может принудительно отправить RESET по соединению.

36
jxh

После вызова write() один (первый раз) (как показано в вашем примере) после того, как клиент close() выполнил сокет, вы получите ожидаемые EPIPE и SIGPIPE при любом последующем вызове write ().

Просто попробуйте добавить другой 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

Вывод последних двух printf() отсутствует, поскольку процесс завершается из-за того, что SIGPIPE был вызван вторым вызовом write(). Чтобы избежать завершения процесса, вы можете сделать так, чтобы процесс игнорировал SIGPIPE.

2
alk

У вас есть два сокета - один для клиента и другой для сервера . Теперь ваш клиент выполняет активное закрытие. Это означает, что соединение 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 is -ve для обозначения ошибки. Я надеюсь, что это проясняет.

2
Tanmoy Bandyopadhyay

Я предполагаю, что вы работаете в стеке TCP, обнаруживая неудачную отправку и пытаясь повторить передачу. Последующие вызовы write() молча проваливаются? Другими словами, попробуйте пять раз записать в закрытый сокет и посмотреть, получите ли вы в конечном итоге SIGPIPE. И когда вы говорите «успешно», вы получаете результат возврата 3?

0
David G

Я подозреваю, что происходит то, что сокет на стороне сервера все еще действителен, поэтому ваш вызов записи делает правильную попытку записи в ваш файловый дескриптор, даже если ваш TCP сеанс находится в закрытом состоянии. Если я полностью неправ, дайте мне знать.

0
Eric Y