1. 程序源码
http://my.oschina.net/lowkey2046/blog/693852
2. 测试方法
a. 启动服务器进程
$ ./tcpserv01 &
[1] 4717
b. 启动客户端进程,输入 hello world
客户端能接收到服务器数据
$ ./tcpcli01 127.0.0.1
hello world
hello world
c. kill 掉与客户端连接的进程
$ ps -aux | grep tcpserv01
test 4717 0.0 0.0 4224 632 pts/1 S 14:08 0:00 ./tcpserv01
test 4721 0.0 0.0 4224 84 pts/1 S 14:08 0:00 ./tcpserv01
test 4728 0.0 0.0 18500 928 pts/1 S+ 14:08 0:00 grep --color=auto tcpserv01
$ kill 4721
child 4721 terminated
kill 掉与客户端相连的服务器进程,客户端程序并没有终止。
因为客户端阻塞在 fgets上,该函数正等待标准输入的数据,客户端程序还不知道服务器程序已经终止。
d. 客户端继续输入字符 terminated
terminated
str_cli: read EOF
因为客户端并不知道服务器程序已经终止,会调用 write 将字符串 'terminated' 发送给服务器。
wirte 只负责将数据写入套接字缓冲区,也就是说,wirte 返回成功,只能说明数据已经成功的写入到系统的套接字缓冲区,并不能说明对方已经接收到数据。
当客户端 read socket 时,读到了服务器终止时发送过来的 'FIN',也就是 EOF,此时客户端终止。
3. wireshark 截图
a. 当 kill 掉与客户端相连接的服务器进程时,服务器会向客户端发送一个 'FIN',客户端响应一个 'ACK'。 服务器向客户端发送 'FIN' 只能说明服务器数据发送完成,并不能说明服务器不再接收数据。因此向客户端的 socket wirte 数据不会出错。
b. 当客户端向已经终止的服务器程序发送数据时,服务器会发送一个 'RST'。
4. SIGPIPE 信号
向已收到 RST 的套接字执行写操作
程序源码
修改之前的程序源码
#include <signal.h>
void sig_pipe(int signo)
{
fprintf(stderr, "signo = %d\n", signo);
return;
}
void str_cli(FILE *fp, int sockfd)
{
char sendbuf[MAXBUF], recvbuf[MAXBUF];
int recvn;
signal(SIGPIPE, sig_pipe);
while (fgets(sendbuf, MAXBUF, fp) != NULL) {
if (write(sockfd, sendbuf, 1) < 0) {
perror("write");
return;
}
sleep(1);
// 向某个已经收到 RST 的进程继续执行写操作
if (write(sockfd, sendbuf + 1, strlen(sendbuf+1)) < 0) {
perror("write");
return;
}
sleep(1);
if ((recvn = read(sockfd, recvbuf, sizeof(recvbuf)-1)) < 0) {
fprintf(stderr, "str_cli: server terminated prematurely");
} else if(recvn == 0) {
fprintf(stderr, "str_cli: read EOF\n");
return;
}
recvbuf[recvn] = '\0';
fputs(recvbuf, stdout);
}
}
执行测试程序
客户端执行
$ ./tcpcli02 127.0.0.1
hello world
hello world
此时将与客户端连接的服务器进程 kill 掉。客户端继续输入:
terminated
signo = 13
write: Broken pipe
当一个进程向某个已收到 RST 的套接字执行写操作时,内核向该进程发送一个 SIGPIPE 信号。该信号的默认行为是终止进程,因此进程必须捕获它以避免不情愿的终止。
不论该进程是捕获了该信号并从信号处理函数返回,还是简单的忽略信号,写操作都将返回 EPIPE 错误。
参考资料
《UNP》