golang的TCPConn.SetWriteDeadline似乎不像预期那样工作。

huangapple go评论123阅读模式
英文:

golang TCPConn.SetWriteDeadline doesn't seem to work as expected

问题

我试图通过检查golang TCPConn.Write返回的错误来检测发送失败,但它是nil。我还尝试使用TCPConn.SetWriteDeadline,但没有成功。

事情是这样发生的:

  1. 服务器启动
  2. 客户端连接
  3. 服务器发送一条消息,客户端接收到
  4. 客户端关闭
  5. 服务器发送一条消息:没有错误
  6. 服务器发送第三条消息:现在才出现错误

问题:为什么只有向不存在的客户端发送第二条消息会导致错误?应该如何正确处理这种情况?

代码如下:

  1. package main
  2. import (
  3. "net"
  4. "os"
  5. "bufio"
  6. "fmt"
  7. "time"
  8. )
  9. func AcceptConnections(listener net.Listener, console <- chan string) {
  10. msg := ""
  11. for {
  12. conn, err := listener.Accept()
  13. if err != nil {
  14. panic(err)
  15. }
  16. fmt.Printf("client connected\n")
  17. for {
  18. if msg == "" {
  19. msg = <- console
  20. fmt.Printf("read from console: %s", msg)
  21. }
  22. err = conn.SetWriteDeadline(time.Now().Add(time.Second))
  23. if err != nil {
  24. fmt.Printf("SetWriteDeadline failed: %v\n", err)
  25. }
  26. _, err = conn.Write([]byte(msg))
  27. if err != nil {
  28. // expecting an error after sending a message
  29. // to a non-existing client endpoint
  30. fmt.Printf("failed sending a message to network: %v\n", err)
  31. break
  32. } else {
  33. fmt.Printf("msg sent: %s", msg)
  34. msg = ""
  35. }
  36. }
  37. }
  38. }
  39. func ReadConsole(network chan <- string) {
  40. console := bufio.NewReader(os.Stdin)
  41. for {
  42. line, err := console.ReadString('\n')
  43. if err != nil {
  44. panic(err)
  45. } else {
  46. network <- line
  47. }
  48. }
  49. }
  50. func main() {
  51. listener, err := net.Listen("tcp", "localhost:6666")
  52. if err != nil {
  53. panic(err)
  54. }
  55. println("listening on " + listener.Addr().String())
  56. consoleToNetwork := make(chan string)
  57. go AcceptConnections(listener, consoleToNetwork)
  58. ReadConsole(consoleToNetwork)
  59. }

服务器控制台输出如下:

  1. listening on 127.0.0.1:6666
  2. client connected
  3. hi there!
  4. read from console: hi there!
  5. msg sent: hi there!
  6. this one should fail
  7. read from console: this one should fail
  8. msg sent: this one should fail
  9. this one actually fails
  10. read from console: this one actually fails
  11. failed sending a message to network: write tcp 127.0.0.1:51194: broken pipe

客户端代码如下:

  1. package main
  2. import (
  3. "net"
  4. "os"
  5. "io"
  6. // "bufio"
  7. // "fmt"
  8. )
  9. func cp(dst io.Writer, src io.Reader, errc chan<- error) {
  10. // -reads from src and writes to dst
  11. // -blocks until EOF
  12. // -EOF is not an error
  13. _, err := io.Copy(dst, src)
  14. // push err to the channel when io.Copy returns
  15. errc <- err
  16. }
  17. func StartCommunication(conn net.Conn) {
  18. //create a channel for errors
  19. errc := make(chan error)
  20. //read connection and print to console
  21. go cp(os.Stdout, conn, errc)
  22. //read user input and write to connection
  23. go cp(conn, os.Stdin, errc)
  24. //wait until nil or an error arrives
  25. err := <- errc
  26. if err != nil {
  27. println("cp error: ", err.Error())
  28. }
  29. }
  30. func main() {
  31. servAddr := "localhost:6666"
  32. tcpAddr, err := net.ResolveTCPAddr("tcp", servAddr)
  33. if err != nil {
  34. println("ResolveTCPAddr failed:", err.Error())
  35. os.Exit(1)
  36. }
  37. conn, err := net.DialTCP("tcp", nil, tcpAddr)
  38. if err != nil {
  39. println("net.DialTCP failed:", err.Error())
  40. os.Exit(1)
  41. }
  42. defer conn.Close()
  43. StartCommunication(conn)
  44. }

