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

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

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

问题

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

事情是这样发生的:

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

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

代码如下:

package main
 
import (
    "net"
    "os"
    "bufio"
    "fmt"
    "time"
)

func AcceptConnections(listener net.Listener, console <- chan string) {

    msg := ""

    for {

        conn, err := listener.Accept()

        if err != nil {
            panic(err)
        }

        fmt.Printf("client connected\n")

        for {

            if msg == "" {
                msg = <- console
                fmt.Printf("read from console: %s", msg)
            }

            err = conn.SetWriteDeadline(time.Now().Add(time.Second))

            if err != nil {
                fmt.Printf("SetWriteDeadline failed: %v\n", err)
            }

            _, err = conn.Write([]byte(msg))

            if err != nil {
                // expecting an error after sending a message
                // to a non-existing client endpoint
                fmt.Printf("failed sending a message to network: %v\n", err)
                break
            } else {
                fmt.Printf("msg sent: %s", msg)
                msg = ""
            }
        }
    }
}

func ReadConsole(network chan <- string) {
    
    console := bufio.NewReader(os.Stdin)

    for {

        line, err := console.ReadString('\n')

        if err != nil {

            panic(err)

        } else {

            network <- line
        }
    }
}

func main() {

    listener, err := net.Listen("tcp", "localhost:6666")

    if err != nil {
        panic(err)
    }

    println("listening on " + listener.Addr().String())

    consoleToNetwork := make(chan string)

    go AcceptConnections(listener, consoleToNetwork)

    ReadConsole(consoleToNetwork)
}

服务器控制台输出如下:

listening on 127.0.0.1:6666
client connected
hi there!
read from console: hi there!
msg sent: hi there!
this one should fail
read from console: this one should fail
msg sent: this one should fail
this one actually fails
read from console: this one actually fails
failed sending a message to network: write tcp 127.0.0.1:51194: broken pipe

客户端代码如下:

package main

import (
    "net"
    "os"
    "io"
    // "bufio"
    // "fmt"
)

func cp(dst io.Writer, src io.Reader, errc chan<- error) {

    // -reads from src and writes to dst
    // -blocks until EOF
    // -EOF is not an error
    _, err :=  io.Copy(dst, src)
    
    // push err to the channel when io.Copy returns
    errc <- err
}

func StartCommunication(conn net.Conn) {

    //create a channel for errors
    errc := make(chan error)

    //read connection and print to console
    go cp(os.Stdout, conn, errc)

    //read user input and write to connection
    go cp(conn, os.Stdin, errc)

    //wait until nil or an error arrives
    err := <- errc

    if err != nil {
        println("cp error: ", err.Error())
    }
}

func main() {

    servAddr := "localhost:6666"

    tcpAddr, err := net.ResolveTCPAddr("tcp", servAddr)

    if err != nil {
        println("ResolveTCPAddr failed:", err.Error())
        os.Exit(1)
    }
 
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    
    if err != nil {
        println("net.DialTCP failed:", err.Error())
        os.Exit(1)
    }

    defer conn.Close()

    StartCommunication(conn)

}

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

package main
 
import (
    "net"
    "os"
    "bufio"
    "fmt"
)

type Connection struct {
    IsFaulted bool
    Conn net.Conn
}

func StartWritingToNetwork(connWrap * Connection, errChannel chan <- error, msgStack chan string) {

    for {

        msg := <- msgStack

        if connWrap.IsFaulted {

            //put it back for another connection
            msgStack <- msg

            return
        }

        _, err := connWrap.Conn.Write([]byte(msg))

        if err != nil {
            
            fmt.Printf("failed sending a message to network: %v\n", err)

            connWrap.IsFaulted = true

            msgStack <- msg

            errChannel <- err

            return

        } else {

            fmt.Printf("msg sent: %s", msg)
        }
    }
}

func StartReadingFromNetwork(connWrap * Connection, errChannel chan <- error){

    network := bufio.NewReader(connWrap.Conn)

    for (!connWrap.IsFaulted) {

        line, err := network.ReadString('\n')

        if err != nil {
            
            fmt.Printf("failed reading from network: %v\n", err)

            connWrap.IsFaulted = true

            errChannel <- err

        } else {

            fmt.Printf("%s", line)
        }
    }
}

func AcceptConnections(listener net.Listener, console chan string) {

    errChannel := make(chan error)

    for {

        conn, err := listener.Accept()

        if err != nil {
            panic(err)
        }

        fmt.Printf("client connected\n")

        connWrap := Connection{false, conn}

        go StartReadingFromNetwork(&connWrap, errChannel)

        go StartWritingToNetwork(&connWrap, errChannel, console)

        //block until an error occurs
        <- errChannel
    }
}

func ReadConsole(network chan <- string) {
    
    console := bufio.NewReader(os.Stdin)

    for {

        line, err := console.ReadString('\n')

        if err != nil {

            panic(err)

        } else {

            network <- line
        }
    }
}

func main() {

    listener, err := net.Listen("tcp", "localhost:6666")

    if err != nil {
        panic(err)
    }

    println("listening on " + listener.Addr().String())

    consoleToNetwork := make(chan string)

    go AcceptConnections(listener, consoleToNetwork)

    ReadConsole(consoleToNetwork)
}
英文:

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:

