我的插座似乎有问题。在下面,您将看到一些分叉服务器和客户端的代码。服务器打开一个TCP套接字,客户端连接到它,然后关闭它。睡眠用于协调时间。在客户端close()之后,服务器尝试将write()写入其自己的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; }
这是Linux手册页关于write和的内容EPIPE:
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使用a pipe或a时socketpair,它可以并且将检查对的 读取结束 ,这两个程序将演示:
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,有关套接字的状态由协议栈维护。您的测试演示了这种行为,但是下面是一个在单个线程中完成所有操作的程序,类似于上面的两个测试:
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转储可能很有用:
CLOSED
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来自服务器的确认收据的数据包。第六行是服务器尝试将PUSH设置了标志的1字节数据发送到客户端。最后一行是客户端RESET数据包,该数据包将释放连接的TCP状态,这就是为什么第三条netstat命令在上述测试中未产生任何输出的原因。
FIN
ACK
PUSH
RESET
netstat
因此,服务器直到尝试向其发送一些数据后才知道客户端将重置连接。进行重置的原因是因为客户端调用了close,而不是其他名称。
close
服务器无法确定客户端实际发出了什么系统调用,它只能遵循TCP状态。例如,我们可以将close呼叫替换为shutdown。
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>
请注意,最后一次重置是在最后一个ACK数据包之后5秒钟进行的。此重置是由于程序未正确关闭套接字而关闭的。ACK重置之前从客户端到服务器的数据包与以前不同。这表明客户端未使用close。在TCP中,该FIN指示实际上表示没有更多数据要发送。但是,由于TCP连接是双向的,因此接收FIN假定的服务器的客户端仍可以接收数据。在上述情况下,客户端实际上确实接受数据。
无论客户端是使用close还是SHUT_WR发出FIN,在两种情况下,都可以FIN通过在服务器套接字上轮询可读事件来检测的到来。如果在调用read结果之后是0,那么您知道FIN到达了,那么您可以使用该信息来做您想做的事情。
SHUT_WR
read
0
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");
相反,如果您希望客户端指示立即重置服务器,则可以通过启用linger选项(0在调用之前有一个较长的超时时间)来强制大多数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这种情况下获取的即时错误,但它不是EPIPE,它是ECONNRESET。TCP转储也反映了这一点:
send
ECONNRESET
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在连接上强制发送a是安全的。