编辑:根据JimB的建议,我提供了一个可行的示例。消息不再丢失,并在新连接中重新发送。但是,我不太确定在不同的go协程之间使用共享变量(connWrap.IsFaulted)有多安全。

  1. package main
  2. import (
  3. "net"
  4. "os"
  5. "bufio"
  6. "fmt"
  7. )
  8. type Connection struct {
  9. IsFaulted bool
  10. Conn net.Conn
  11. }
  12. func StartWritingToNetwork(connWrap * Connection, errChannel chan <- error, msgStack chan string) {
  13. for {
  14. msg := <- msgStack
  15. if connWrap.IsFaulted {
  16. //put it back for another connection
  17. msgStack <- msg
  18. return
  19. }
  20. _, err := connWrap.Conn.Write([]byte(msg))
  21. if err != nil {
  22. fmt.Printf("failed sending a message to network: %v\n", err)
  23. connWrap.IsFaulted = true
  24. msgStack <- msg
  25. errChannel <- err
  26. return
  27. } else {
  28. fmt.Printf("msg sent: %s", msg)
  29. }
  30. }
  31. }
  32. func StartReadingFromNetwork(connWrap * Connection, errChannel chan <- error){
  33. network := bufio.NewReader(connWrap.Conn)
  34. for (!connWrap.IsFaulted) {
  35. line, err := network.ReadString('\n')
  36. if err != nil {
  37. fmt.Printf("failed reading from network: %v\n", err)
  38. connWrap.IsFaulted = true
  39. errChannel <- err
  40. } else {
  41. fmt.Printf("%s", line)
  42. }
  43. }
  44. }
  45. func AcceptConnections(listener net.Listener, console chan string) {
  46. errChannel := make(chan error)
  47. for {
  48. conn, err := listener.Accept()
  49. if err != nil {
  50. panic(err)
  51. }
  52. fmt.Printf("client connected\n")
  53. connWrap := Connection{false, conn}
  54. go StartReadingFromNetwork(&connWrap, errChannel)
  55. go StartWritingToNetwork(&connWrap, errChannel, console)
  56. //block until an error occurs
  57. <- errChannel
  58. }
  59. }
  60. func ReadConsole(network chan <- string) {
  61. console := bufio.NewReader(os.Stdin)
  62. for {
  63. line, err := console.ReadString('\n')
  64. if err != nil {
  65. panic(err)
  66. } else {
  67. network <- line
  68. }
  69. }
  70. }
  71. func main() {
  72. listener, err := net.Listen("tcp", "localhost:6666")
  73. if err != nil {
  74. panic(err)
  75. }
  76. println("listening on " + listener.Addr().String())
  77. consoleToNetwork := make(chan string)
  78. go AcceptConnections(listener, consoleToNetwork)
  79. ReadConsole(consoleToNetwork)
  80. }
英文:

I'm trying to detect sending failures by inspecting the error returned by golang TCPConn.Write, but it's nil. I also tried using TCPConn.SetWriteDeadline without success.

That's how things happen:

  1. the server starts
  2. a client connects
  3. the server sends a message and the client receives it
  4. the client shuts down
  5. the server sends one more message: no error
  6. the server sends the third message: only now the error appears

Question: why only the second message to a non-existing client results in an error? How should the case be handled properly?

