TCP 连接可以看作是两个端点之间的两条数据管道。一条数据流水线用于从 A 向 B 发送数据,一条数据流水线用于从 B 向 A 发送数据。这两条流水线属于一个连接,但它们不会相互影响。在一个管道上发送数据对在另一管道上发送的数据没有影响。如果一个管道上的数据是对先前在另一个管道上发送的数据的回复数据,这只有您的应用程序会知道,TCP 对此一无所知。TCP 的任务是确保数据可靠地从管道的一端传输到另一端,并且尽可能快,这就是 TCP 所关心的。
FIN
一旦一方完成发送数据,它就会通过向另一方发送一个设置了标志的数据包来告诉另一方它已完成。发送FIN
标志的意思是“我已经发送了所有我想发送给你的数据,所以我的发送管道现在关闭了”。您可以通过调用在代码中有意触发它shutdown(socketfd, SHUT_WR)
。如果对方随后调用recv()
套接字,它不会收到错误,但接收会说它读取零字节,这意味着“流结束”。流结束不是错误,它只意味着不再有数据到达那里,无论您多久调用recv()
该套接字。
当然,这并不影响其他管道,所以当A -> B
关闭时,B -> A
仍然可以使用。即使您关闭了发送管道,您仍然可以从该套接字接收。但是,在某些时候,B 也将完成发送数据并传输 a FIN
。一旦两个管道都关闭了,整个连接就关闭了,这将是一个正常的关闭,因为双方都能够发送他们想要发送的所有数据并且没有数据应该丢失,因为只要有传输中未经确认的数据,对方不会说它已经完成,而是等待该数据首先可靠地传输。
或者,有一个RST
标志会立即关闭整个连接,无论对方是否已完成发送,也无论是否有未确认的数据在传输中,因此RST
很有可能导致数据丢失。由于这是一种可能需要特殊处理的特殊情况,因此程序员知道是否是这种情况会很有用,这就是存在两个错误的原因:
EPIPE
- 您无法通过该管道发送,因为该管道不再有效。但是,您在损坏之前发送的所有数据仍然可靠地传递,您只是无法发送任何新数据。
ECONNRESET
- 您的管道坏了,可能是您之前尝试发送的数据在传输过程中丢失了。如果这是一个问题,你最好以某种方式处理它。
但是这两个错误并没有一对一地映射到FIN
andRST
标志。如果您在系统认为没有数据丢失风险的情况下收到RST
通知,则没有理由无缘无故地让您绕弯子。RST
因此,如果您之前发送的所有数据都被确认正确接收,然后当您尝试发送新数据时连接被关闭,则不会丢失任何数据。这包括您尝试发送的当前数据,因为该数据没有丢失,从未在途中发送,这是不同的,因为您仍然拥有它,而您之前发送的数据可能不再存在。如果您的汽车在公路旅行中抛锚了,那么这与您仍然在家的情况完全不同,因为您的汽车发动机甚至无法启动。所以最终是你的系统决定一个RST
触发 aECONNRESET
或 a EPIPE
。
好的,但是为什么对方首先要给你发一个RST
呢?为什么不总是以 结束FIN
?嗯,有几个原因,但最突出的两个是:
一方只能向另一方发出它已完成发送的信号,但发出整个连接已完成的唯一方法是发送一个RST
. 所以如果一方想要关闭一个连接并且它想要优雅地关闭它,它会首先发送一个FIN
信号,表示它不会再发送新数据,然后给另一方一些时间来停止发送数据,允许在飞行中要传递的数据并最终发送 a FIN
。但是,如果对方不想停下来继续发送又发送怎么办?这种行为是合法的,因为这FIN
并不意味着连接需要关闭,它只意味着一侧已经完成。结果是FIN
紧随其后的是RST
最终关闭该连接。这可能会导致飞行中的数据丢失,也可能不会,只有遗嘱的接收者RST
才能肯定地知道,好像数据丢失了,它一定是站在他这边的,因为发送者RST
肯定不会再发送任何数据了之后FIN
。对于recv()
呼叫,这RST
没有任何效果,因为FIN
之前有一个信号“流结束”,因此recv()
将报告已读取零字节。
一侧应关闭连接,但仍有未发送的数据。理想情况下,它会等到所有未发送的数据都发送完毕,然后发送一个FIN
,但是,它允许等待的时间是有限的,并且在该时间过去之后,仍然有未发送的数据。在那种情况下,它不能发送 a FIN
,因为那FIN
将是一个谎言。它会告诉对方“嘿,我发送了我想发送的所有数据”,但这不是真的。本来应该发送的数据,但由于要求立即关闭,因此必须丢弃该数据,结果,本方将直接发送RST
. 这是否RST
触发ECONNRESET
呼叫send()
再次取决于事实,如果RST
是否有未发送的数据。ECONNRESET
但是,它肯定会在下一次调用时触发错误recv()
,告诉程序“对方实际上想向您发送更多数据,但它不能,因此其中一些数据丢失了”,因为这可能又是一个以某种方式处理的情况,因为您收到的数据肯定是不完整的,这是您应该注意的事情。
如果你想强制一个套接字总是直接关闭RST
而不是FIN
/FIN
或FIN
/ RST
,你可以将 Linger 时间设置为零。
struct linger l = { .l_onoff = 1, .l_linger = 0 };
setsockopt(socketfd, SOL_SOCKET, SO_LINGER, &l, sizeof(l));
现在套接字必须立即关闭并且没有任何延迟,无论多么少,立即关闭 TCP 套接字的唯一方法是发送一个RST
. 有些人认为“为什么要启用它并将时间设置为零?为什么不只是禁用它呢? ”但禁用具有不同的含义。
逗留时间是close()
调用可能会阻塞以执行挂起的发送操作以优雅地关闭套接字的时间。如果启用 ( .l_onoff != 0
),调用close()
可能会阻塞长达.l_linger
几秒钟。如果您将时间设置为零,它可能根本不会阻塞,因此会立即终止 ( RST
)。但是,如果您禁用它,则close()
也永远不会阻塞,但系统可能仍会在关闭时徘徊,但这种徘徊发生在后台,因此您的进程不会再注意到它,因此也无法知道套接字何时真正关闭,因为socketfd
即使内核中的底层套接字仍然存在,它也会立即失效。