有时候,在网络传输时我们会遇到数据接收不全的情况。这很可能是遇到了网络编程的又一个“坑”。
先来看代码:
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.     int len = 10*1024*1024;
  19.     char *buf = malloc(len);
  20.     if (buf == NULL)
  21.         ERR_EXIT("malloc error");
  22.     while (1) {
  23.         int conn;
  24.         struct sockaddr_in peeraddr;
  25.         socklen_t peerlen = sizeof(peeraddr);
  26.         if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
  27.             ERR_EXIT("accept error");
  28.         printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

  29.         sleep(5);
  30.         int writelen;
  31.         writelen = send(conn, buf, len, 0);
  32.         printf("writelen = %d\n", writelen);
  33.         close(conn);
  34.         printf("close\n");
  35.     }
  36.     close(listenfd);

  37.     return 0;
  38. }
我们看到在服务器端,一旦有客户端连接上,就向他发送10M数据。中间为了实验效果延时了5秒。

在客户端,我们用nc命令接收服务器发送过来的数据:
# cat client.sh
nc localhost 5188 | wc -c
nc是netcat的缩写,在网络工具中有“瑞士军刀”美称。其功能十分强大,可以传输文件、扫描端口、抓包等。

运行程序:
# ./server     
recv connect ip=127.0.0.1 port=33384
writelen = 10485760
close

# ./client.sh
10485760

可以看到,客户端接收全了数据。
如果客户端也向服务器发送了数据会怎么样呢?
# ./server
recv connect ip=127.0.0.1 port=33385
writelen = 10485760
close

# ./client.sh
abc
6969964
在运行client.sh后,输入abc
这次只接收到了6M多的数据。

原因是,当调用close时,如果接收缓冲区中还有数据,将会引起RST,连接立刻终止。此时发送缓冲区中的数据可能还未全部发送,就引起了数据丢失。
要避免这种情况,就必须保证在调用close前接收缓冲区中没有数据。问题的关键是close函数会关闭读和写端,而不能像tcp协议中四次挥手可以独立关闭读端或写端。因此我们需要引入一个新的函数:shutdown。该函数允许只停止在某个方向上的数据传输。

点击(此处)折叠或打开

  1. int shutdown(int sockfd,int how);
sockfd是需要关闭的socket描述符,how允许为shutdown操作选择以下几种方式:
SHUT_RD:关闭连接的读端。
SHUT_WR:关闭连接的写端.
SHUT_RDWR:关闭连接的读写端,相当于调用close


我们可以使用shutdown函数先关闭服务器的写端,然后等待read返回0。shutdown函数会向客户端发送FIN,客户端read返回0,然后客户端调用close,服务器的read返回0,再调用close
具体流程如下:
server    client
send
shutdown  
          recv返回0
          close
recv返回0
close

服务器端在send调用后新增代码:

  1.     int ret = shutdown(conn, SHUT_WR);
  2.     printf("shutdown ret %d\n", ret);

  3.     int readlen;
  4.     while (1) {
  5.         readlen = recv(conn, buf, len, 0);
  6.         if (readlen == 0)
  7.             break;
  8.     }

运行:
# ./server
recv connect ip=127.0.0.1 port=33385
writelen = 10485760
close

# ./client.sh
abc
10485760
客户端收到了完整的数据。

到目前为止,这个问题被完美解决了吗?没有。试想如果遇到恶意的客户端,故意不close,那么服务器的recv永远不会返回0,连接永远不会被关闭。解决的方法也很简单,增加超时退出机制。recv的阻塞超时怎么设置?使用setsockopt函数,这个在这里就不啰嗦了。

本文中的代码可以在这里下载。
12-23 09:23