The code follows:

  1. package main
  2. import (
  3. &quot;net&quot;
  4. &quot;os&quot;
  5. &quot;bufio&quot;
  6. &quot;fmt&quot;
  7. &quot;time&quot;
  8. )
  9. func AcceptConnections(listener net.Listener, console &lt;- chan string) {
  10. msg := &quot;&quot;
  11. for {
  12. conn, err := listener.Accept()
  13. if err != nil {
  14. panic(err)
  15. }
  16. fmt.Printf(&quot;client connected\n&quot;)
  17. for {
  18. if msg == &quot;&quot; {
  19. msg = &lt;- console
  20. fmt.Printf(&quot;read from console: %s&quot;, msg)
  21. }
  22. err = conn.SetWriteDeadline(time.Now().Add(time.Second))
  23. if err != nil {
  24. fmt.Printf(&quot;SetWriteDeadline failed: %v\n&quot;, err)
  25. }
  26. _, err = conn.Write([]byte(msg))
  27. if err != nil {
  28. // expecting an error after sending a message
  29. // to a non-existing client endpoint
  30. fmt.Printf(&quot;failed sending a message to network: %v\n&quot;, err)
  31. break
  32. } else {
  33. fmt.Printf(&quot;msg sent: %s&quot;, msg)
  34. msg = &quot;&quot;
  35. }
  36. }
  37. }
  38. }
  39. func ReadConsole(network chan &lt;- string) {
  40. console := bufio.NewReader(os.Stdin)
  41. for {
  42. line, err := console.ReadString(&#39;\n&#39;)
  43. if err != nil {
  44. panic(err)
  45. } else {
  46. network &lt;- line
  47. }
  48. }
  49. }
  50. func main() {
  51. listener, err := net.Listen(&quot;tcp&quot;, &quot;localhost:6666&quot;)
  52. if err != nil {
  53. panic(err)
  54. }
  55. println(&quot;listening on &quot; + listener.Addr().String())
  56. consoleToNetwork := make(chan string)
  57. go AcceptConnections(listener, consoleToNetwork)
  58. ReadConsole(consoleToNetwork)
  59. }

The server console looks like this:

  1. listening on 127.0.0.1:6666
  2. client connected
  3. hi there!
  4. read from console: hi there!
  5. msg sent: hi there!
  6. this one should fail
  7. read from console: this one should fail
  8. msg sent: this one should fail
  9. this one actually fails
  10. read from console: this one actually fails
  11. failed sending a message to network: write tcp 127.0.0.1:51194: broken pipe

The client looks like this:

  1. package main
  2. import (
  3. &quot;net&quot;
  4. &quot;os&quot;
  5. &quot;io&quot;
  6. //&quot;bufio&quot;
  7. //&quot;fmt&quot;
  8. )
  9. func cp(dst io.Writer, src io.Reader, errc chan&lt;- error) {
  10. // -reads from src and writes to dst
  11. // -blocks until EOF
  12. // -EOF is not an error
  13. _, err := io.Copy(dst, src)
  14. // push err to the channel when io.Copy returns
  15. errc &lt;- err
  16. }
  17. func StartCommunication(conn net.Conn) {
  18. //create a channel for errors
  19. errc := make(chan error)
  20. //read connection and print to console
  21. go cp(os.Stdout, conn, errc)
  22. //read user input and write to connection
  23. go cp(conn, os.Stdin, errc)
  24. //wait until nil or an error arrives
  25. err := &lt;- errc
  26. if err != nil {
  27. println(&quot;cp error: &quot;, err.Error())
  28. }
  29. }
  30. func main() {
  31. servAddr := &quot;localhost:6666&quot;
  32. tcpAddr, err := net.ResolveTCPAddr(&quot;tcp&quot;, servAddr)
  33. if err != nil {
  34. println(&quot;ResolveTCPAddr failed:&quot;, err.Error())
  35. os.Exit(1)
  36. }
  37. conn, err := net.DialTCP(&quot;tcp&quot;, nil, tcpAddr)
  38. if err != nil {
  39. println(&quot;net.DialTCP failed:&quot;, err.Error())
  40. os.Exit(1)
  41. }
  42. defer conn.Close()
  43. StartCommunication(conn)
  44. }

EDIT: Following JimB's suggestion I came up with a working example. Messages don't get lost any more and are re-sent in a new connection. I'm not quite sure though how safe is it to use a shared variable (connWrap.IsFaulted) between different go routines.

  1. package main
  2. import (
  3. &quot;net&quot;
  4. &quot;os&quot;
  5. &quot;bufio&quot;
  6. &quot;fmt&quot;
  7. )
  8. type Connection struct {
  9. IsFaulted bool
  10. Conn net.Conn
  11. }
  12. func StartWritingToNetwork(connWrap * Connection, errChannel chan &lt;- error, msgStack chan string) {
  13. for {
  14. msg := &lt;- msgStack
  15. if connWrap.IsFaulted {
  16. //put it back for another connection
  17. msgStack &lt;- msg
  18. return
  19. }
  20. _, err := connWrap.Conn.Write([]byte(msg))
  21. if err != nil {
  22. fmt.Printf(&quot;failed sending a message to network: %v\n&quot;, err)
  23. connWrap.IsFaulted = true
  24. msgStack &lt;- msg
  25. errChannel &lt;- err
  26. return
  27. } else {
  28. fmt.Printf(&quot;msg sent: %s&quot;, msg)
  29. }
  30. }
  31. }
  32. func StartReadingFromNetwork(connWrap * Connection, errChannel chan &lt;- error){
  33. network := bufio.NewReader(connWrap.Conn)
  34. for (!connWrap.IsFaulted) {
  35. line, err := network.ReadString(&#39;\n&#39;)
  36. if err != nil {
  37. fmt.Printf(&quot;failed reading from network: %v\n&quot;, err)
  38. connWrap.IsFaulted = true
  39. errChannel &lt;- err
  40. } else {
  41. fmt.Printf(&quot;%s&quot;, line)
  42. }
  43. }
  44. }
  45. func AcceptConnections(listener net.Listener, console chan string) {
  46. errChannel := make(chan error)
  47. for {
  48. conn, err := listener.Accept()
  49. if err != nil {
  50. panic(err)
  51. }
  52. fmt.Printf(&quot;client connected\n&quot;)
  53. connWrap := Connection{false, conn}
  54. go StartReadingFromNetwork(&amp;connWrap, errChannel)
  55. go StartWritingToNetwork(&amp;connWrap, errChannel, console)
  56. //block until an error occurs
  57. &lt;- errChannel
  58. }
  59. }
  60. func ReadConsole(network chan &lt;- string) {
  61. console := bufio.NewReader(os.Stdin)
  62. for {
  63. line, err := console.ReadString(&#39;\n&#39;)
  64. if err != nil {
  65. panic(err)
  66. } else {
  67. network &lt;- line
  68. }
  69. }
  70. }
  71. func main() {
  72. listener, err := net.Listen(&quot;tcp&quot;, &quot;localhost:6666&quot;)
  73. if err != nil {
  74. panic(err)
  75. }
  76. println(&quot;listening on &quot; + listener.Addr().String())
  77. consoleToNetwork := make(chan string)
  78. go AcceptConnections(listener, consoleToNetwork)
  79. ReadConsole(consoleToNetwork)
  80. }

答案1

得分: 14

这不是Go特定的问题,而是底层TCP套接字的一个问题。

TCP终止步骤的一个良好的图表在这个页面的底部:
http://www.tcpipguide.com/free/t_TCPConnectionTermination-2.htm

简单来说,当客户端关闭它的套接字时,它发送一个FIN,并从服务器接收一个ACK。然后它等待服务器做同样的事情。但是你发送的不是FIN,而是更多的数据,这些数据被丢弃,客户端套接字现在假设来自你的任何更多数据都是无效的,所以下次你发送时会收到一个RST,这就是你看到的错误。

回到你的程序,你需要以某种方式处理这个问题。通常情况下,你可以认为负责发起发送的人也负责发起终止,因此你的服务器应该假设它可以继续发送直到关闭连接或遇到错误。如果你需要更可靠地检测客户端关闭,你需要在协议中有某种形式的客户端响应。这样,可以在套接字上调用recv并返回0,这会提醒你连接已关闭。

在Go中,这将从连接的Read方法(或在你的情况下从Copy中)返回一个EOF错误。SetWriteDeadline不起作用,因为一个小写操作会被静默丢弃,或者客户端最终会以RST响应,给你一个错误。

英文:

This isn't Go specific, and is a artifact of the underlying TCP socket showing through.

A decent diagram of the TCP termination steps is at the bottom of this page:
http://www.tcpipguide.com/free/t_TCPConnectionTermination-2.htm

The simple version is that when the client closes its socket, it sends a FIN, and receives an ACK from the server. It then waits for the server to do the same. Instead of sending a FIN though, you're sending more data, which is discarded, and the client socket now assumes that any more data coming from you is invalid, so the next time you send you get an RST, which is what bubbles up into the error you see.

Going back to your program, you need to handle this somehow. Generally you can think of whomever is in charge of initiating a send, is also in charge of initiating termination, hence your server should assume that it can continue to send until it closes the connection, or encounters an error. If you need to more reliably detect the client closing, you need to have some sort of client response in the protocol. That way recv can be called on the socket and return 0, which alerts you to the closed connection.

In go, this will return an EOF error from the connection's Read method (or from within the Copy in your case). SetWriteDeadline doesn't work because a small write will go though and get dropped silently, or the client will eventually respond with an RST, giving you an error.

huangapple
  • 本文由 发表于 2013年2月25日 20:44:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/15067286.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定