reg*_*fry 22 c sockets linux tcp glibc
我的套接字似乎有问题.下面,您将看到一些分叉服务器和客户端的代码.服务器打开TCP套接字,客户端连接到它,然后关闭它.睡眠用于协调时间.在客户端关闭()之后,服务器尝试将write()写入其自己的TCP连接端.根据write(2)手册页,这应该给我一个SIGPIPE和一个EPIPE错误.但是,我没有看到这一点.从服务器的角度来看,写入本地已关闭的套接字成功,如果没有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;
}
Run Code Online (Sandbox Code Playgroud)
jxh*_*jxh 39
这是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.)
Run Code Online (Sandbox Code Playgroud)
当Linux使用a pipe
或a时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);
}
Run Code Online (Sandbox Code Playgroud)
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);
Run Code Online (Sandbox Code Playgroud)
如果运行上面的程序,您将获得类似于此的输出:
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
Run Code Online (Sandbox Code Playgroud)
这表明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
Run Code Online (Sandbox Code Playgroud)
前三行代表三次握手.第四行是FIN
客户端发送到服务器的数据包,第五行是ACK
来自服务器的数据包,确认收到.第六行是服务器尝试将1字节数据发送到客户端并设置PUSH
标志.最后一行是客户端RESET
数据包,它会释放连接的TCP状态,这就是为什么第三个netstat
命令在上面的测试中没有产生任何输出的原因.
因此,服务器不知道客户端将重置连接,直到它尝试向其发送一些数据.重置的原因是因为客户端调用了close
而不是其他东西.
服务器无法确定客户端实际发出了什么系统调用,它只能遵循TCP状态.例如,我们可以通过close
调用替换呼叫shutdown
.
//close(c_sock);
shutdown(c_sock, SHUT_WR);
Run Code Online (Sandbox Code Playgroud)
shutdown
和之间的区别在于close
它shutdown
仅控制连接的状态,同时close
还控制表示套接字的文件描述符的状态.A 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
Run Code Online (Sandbox Code Playgroud)
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>
Run Code Online (Sandbox Code Playgroud)
请注意最后一个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"); }
}
Run Code Online (Sandbox Code Playgroud)
现在,它是平凡的事实,如果服务器的问题SHUT_WR
与shutdown
它试图做一个写之前,它实际上将得到EPIPE
错误.
shutdown(s_sock, SHUT_WR);
if (send(s_sock, "a", 1, MSG_NOSIGNAL) < 0) perror("send");
Run Code Online (Sandbox Code Playgroud)
相反,如果你希望客户端指示立即复位到服务器,你可以强制通过启用灵儿选项大多数TCP协议栈的情况发生,用的灵儿超时0
调用之前close
.
struct linger lo = { 1, 0 };
setsockopt(c_sock, SOL_SOCKET, SO_LINGER, &lo, sizeof(lo));
close(c_sock);
Run Code Online (Sandbox Code Playgroud)
通过上述更改,程序的输出变为:
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
Run Code Online (Sandbox Code Playgroud)
在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>
Run Code Online (Sandbox Code Playgroud)
该RESET
包是在三次握手完成之后.但是,使用此选项有其危险性.如果另一端在RESET
到达时在套接字缓冲区中有未读数据,则将清除该数据,从而导致数据丢失.强制a RESET
发送通常用于请求/响应样式协议.请求的发送方可以知道在收到对其请求的整个响应时不会丢失数据.然后,请求发送方强制RESET
在连接上发送一个是安全的.