【问题标题】:TCP accept and Go concurrency modelTCP接受和Go并发模型
【发布时间】:2015-07-09 00:29:03
【问题描述】:

查看net.TCPListener。考虑到 Go 并发范式,人们会期望将此系统功能实现为通道,以便您从 Listen() 函数中获得 chan *net.Conn,或类似的东西。

但似乎 Accept() 是一种方式,它只是阻塞,就像系统接受一样。除了它是残废的,因为:

  • 没有合适的 select() 可以用于它,因为 go 更喜欢频道
  • 无法为服务器套接字设置阻塞选项。

所以我正在做类似的事情:

    acceptChannel = make(chan *Connection)
    go func() {
      for {
       rw, err := listener.Accept()
       if err != nil { ... handle error ... close(acceptChannel) ... return }
       s.acceptChannel <-&Connection{tcpConn: rw, .... }
      }
    }()

这样我就可以在一个选择中使用多个服务器套接字,或者将 Accept() 上的等待与其他通道多路复用。我错过了什么吗?我是 Go 新手,所以我可能会忽略一些事情——但 Go 真的没有用自己的并发范式实现自己的阻塞系统功能吗?我真的需要为每个我想听的套接字(可能数百或数千个)单独的 goroutine 吗?这是要使用的正确成语,还是有更好的方法?

【问题讨论】:

  • goroutines 不是线程。然而,它们是处理语言中并发操作的正确方法,例如等待来自多个套接字的接受。
  • 您或许应该观看此视频youtube.com/watch?v=cN_DpYBzKso,并且通常尝试重新考虑频道,因为您不了解频道的目的或如何使用它们来控制节目的流程......频道可以作为一种阻塞机制,除了它们的主要用途是将数据传入和传出 goroutines 之外。将连接传递到通道是没有意义的。更明智的应用是将通道传递给您在 goroutine 中调用的方法,在那里打开连接并将数据发送回通道上的调用者。也许你在那里有一个阻塞选择......
  • @evanmcdonnal - 你能给我看一个你建议的方法的例子吗?我不明白你在说什么。我正在尝试将来自多个套接字的accept() 与其他通道recvs 复用。
  • Go 不是 Node.js。您可以很好地生成 50k goroutines,并且阻塞 I/O 是根据异步 I/O 实现的。
  • 供将来参考,而不是对语言 X 无法按照您认为的方式工作表示怀疑;只需用 X 语言解释 what 您想做什么,并询问 如何,您就会得到更好的回应。

标签: tcp concurrency go


【解决方案1】:

您的代码很好。你甚至可以更进一步替换:

s.acceptChannel <-&Connection{tcpConn: rw, .... }

与:

go handleConnection(&Connection{tcpConn: rw, .... })

正如 cmets 中提到的,例程不是系统线程,它们是由 Go 运行时管理的轻量级线程。当您为每个连接创建例程时,您可以轻松使用更容易实现的阻塞操作。然后,Go 运行时为您选择例程,因此您正在寻找的行为只是隐藏在语言中的其他地方。你看不到它,但它无处不在。

现在,如果您需要更复杂的东西,并且根据我们的对话,实现类似于选择超时的东西,您将完全按照您的建议进行操作:将所有新连接推送到通道并使用计时器多路复用.这似乎是 Go 的方式。

请注意,如果 一个 接收器失败,因为另一个接收器会在写入时出现恐慌,您无法关闭接收通道。

我的(更完整的)示例:

newConns := make(chan net.Conn)

// For every listener spawn the following routine
go func(l net.Listener) {
    for {
        c, err := l.Accept()
        if err != nil {
            // handle error (and then for example indicate acceptor is down)
            newConns <- nil
            return
        }
        newConns <- c
    }
}(listener)

for {
    select {
    case c := <-newConns:
        // new connection or nil if acceptor is down, in which case we should
        // do something (respawn, stop when everyone is down or just explode)
    case <-time.After(time.Minute):
        // timeout branch, no connection for a minute
    }
}

【讨论】:

  • 这样做的目的不是为这里的每个连接生成一个连接。这当然会发生,就像在任何具有任何语言的并发模型的任何接受/处理网络服务器中一样。目标是能够一致地处理所有阻塞操作。例如,我可能希望能够将 accept() 与一个通过通道发出信号的计时器 goroutine 复用 - 如果这些 N 个服务器套接字中的任何一个在 timeMillis 秒内没有发生接受,则执行此代码...跨度>
  • 按原样使用 Accept() 或按照您的建议在接受时立即生成处理程序是不可能的。对于传统的并发,您将在套接字 fds 上使用“正常”类型的 select() 并进行定时等待。但这在 Go 中不可用,因为渠道受到青睐。所以我试图将 accept() 包装在一个频道中。有意义吗?
  • @BadZen 这样做的方法是在 go 例程之后进行选择。您使用计时器并在频道到期时发送。在选择中,您在通道上接收的情况下调用您的方法。因此,无论在 Duration d 之后发生什么,您都将在通道“doCleanUp”上发送,这将调用您的方法“doCleanUp”,例如,它可能会杀死您所有的 go 例程并将一些状态写入磁盘然后退出。如果我今天有时间,我会编写一些人为的示例,但基本上你应该在 async 方法中并在调用它之后进行选择。在这两种情况下,您都需要一个您收听/发送的退出/中止频道
  • @BadZen,我添加了一个更完整的示例,它可能与您已有的类似,但它会使我的回答更清晰。
  • 我的示例更多的是多个侦听器,每个侦听器都有一个接受器例程,因此一个接受失败不会指示另一个失败。您是否建议单个侦听器使用多个接受器?我认为这在这个用例中不是很有用,一个就足够了。无论如何,关于恢复,当您知道它可能由于竞争条件而发生并且您将为非常特定的用例(例如,一个且只有一个侦听器)实施它时,我认为这不是一个好的解决方案。如果你能以正确的方式轻松做到这一点,那就没有理由了。
猜你喜欢
  • 2019-10-14
  • 2010-12-30
  • 1970-01-01
  • 2015-04-14
  • 2020-08-21
  • 2015-05-02
  • 2014-03-11
  • 2012-02-10
  • 1970-01-01
相关资源
最近更新 更多