如何可靠关闭 TCP 连接

write 的含义

  用write()向 socket 描述符写入n个字节,write()返回n,这意味着什么呢?你可能这样想,write()返回n,那不就表示对方已经收到n个字节了吗?

1
2
char buffer[1024];
ssize_t n = write(sockfd, buffer, sizeof(buffer));

  然而,实际上write()返回n仅仅保证将n个字节写入内核中sockfd的发送缓冲区。但是内核是否已经将数据转发给网卡,对方是否收到了数据,就无从得知了。

TCP 的可靠关闭

  既然调用write()无法确保对方收到了数据,那么在调用write()之后就调用close()来关闭连接是否合适呢?RFC1122指出,由于 TCP 是全双工的,调用close()时如果有可读数据停留在接收缓冲区,或者在close()之后收到新的数据,数据就无法被读取,这时 TCP 将会发送 RST 通知对方数据已经丢失了。


  让我们看一个例子,首先客户端发送1000000个字节给服务端:

1
2
3
4
5
6
7
// write n bytes to socket descriptor, see UNP volume 1
ssize_t writen(int sockfd, const void *buf, size_t n);
char data[1000000];
ssize_t n = writen(sockfd, data, sizeof(data));
printf("write %zd bytes\n", n); // write 1000000 bytes
close(sockfd);

  服务端在接收客户端的连接之后,首先write()一行数据给客户端,接着读取从客户端发来的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void handle_client(int sockfd)
{
write(sockfd, "welcome\r\n", 9);
read_data(sockfd);
close(sockfd);
}
void read_data(int sockfd)
{
char buffer[4096];
ssize_t bytes = 0;
while(true)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n < 0)
{
if (errno == EINTR) { continue; }
fprintf(stderr, "get error after read %zd bytes: %s\n",
bytes, strerror(errno));
break;
}
if (n == 0)
{
printf("Connection close by peer\n");
break;
}
bytes += n;
}
printf("read %zd bytes\n", bytes);
}

  程序看起来似乎没有问题,但运行服务端程序就会发现,尽管客户端发送1000000个字节,但服务端在收到961180个字节之后,收到了 RST segment:

1
get error after read 961180 bytes: Connection reset by peer

  让我们分析一下上面的代码:

  • 客户端调用close()时,它的接收缓冲区有一行数据未读取,这时close()将导致客户端发送一个 RST 给服务端。
  • 服务端调用read()返回-1,错误信息为:Connection reset by peer

  只要客户端在close()之前,读取完接收缓冲区的数据,就不会出现错误了,具体可以这样做:

  • 客户端写完所有数据,调用shutdown(sockfd, SHUT_WR),这会导致客户端发送一个 FIN 给服务端。
  • 服务端读取完接收缓冲区的数据之后,read()将会返回0,这时服务端将会调用close()关闭连接,导致一个 FIN 发送给客户端。
  • 客户端读取完接收缓冲区的数据之后,read()返回0,这时调用close()关闭连接。

  所以我们只需要修改客户端的几行代码,就可以实现连接的可靠关闭:

1
2
3
4
5
6
7
char data[1000000];
ssize_t n = writen(sockfd, data, sizeof(data));
printf("write %zd bytes\n", n); // write 1000000 bytes
shutdown(sockfd, SHUT_WR);
read_data(sockfd);
close(sockfd);

  这次运行服务端就会发现数据接收正确:

1
2
Connection close by peer
read 1000000 bytes

参考资料