关闭 vs 断开socket连接?

252

在C语言中,关闭一个socket意味着该socket会被销毁并可以在之后重新使用。

那么shutdown呢?描述中说它关闭了与该socket的双工连接的一半。但是该socket是否会像close系统调用一样被销毁?


2
它无法再被重复使用。它已关闭。完成了。做完了。 - user207421
@user207421 他们可能是指套接字处理程序变量可以被重复使用(用于保存新的套接字),而不会造成内存泄漏。 - Daniel Chin
9个回答

227

这在 Beej's 网络编程指南中有 解释shutdown 是一种灵活的方式,可以阻止单向或双向的通信。 当第二个参数是 SHUT_RDWR 时,它将阻止发送和接收(就像 close 一样)。 不过,close 才是实际上销毁套接字的方法。

使用 shutdown,您仍然能够接收对等方已经发送但未处理的数据(感谢 Joey Adams 指出这一点)。


25
请记住,即使您关闭了TCP套接字(socket),它也不一定会立即可重用,因为在操作系统确保没有未完成的数据包可能会被误认为是新信息之前,它将处于TIME_WAIT状态。因此,如果您立即将该套接字用于其他用途,可能会出现混乱。 - alesplin
114
shutdown和close在套接字共享时的行为存在很大差异。shutdown()会影响所有该套接字的副本,而close()只会影响一个进程中的文件描述符。 - Zan Lynx
7
如果你使用了fdopen将socket转换成了FILE引用,那么你可能会希望在两个方向上使用shutdown而不是close,因为如果你关闭了socket,可能会有一个新打开的文件使用相同的文件描述符(fd),这样后续使用FILE读写操作就会造成错误,后果可能非常严重。如果你只是使用shutdown,那么后续对于FILE的使用只会产生错误,直到调用fclose函数。 - R.. GitHub STOP HELPING ICE
34
关于 TIME_WAIT 的评论是不正确的。这只适用于端口,而不是套接字。您无法重用套接字。 - user207421
61
这篇文章和链接都忽略了一个重要的概念原因,即希望使用“shutdown”的原因是向对等方发出EOF信号,同时仍然能够接收对等方发送的待处理数据。 - Joey Adams
显示剩余15条评论

208

现有的回答都没有说明如何在TCP协议级别上工作的shutdownclose,因此值得添加这个内容。

标准的TCP连接通过4次挥手来终止:

  1. 一个参与者没有更多数据可发送时,向另一个参与者发送FIN数据包
  2. 另一方返回给FIN数据包一个ACK确认
  3. 当另一方也完成数据传输时,它会再次发送一个FIN数据包
  4. 初始参与者返回一个ACK确认,并完成传输

但是,还存在另一种“紧急”关闭TCP连接的方式:

  1. 一个参与者发送RST数据包并且放弃连接
  2. 另一方接收到RST后也放弃连接

在我的Wireshark测试中,使用默认套接字选项,shutdown向另一端发送FIN数据包,但仅此而已。在另一方发送FIN数据包之前,您仍然可以接收数据。一旦发生这种情况,您的Receive将得到0大小的结果。因此,如果你是第一个关闭"send"的人,你应该在接收到数据后立即关闭套接字。

另一方面,在连接仍处于活动状态(另一方仍处于活动状态且您的系统缓冲区中可能有未发送的数据)时,如果调用close,将向另一端发送RST数据包。这对于错误处理很有用。例如,如果你认为另一方提供了错误的数据或拒绝提供数据(DOS攻击?),你可以立即关闭套接字。

