有时候,在网络传输时我们会遇到数据接收不全的情况。这很可能是遇到了网络编程的又一个“坑”。
先来看代码:
server.c
-
int main(void)
-
{
-
int listenfd;
-
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
-
ERR_EXIT("socket error");
-
-
struct sockaddr_in servaddr;
-
memset(&servaddr, 0, sizeof(servaddr));
-
servaddr.sin_family = AF_INET;
-
servaddr.sin_port = htons(5188);
-
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
-
-
int on = 1;
-
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
-
ERR_EXIT("setsockopt error");
-
-
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
-
ERR_EXIT("bind error");
-
-
if (listen(listenfd, SOMAXCONN) < 0)
-
ERR_EXIT("listen error");
-
-
int len = 10*1024*1024;
-
char *buf = malloc(len);
-
if (buf == NULL)
-
ERR_EXIT("malloc error");
-
while (1) {
-
int conn;
-
struct sockaddr_in peeraddr;
-
socklen_t peerlen = sizeof(peeraddr);
-
if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
-
ERR_EXIT("accept error");
-
printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
-
-
sleep(5);
-
int writelen;
-
writelen = send(conn, buf, len, 0);
-
printf("writelen = %d\n", writelen);
-
close(conn);
-
printf("close\n");
-
}
-
close(listenfd);
-
-
return 0;
-
}
我们看到在服务器端,一旦有客户端连接上,就向他发送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。该函数允许只停止在某个方向上的数据传输。
-
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调用后新增代码:
-
int ret = shutdown(conn, SHUT_WR);
-
printf("shutdown ret %d\n", ret);
-
-
int readlen;
-
while (1) {
-
readlen = recv(conn, buf, len, 0);
-
if (readlen == 0)
-
break;
-
}
运行:
# ./server
recv connect ip=127.0.0.1 port=33385
writelen = 10485760
close
# ./client.sh
abc
10485760
客户端收到了完整的数据。
到目前为止,这个问题被完美解决了吗?没有。试想如果遇到恶意的客户端,故意不close,那么服务器的recv永远不会返回0,连接永远不会被关闭。解决的方法也很简单,增加超时退出机制。recv的阻塞超时怎么设置?使用setsockopt函数,这个在这里就不啰嗦了。
本文中的代码可以在下载。
阅读(3297) | 评论(0) | 转发(0) |