如何在Go中查询TCP连接状态?

3
在TCP连接的客户端,我试图尽可能地重用已建立的连接,以避免每次需要连接时都拨号的开销。从根本上讲,这是连接池,尽管技术上,我的池大小恰好为1。
我遇到的问题是,如果连接闲置时间过长,另一端会断开连接。我尝试使用以下内容来保持连接活动状态:
err = conn.(*net.TCPConn).SetKeepAlive(true)
if err != nil {
    fmt.Println(err)
    return
}
err = conn.(*net.TCPConn).SetKeepAlivePeriod(30*time.Second)
if err != nil {
    fmt.Println(err)
    return
}

但这并没有起到帮助的作用。事实上,它会导致我的连接更快关闭。我相当确定这是因为(在Mac上),这意味着在30秒后开始探测连接健康状况,然后每隔30秒进行8次探测。服务器端不支持keepalive,因此在4分30秒后,客户端会断开连接。
也许我无法永久保持空闲连接处于活动状态,如果有一种方法可以至少检测连接是否已关闭,那就完全没问题了,这样我就可以顺利地用新连接替换它。可惜的是,即使阅读了所有文档并在博客圈中寻找帮助,我也找不到任何一种方法在go中查询TCP连接的状态。
必须有一种方法。有人知道如何实现吗?非常感谢提供帮助的人!
编辑:
理想情况下,我希望学习如何使用纯go处理这个问题--而不使用第三方库来完成。当然,如果有一些库可以做到这一点,我也不介意被指向它们,以便我可以看看它们是如何做到的。

也许我需要继续向连接写入数据,然后捕获并分析错误,以确定是否建议重新拨号和再次写入数据? - undefined
你通过从中读取数据而不是写入数据来检测关闭的TCP连接。这在任何语言中都是一样的,因为这是底层的Berkeley sockets API的工作原理。 - undefined
2个回答

7
socket api无法访问连接状态。你可以从内核中以各种方式查询当前状态(例如在Linux上使用/proc/net/tcp[6]),但这并不能保证进一步的发送将成功。

我有一个小疑问。我的客户端只发送数据。除了确认数据包之外,服务器不会发送任何内容。读取似乎不是确定连接状态的合适方法,因为没有可读内容。

socket API 的定义使得通过读取返回 0 字节来检测关闭连接。这就是它的工作方式。在 Go 中,这被转换为 Read 返回 io.EOF。这通常是检测断开连接的最快方法。

那么我只需要发送并处理出现的错误吗?如果是这样,那就有问题了,因为我观察到当尝试在断开的管道上发送时,通常根本没有收到任何错误——这似乎完全错误

如果仔细观察 TCP 的工作原理,这是预期的行为。如果远程端关闭了连接,则第一次发送将触发服务器发送 RST,完全关闭本地连接。你需要从连接中读取以检测关闭,或者如果再次尝试发送,你将收到错误(假设等待足够长的时间使数据包进行往返传输),例如 Linux 上的 "broken pipe"。

澄清一下...我可以拨号,拔掉以太网电缆,仍然能发送而没有错误。显然,消息无法传递,但我没有收到任何错误

如果连接实际上已经断开,或者服务器完全不响应,则你正在向无处发送数据包。TCP 栈无法区分真正缓慢、数据包丢失、拥塞还是断开的连接。系统需要等待重传超时,并在失败之前尝试多次重传数据包。仅重试的标准配置就可能需要花费 13 到 30 分钟才能触发错误。
在代码中,你可以:
  • 打开 keepalive。这将更快地通知你连接已断开,因为空闲连接始终在被测试。
  • 从 socket 中读取。要么有一个并发的 Read 在进行中,要么使用 select/poll/epoll 先检查是否有内容可读(Go 通常使用第一个)
  • 为所有操作设置超时(Go 中称为 deadline)。
如果你不期望从连接中接收任何数据,在 Go 中检查关闭的连接非常容易;只需调度一个 goroutine 来从连接中读取,直到出现错误。
notify := make(chan error)

go func() {
    buf := make([]byte, 1024)
    for {
        n, err := conn.Read(buf)
        if err != nil {
            notify <- err
            return
        }
        if n > 0 {
            fmt.Println("unexpected data: %s", buf[:n])
        }
    }
}()

