Parece que tengo un problema con mis enchufes. A continuación, verá un código que bifurca un servidor y un cliente. El servidor abre un socket TCP y el cliente se conecta a él y luego lo cierra. Los sueños se utilizan para coordinar el tiempo. Después del cierre() del lado del cliente, el servidor intenta escribir() en su propio extremo de la conexión TCP. De acuerdo con la página del manual write(2), esto deberían dame un SIGPIPE y un EPIPE errno. Sin embargo, no veo esto. Desde el punto de vista del servidor, la escritura en un socket local cerrado tiene éxitoy en ausencia del EPIPE, no puedo ver cómo el servidor debería detectar que el cliente ha cerrado el socket.
En el intervalo entre el cliente que cierra su extremo y el servidor que intenta escribir, una llamada a netstat mostrará que la conexión está en un estado CLOSE_WAIT/FIN_WAIT2, por lo que el extremo del servidor definitivamente debería poder rechazar la escritura.
Como referencia, estoy en Debian Squeeze, uname -r es 2.6.39-bpo.2-amd64.
¿Que está pasando aqui?
#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;
}
Esto es lo que dice la página de manual de Linux sobre write
y 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.)
Cuando Linux está usando un pipe
o un socketpair
puede y comprobará la final de lectura de la pareja, como demostrarían estos dos programas:
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 puede hacerlo porque el kernel tiene un conocimiento innato sobre el otro extremo de la tubería o el par conectado. Sin embargo, al usar connect
, la pila de protocolos mantiene el estado del socket. Su prueba demuestra este comportamiento, pero a continuación hay un programa que lo hace todo en un solo hilo, similar a las dos pruebas anteriores:
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 ejecuta el programa anterior, obtendrá un resultado similar a este:
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
Esto muestra que tomó uno write
para que los enchufes hagan la transición al CLOSED
estados Para averiguar por qué ocurrió esto, un volcado TCP de la transacción puede ser útil:
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
Las primeras tres líneas representan el apretón de manos de 3 vías. La cuarta línea es la FIN
paquete que el cliente envía al servidor, y la quinta línea es el ACK
del servidor, acusando recibo. La sexta línea es el servidor que intenta enviar 1 byte de datos al cliente con el PUSH
conjunto de banderas La línea final es el cliente. RESET
paquete, lo que hace que se libere el estado TCP para la conexión, y es por eso que el tercer netstat
El comando no dio como resultado ningún resultado en la prueba anterior.
Por lo tanto, el servidor no sabe que el cliente restablecerá la conexión hasta que intente enviarle algunos datos. El motivo del reinicio es porque el cliente llamó close
en lugar de otra cosa.
El servidor no puede saber con certeza qué llamada al sistema ha emitido realmente el cliente, solo puede seguir el estado de TCP. Por ejemplo, podríamos reemplazar el close
llamar con una llamada a shutdown
en lugar de.
//close(c_sock);
shutdown(c_sock, SHUT_WR);
La diferencia entre shutdown
y close
es eso shutdown
sólo rige el estado de la conexión, mientras que close
también rige el estado de la descriptor de archivo que representa el zócalo. A shutdown
no close
un zócalo
La salida será diferente con el shutdown
cambiar:
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
El volcado de TCP también mostrará algo diferente:
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>
Observe que el reinicio al final llega 5 segundos después del último ACK
paquete. Este restablecimiento se debe a que el programa se cerró sin cerrar correctamente los enchufes. Es el ACK
paquete del cliente al servidor antes del reinicio que es diferente al anterior. Esta es la indicación de que el cliente no usó close
. En TCP, el FIN
indicación es realmente una indicación de que no hay más datos para enviar. Pero como una conexión TCP es bidireccional, el servidor que recibe la FIN
asume que el cliente aún puede recibir datos. En el caso anterior, el cliente sí acepta los datos.
Si el cliente usa close
o SHUT_WR
emitir un FIN
en cualquier caso se puede detectar la llegada del FIN
sondeando en el socket del servidor para un evento legible. Si después de llamar read
el resultado es 0
entonces sabes el FIN
ha llegado, y puedes hacer lo que quieras con esa información.
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"); }
}
Ahora, es trivialmente cierto que si el servidor emite SHUT_WR
con shutdown
antes de que intente hacer una escritura, de hecho obtendrá el EPIPE
error.
shutdown(s_sock, SHUT_WR);
if (send(s_sock, "a", 1, MSG_NOSIGNAL) < 0) perror("send");
Si, en cambio, desea que el cliente indique un restablecimiento inmediato en el servidor, puede forzar que eso suceda en la mayoría de las pilas de TCP habilitando la opción de permanencia, con un tiempo de espera de permanencia de 0
antes de llamar close
.
struct linger lo = { 1, 0 };
setsockopt(c_sock, SOL_SOCKET, SO_LINGER, &lo, sizeof(lo));
close(c_sock);
Con el cambio anterior, la salida del programa se convierte en:
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
los send
obtiene un error inmediato en este caso, pero no es EPIPE
está ECONNRESET
. El volcado de TCP también refleja esto:
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>
los RESET
el paquete llega justo después de que se completa el protocolo de enlace de 3 vías. Sin embargo, usar esta opción tiene sus peligros. Si el otro extremo tiene datos no leídos en el búfer del socket cuando el RESET
llega, esos datos se eliminarán y se perderán. forzando un RESET
to be sent se usa generalmente en protocolos de estilo de solicitud/respuesta. El remitente de la solicitud puede saber que no puede haber pérdida de datos cuando recibe la respuesta completa a su solicitud. Entonces, es seguro para el remitente de la solicitud forzar una RESET
para ser enviado en la conexión.
Tiene dos sockets, uno para el cliente y otro para el servidor. Ahora su cliente está realizando el cierre activo. Esto significa que el cliente ha iniciado la finalización de la conexión de TCP (se ha enviado un segmento TCP FIN desde el envío del cliente).
En esta etapa, verá el socket del cliente en el estado FIN_WAIT1. Ahora, ¿cuál es el estado del socket del servidor ahora? Está en estado CLOSE_WAIT. Por lo tanto, el socket del servidor no está cerrado.
El FIN del servidor aún no se ha enviado. (Por qué, ya que la aplicación no ha cerrado el zócalo). En esta etapa, está escribiendo sobre el socket del servidor para que no obtenga un error.
Ahora, si desea ver el error, simplemente escriba close(client_fd) antes de escribir sobre el socket.
close(client_fd);
printf( "Write result: %d\n", write( client_fd, "123", 3 ) );
Aquí, el socket del servidor ya no está en estado CLOSE_WAIT, por lo que puede ver que el valor de retorno de escritura es -ve para indicar el error. Espero que esto aclare.
Después de haber llamado write()
una (primera) vez (como se codifica en su ejemplo) después del cliente close()
ed el socket, obtendrá el esperado EPIPE
y SIGPIPE
en cualquier llamada sucesiva a write().
Simplemente intente agregar otra escritura () para provocar el error:
...
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 salida será:
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 salida de los dos últimos printf()
s falta ya que el proceso termina debido a SIGPIPE
siendo planteada por la segunda llamada a write()
. Para evitar la finalización del proceso, es posible que desee hacer que el proceso ignore SIGPIPE
.
no estás configurando
ai_family = AF_INET
pero asumes que obtienes unsockaddr_in
devuelto Es probable que se rompa en algún momento en el futuro.– Por Johansson
11 de julio de 2012 a las 17:46
A riesgo de no responder la pregunta, ¿por qué confía en una escritura () para ver si la conexión está cerrada? ¿Has mirado select() y/o poll()? Al bloquear en accept(), siempre acepta la primera conexión a su puerto, ya sea la conexión que desea o no.
– ChrisH
12 de julio de 2012 a las 0:01
¿Has probado un
shutdown()
en el socket del lado del cliente antes de llamarclose()
?– alk
12 de julio de 2012 a las 8:16
@ChrisH: Obtengo un resultado similar con select(), cerrar el socket desde el extremo del cliente es invisible para el servidor. No hace que select() devuelva el fd en ninguno de los tres estados.
– freír regular
12 de julio de 2012 a las 8:20
@regularfry, el socket debe devolverse en el conjunto de lectura de select, ya que obtiene EOF en la lectura.
– Por Johansson
15 de julio de 2012 a las 17:21