我的规则意见是:

  1. 尽可能考虑使用shutdown而不是close
  2. 如果在决定关闭之前完成了接收(收到0大小的数据),请在最后一个发送(如果有)完成后关闭连接。
  • 如果要正常关闭连接,请关闭连接(使用SHUT_WR,如果在此之后您不关心接收数据,请也使用SHUT_RD),并等待直到收到0大小的数据,然后关闭套接字。
  • 在任何情况下,如果发生任何其他错误(例如超时),请直接关闭套接字。
  • SHUT_RD和SHUT_WR的理想实现

    以下实现尚未测试,请自行信任。但是,我相信这是一种合理和实用的处理方式。

    如果TCP堆栈仅接收到带有SHUT_RD的关机请求,它将标记该连接为不再期待数据。无论哪个线程还在进行中,所有挂起的和随后的read请求都将返回零大小的结果。但是,连接仍然处于活动状态且可用 - 您仍然可以接收OOB数据,例如。此外,操作系统将删除接收到的任何此连接的数据。但是这就是全部,不会向另一侧发送任何数据包。

    如果TCP堆栈仅接收到带有SHUT_WR的关机请求,则应将此连接标记为无法发送更多数据。所有挂起的写入请求将完成,但随后的写入请求将失败。此外,将向另一侧发送FIN包以通知其我们没有更多数据可发送。


    2
    如果您在连接仍然存在时调用close,则是自我重复的,并且不会导致发送RST。 (1) 不是必需的。在(4)中,超时并不一定对连接造成致命影响,并且不总是表明您可以关闭它。 - user207421
    6
    不,这不是自我证明的。你可以使用 shutdown() 关闭连接,这样它就不再活动了。你仍然拥有文件描述符,可以从接收缓冲区中继续 recv()。但你仍需要调用 close() 来释放文件描述符。 - Pavel Šimerda
    1
    这是关于线路格式的最佳答案。我对使用SHUT_RD进行关闭的更多细节很感兴趣。没有TCP信号表示不再期望更多数据,对吗?难道只有FIN用于表示不再发送更多数据的信号吗? - Pavel Šimerda
    4
    @PavelŠimerda 是的,TCP不会对不期望接收更多数据进行信号传递。这应该考虑到高层协议中。我认为,一般情况下这是不必要的。你可以关闭门,但你不能阻止人们在门前放礼物。这是他们自己的决定,而不是你的。 - Earth Engine
    1
    @EJP 你有任何实际的论据吗?否则我不感兴趣。 - Pavel Šimerda
    显示剩余3条评论

    38

    如果使用close()方法,会有一些限制,而使用shutdown()方法可以避免这些限制。

    close()方法将终止TCP连接的双向方向。有时候您想告诉对端您已经完成了发送数据,但仍想接收数据。

    close()方法会递减描述符的引用计数(在文件表条目中维护,并计算当前打开的引用文件/套接字数),如果描述符不为0,则不会关闭套接字/文件。这意味着,如果您正在分叉,则只有当引用计数降至0时,清除才会发生。使用shutdown()可以启动正常的TCP关闭序列,忽略引用计数。

    参数如下:

    int shutdown(int s, int how); // s is socket descriptor
    

    int how 表示以下三种取值之一:

    SHUT_RD0:禁止进一步的接收操作

    SHUT_WR1:禁止进一步的发送操作

    SHUT_RDWR2:禁止进一步的发送和接收操作


    10
    这两个功能是完全不同的。事实上,如果尚未启动关闭序列,则套接字上的最终关闭将启动关闭序列,但这并不改变关闭用于清理套接字数据结构,而关闭连接则用于启动TCP级别的关闭序列。 - Len Holgate
    27
    你不能仅仅使用“shutdown”命令。你可以同时使用它,但是你必须在某个时间关闭套接字。 - user207421

    19

    这可能与平台有关,但我有些怀疑。无论如何,我看到的最好的解释在 这个 msdn 页面 上,他们解释了关于关闭、逗留选项、套接字关闭和一般连接终止序列。

    总之,在 TCP 级别使用 shutdown 发送关闭序列,并使用 close 释放您的进程中套接字数据结构使用的资源。如果在调用关闭之前您还没有发出显式的关闭序列,则会为您启动一个序列。


    2
    任何试图实现HTTP连接优雅关闭的人都可能想阅读古老的Apache关于“延迟关闭”的笔记,可以在http://cluster.cis.drexel.edu/manual/misc/perf-tuning.html等地方找到,以及更现代的评论:https://apenwarr.ca/log/20090814。 - Ron Burk

    11

    我也在Linux下使用shutdown()成功地实现了一个线程强制中止另一个线程,该线程当前正被阻塞在connect()上。

    在其他操作系统(至少是OSX)中,我发现调用close()就足以使connect()失败。


    10

    "shutdown()并不会真正关闭文件描述符,它只是改变了其可用性。要释放套接字描述符,您需要使用close()函数。"1


    4

    关闭

    当你使用完一个套接字后,可以使用close函数来关闭其文件描述符;如果仍有数据等待传输到连接上,通常close会尝试完成这个传输。你可以使用SO_LINGER套接字选项来控制这种行为,指定一个超时时间;请参见Socket Options。

    关闭连接

    你也可以通过调用shutdown函数来仅关闭连接的接收或传输。

    shutdown函数关闭套接字的连接。它的参数how指定要执行的操作: 0 停止接收此套接字的数据。如果有更多的数据到达,拒绝它。 1 停止尝试从此套接字传输数据。丢弃任何等待发送的数据。停止寻找已发送数据的确认;如果数据丢失,则不重新传输。 2 停止接收和传输。

    成功返回0,失败返回-1。


    3

    在我的测试中。

    close会发送fin包并在套接字未与其他进程共享时立即销毁fd。

    shutdown SHUT_RD,进程仍然可以从套接字中接收数据,但是如果TCP缓冲区为空,则recv会返回0。在对等方发送更多数据后,recv将再次返回数据。

    shutdown SHUT_WR将发送fin包以指示进一步发送被禁止。对等方可以接收数据,但如果其TCP缓冲区为空,则它将接收到0。

    shutdown SHUT_RDWR(等同于同时使用SHUT_RDSHUT_WR)将在对等方发送更多数据时发送rst包。


    我刚尝试了一下,close() 在 Linux 上发送的是 RST 而不是 FIN。 - Pavel Šimerda
    你是想说在关闭读取端之后,程序会接收到更多的数据吗? - Pavel Šimerda
    @PavelŠimerda 我认为发送 RST 或 FIN 取决于 TCP 状态?我不确定。 - simpx
    1. "在对等方发送更多数据后,recv()不会再次返回数据" 是不正确的。
    2. 如果在 SHUT_RD 之后对等方发送了更多数据,则其行为取决于平台。
    - user207421
    @EJP 我自己尝试了一下,抱歉我忘记在哪个平台上进行了这个测试,是CentOS还是Mac。这就是我得到的结果。 - simpx

    0

    Linux: 调用shutdown()会导致监听线程的select()被唤醒并产生错误。调用shutdown(); close();将导致无限等待。

    Winsock: 相反,shutdown()没有效果,而close()可以成功捕获。


    我不理解这个答案。 - Ingo
    谢谢!这真是一个很好的提示,告诉我们如何唤醒在select()中等待的另一个线程。 很可能poll()的工作方式也是一样的。 - undefined

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接