先来看代码:
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;
- }
在客户端,我们用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);
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函数,这个在这里就不啰嗦了。
本文中的代码可以在这里下载。