我真的很想感谢你的帮助。你为我提供了很多帮助,让我更好地理解了这一切是如何运作的。我真的认为你在引导我朝着正确的方向前进。话虽如此,你给出的代码片段似乎只能检测到对方已经挂断的情况。但我关心的并不是这种情况,而是网络问题干扰连接的情况。例如,拔掉网络电缆并不会导致上述代码检测到连接已不可行。你有什么想法吗? - undefined
@KentRancourt:我不确定还能添加什么;这就是TCP的工作原理。如果出现网络问题,TCP被设计为接受你的数据,并不断尝试将其传送到目的地。如果你想向服务器发送数据,并确保它已被处理,你必须有一个应用层的确认。如果有了确认,你可以指定一个等待时间限制,在宣布连接中断之前等待。除非改变你的协议,否则你只能尽力发送数据包并希望它们到达。 - undefined
再次感谢。我接受了你的答案。你真是太有帮助了。顺便说一下,我无法控制连接的远程端,所以发送应用层确认是不可能的。最后一个问题:如果一切都按照你描述的方式运行(我毫不怀疑),并且在多次重试和未收到确认的情况下,无法检测到连接中断的时间长达几分钟,这是否与可靠传输的概念相矛盾?有没有办法更早地知道某个数据包无法传递? - undefined
@KentRancourt:“保证传输”位于应用逻辑的较低层,并且甚至不意味着有效载荷到达远程应用程序,只是TCP堆栈确认了它。就像我之前说的那样,你无法区分延迟的数据包或丢失的数据包(由于错误或拥塞),所以TCP为了“保证传输”只能继续重试并等待一段时间。如果你发送足够多的数据以填满窗口大小,发送将被阻塞,这就是设置写入截止日期有助于断开连接的地方。 - undefined

2
  • 按照设计,'TCP连接状态'并不存在。只有当你发送数据时才会发生某些事情。在任何层次上,从硅到应用程序,都没有TCP API会告诉你TCP连接的当前状态。你必须尝试使用它。

  • 如果你正在发送keepalive探测包,服务器没有任何选择,只能适当地做出响应。服务器甚至不知道它们是keepalive探测包,它们只是重复的ACK。支持keepalive仅意味着支持发送keepalive探测包。


1
@KentRancourt:你可以从内核获取那些信息。在Linux上,netstat解析/proc/net/tcp[6]。但是在你的程序中这并没有什么用,因为状态可能会在你尝试使用套接字时发生变化。如果你想查看是否有数据或套接字是否已关闭,你需要使用recv函数;如果你想查看网络是否有响应,你需要使用send函数;如果你不希望这些操作占用不确定的时间,你可以使用超时机制。 - undefined
澄清一下...我可以拨号、拔掉以太网线,仍然能够发送而没有错误。显然,消息无法传递,但我却没有收到任何错误提示。 - undefined
1
@KentRancourt 请记住,连接状态是短暂的。您可以查询连接状态,在纳秒后当您想在连接上发送消息时,连接可能已经断开。有很多情况下连接会断开,但是您的应用程序(或操作系统的TCP堆栈)直到您发送消息并等待TCP因为缺少来自另一端的ACK而超时才会知道这个事实。在断开的连接上发送消息并且在稍后没有收到错误(直到稍后)是正常的。错误会在稍后显现出来。 - undefined
1
@KentRancourt 发送TCP数据不是同步完成的。你将数据缓冲区交给TCP堆栈处理。返回的计数告诉你有多少字节被复制到该缓冲区中。然后TCP堆栈会异步地尝试传递数据。例如,如果你拔掉网络电缆,显然无法传递数据。但是TCP将尝试一段时间,直到其定时器过期。如果你重新插入电缆,TCP可能能够成功传递数据。这意味着即使另一端刚被摧毁,send()也可能会成功,因为TCP会努力一段时间来传递你交给TCP的数据。 - undefined
1
@KentRancourt ESTABLISHED, CLOSE_WAIT等是端口状态,而不是连接状态,它们是通过内核或SNMP进行查找的。例如,CLOSE_WAIT中的本地端口在另一端将处于FIN_WAIT_1状态 => 不是连接状态。重新发送,一旦您有足够的待发送数据未被确认,由于电缆断开而导致TCP尝试发送的时间已经超时,那么您将收到连接重置。 - undefined
显示剩余4条评论

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