Chinaunix首页 | 论坛 | 博客
  • 博客访问: 302212
  • 博文数量: 21
  • 博客积分: 250
  • 博客等级: 二等列兵
  • 技术积分: 484
  • 用 户 组: 普通用户
  • 注册时间: 2010-10-06 23:10
个人简介

程序猿

文章分类

全部博文(21)

文章存档

2016年(17)

2014年(3)

2013年(1)

分类: 网络与安全

2016-01-03 12:07:03

首先看代码。(完整代码在下载)

server.c
  1. int main(void)
  2. {
  3.     int listenfd;
  4.     if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
  5.         ERR_EXIT("socket error");

  6.     struct sockaddr_in servaddr;
  7.     memset(&servaddr, 0, sizeof(servaddr));
  8.     servaddr.sin_family = AF_INET;
  9.     servaddr.sin_port = htons(5188);
  10.     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

  11.     int on = 1;
  12.     if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
  13.         ERR_EXIT("setsockopt error");

  14.     if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
  15.         ERR_EXIT("bind error");

  16.     if (listen(listenfd, SOMAXCONN) < 0)
  17.         ERR_EXIT("listen error");

  18.     while (1) {
  19.         int conn;
  20.         struct sockaddr_in peeraddr;
  21.         socklen_t peerlen = sizeof(peeraddr);
  22.         if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
  23.             ERR_EXIT("accept error");
  24.         printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

  25.         while (1) {
  26.             char recvbuf[1024];
  27.             int readlen, writelen;

  28.             readlen = recv(conn, recvbuf, sizeof(recvbuf), 0);
  29.             printf("read len = %d\n", readlen);
  30.             if (readlen <= 0)
  31.                 break;
  32.             writelen = send(conn, recvbuf, readlen, 0);
  33.             printf("write1 len = %d\n", writelen);
  34.             sleep(1);
  35.             writelen = send(conn, recvbuf, readlen, 0);
  36.             printf("write2 len = %d\n", writelen);
  37.             writelen = send(conn, recvbuf, readlen, 0);
  38.             printf("write3 len = %d\n", writelen);
  39.             if (writelen < 0)
  40.                 perror("write error");
  41.         }
  42.         printf("close\n");
  43.         close(conn);
  44.     }
  45.     close(listenfd);

  46.     return 0;
  47. }
client.c
  1. int main(int argc, const char *argv[])
  2. {
  3.     int sock;
  4.     char buffer[1024];

  5.     if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
  6.         ERR_EXIT("socket error");

  7.     struct sockaddr_in servaddr;
  8.     memset(&servaddr, 0, sizeof(servaddr));
  9.     servaddr.sin_family = AF_INET;
  10.     servaddr.sin_port = htons(5188);
  11.     servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

  12.     if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
  13.         ERR_EXIT("connect error");

  14.     printf("write...\n");
  15.     send(sock, buffer, sizeof(buffer), 0);
  16.     printf("read...\n");
  17.     recv(sock, buffer, sizeof(buffer), 0);

  18.     printf("close\n");
  19.     close(sock);

  20.     return 0;
  21. }
代码很简单,客户端发送一串数据,等待接收,然后关闭socket退出。服务器端接收到数据后,把数据发送三次给客户端。
为了让问题容易暴露,在第一和第二次发送之间加了延时。

执行结果:
# ./server
recv connect ip=127.0.0.1 port=60944
read len = 1024
write len = 1024
write len = 1024

# ./client
write...
read...
close

我们看到,服务器在发送两次数据之后竟然退出了!而且没有任何错误信息!

使用gdb试试有没有线索:
# gdb ./server       
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
.
Find the GDB manual and other documentation resources online at:
.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./server...done.
(gdb) r
Starting program: /root/zhengdi/network/network_examples/sigpipe/server
recv connect ip=127.0.0.1 port=60947
read len = 1024
write len = 1024
write len = 1024

Program received signal SIGPIPE, Broken pipe.
0x00007ffff7b104fd in __libc_send (fd=4, buf=0x7fffffffe140, n=1024, flags=-1)
    at ../sysdeps/unix/sysv/linux/x86_64/send.c:27
27      ../sysdeps/unix/sysv/linux/x86_64/send.c: No such file or directory.
(gdb) bt
#0  0x00007ffff7b104fd in __libc_send (fd=4, buf=0x7fffffffe140, n=1024, flags=-1)
    at ../sysdeps/unix/sysv/linux/x86_64/send.c:27
#1  0x0000000000400cc3 in main () at server.c:61
可以看到,服务器在调用第三次send函数时收到了SIGPIPE信号。

关于SIGPIPE信号的解释:
SIGPIPE
    The SIGPIPE signal is sent to a process when it attempts to write to a pipe without a process connected to the other end.

由于程序没对该信号处理,该信号的默认动作是退出进程。

我们结合TCP断开连接时的四次握手来分析一下。
假设客户端调用close函数,其本意是结束与服务器端的通信,即不再进行发送和接收。但是,根据TCP协议,协议栈会只发送一个FIN给服务器 ,此时,客户端只是关闭了对服务器“写”的通道,服务器并不知道它还能不能对客户端写数据(即,服务器不知道客户端调用的是close还是shutdown)。
如果服务器再发送数据给客户端,由于客户端的连接已断开,服务器会收到RST,协议栈无法把该状态返回给应用层。当服务器再次给客户端发送数据时,由于TCP协议层已处于RST状态,数据将不会发出,而是返回SIGPIPE信号给应用层。

SIGPIPE在写服务器端的程序时,一般都需要忽略掉。在程序开始处添加:signal(SIGPIPE, SIG_IGN);
运行程序:
# ./server     
recv connect ip=127.0.0.1 port=32832
read len = 1024
write1 len = 1024
write2 len = 1024
write3 len = -1
write error: Broken pipe
read len = 0
close
recv connect ip=127.0.0.1 port=32833
read len = 1024
write1 len = 1024
write2 len = 1024
write3 len = -1
write error: Broken pipe
read len = 0
close

程序还是接收到了SIGPIPE信号,但不会自动退出了。

最后一个问题,既然写服务器程序的时候都要忽略SIGPIPE,那么SIGPIPE用在什么时候呢?
答案是,在命令行执行管道程序时,这个信号就很有用。例如,cat log | grep error | head,当log数据量很大,只要找出了前10行包含error行,grep和cat就会依次接收到SIGPIPE信号退出。

阅读(3806) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~