package main
import (
&quot;net&quot;
&quot;os&quot;
&quot;bufio&quot;
&quot;fmt&quot;
&quot;time&quot;
)
func AcceptConnections(listener net.Listener, console &lt;- chan string) {
msg := &quot;&quot;
for {
conn, err := listener.Accept()
if err != nil {
panic(err)
}
fmt.Printf(&quot;client connected\n&quot;)
for {
if msg == &quot;&quot; {
msg = &lt;- console
fmt.Printf(&quot;read from console: %s&quot;, msg)
}
err = conn.SetWriteDeadline(time.Now().Add(time.Second))
if err != nil {
fmt.Printf(&quot;SetWriteDeadline failed: %v\n&quot;, err)
}
_, err = conn.Write([]byte(msg))
if err != nil {
// expecting an error after sending a message
// to a non-existing client endpoint
fmt.Printf(&quot;failed sending a message to network: %v\n&quot;, err)
break
} else {
fmt.Printf(&quot;msg sent: %s&quot;, msg)
msg = &quot;&quot;
}
}
}
}
func ReadConsole(network chan &lt;- string) {
console := bufio.NewReader(os.Stdin)
for {
line, err := console.ReadString(&#39;\n&#39;)
if err != nil {
panic(err)
} else {
network &lt;- line
}
}
}
func main() {
listener, err := net.Listen(&quot;tcp&quot;, &quot;localhost:6666&quot;)
if err != nil {
panic(err)
}
println(&quot;listening on &quot; + listener.Addr().String())
consoleToNetwork := make(chan string)
go AcceptConnections(listener, consoleToNetwork)
ReadConsole(consoleToNetwork)
}

The server console looks like this:

listening on 127.0.0.1:6666
client connected
hi there!
read from console: hi there!
msg sent: hi there!
this one should fail
read from console: this one should fail
msg sent: this one should fail
this one actually fails
read from console: this one actually fails
failed sending a message to network: write tcp 127.0.0.1:51194: broken pipe

The client looks like this:

package main
import (
&quot;net&quot;
&quot;os&quot;
&quot;io&quot;
//&quot;bufio&quot;
//&quot;fmt&quot;
)
func cp(dst io.Writer, src io.Reader, errc chan&lt;- error) {
// -reads from src and writes to dst
// -blocks until EOF
// -EOF is not an error
_, err :=  io.Copy(dst, src)
// push err to the channel when io.Copy returns
errc &lt;- err
}
func StartCommunication(conn net.Conn) {
//create a channel for errors
errc := make(chan error)
//read connection and print to console
go cp(os.Stdout, conn, errc)
//read user input and write to connection
go cp(conn, os.Stdin, errc)
//wait until nil or an error arrives
err := &lt;- errc
if err != nil {
println(&quot;cp error: &quot;, err.Error())
}
}
func main() {
servAddr := &quot;localhost:6666&quot;
tcpAddr, err := net.ResolveTCPAddr(&quot;tcp&quot;, servAddr)
if err != nil {
println(&quot;ResolveTCPAddr failed:&quot;, err.Error())
os.Exit(1)
}
conn, err := net.DialTCP(&quot;tcp&quot;, nil, tcpAddr)
if err != nil {
println(&quot;net.DialTCP failed:&quot;, err.Error())
os.Exit(1)
}
defer conn.Close()
StartCommunication(conn)
}

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.

package main
import (
&quot;net&quot;
&quot;os&quot;
&quot;bufio&quot;
&quot;fmt&quot;
)
type Connection struct {
IsFaulted bool
Conn net.Conn
}
func StartWritingToNetwork(connWrap * Connection, errChannel chan &lt;- error, msgStack chan string) {
for {
msg := &lt;- msgStack
if connWrap.IsFaulted {
//put it back for another connection
msgStack &lt;- msg
return
}
_, err := connWrap.Conn.Write([]byte(msg))
if err != nil {
fmt.Printf(&quot;failed sending a message to network: %v\n&quot;, err)
connWrap.IsFaulted = true
msgStack &lt;- msg
errChannel &lt;- err
return
} else {
fmt.Printf(&quot;msg sent: %s&quot;, msg)
}
}
}
func StartReadingFromNetwork(connWrap * Connection, errChannel chan &lt;- error){
network := bufio.NewReader(connWrap.Conn)
for (!connWrap.IsFaulted) {
line, err := network.ReadString(&#39;\n&#39;)
if err != nil {
fmt.Printf(&quot;failed reading from network: %v\n&quot;, err)
connWrap.IsFaulted = true
errChannel &lt;- err
} else {
fmt.Printf(&quot;%s&quot;, line)
}
}
}
func AcceptConnections(listener net.Listener, console chan string) {
errChannel := make(chan error)
for {
conn, err := listener.Accept()
if err != nil {
panic(err)
}
fmt.Printf(&quot;client connected\n&quot;)
connWrap := Connection{false, conn}
go StartReadingFromNetwork(&amp;connWrap, errChannel)
go StartWritingToNetwork(&amp;connWrap, errChannel, console)
//block until an error occurs
&lt;- errChannel
}
}
func ReadConsole(network chan &lt;- string) {
console := bufio.NewReader(os.Stdin)
for {
line, err := console.ReadString(&#39;\n&#39;)
if err != nil {
panic(err)
} else {
network &lt;- line
}
}
}
func main() {
listener, err := net.Listen(&quot;tcp&quot;, &quot;localhost:6666&quot;)
if err != nil {
panic(err)
}
println(&quot;listening on &quot; + listener.Addr().String())
consoleToNetwork := make(chan string)
go AcceptConnections(listener, consoleToNetwork)
ReadConsole(consoleToNetwork)
}

答案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:

确定