在已关闭的 net.Conn 上写入但返回空错误

5

言之无物,下面是简单的代码:

package main

import (
    "fmt"
    "time"
    "net"
)

func main() {
    addr := "127.0.0.1:8999"

    // Server
    go func() {
        tcpaddr, err := net.ResolveTCPAddr("tcp4", addr)
        if err != nil {
            panic(err)
        }
        listen, err := net.ListenTCP("tcp", tcpaddr)
        if err != nil {
            panic(err)
        }
        for  {
            if conn, err := listen.Accept(); err != nil {
                panic(err)
            } else if conn != nil {
                go func(conn net.Conn) {
                    buffer := make([]byte, 1024)
                    n, err := conn.Read(buffer)
                    if err != nil {
                        fmt.Println(err)
                    } else {
                        fmt.Println(">", string(buffer[0 : n]))
                    }
                    conn.Close()
                }(conn)
            }
        }
    }()

    time.Sleep(time.Second)

    // Client
    if conn, err := net.Dial("tcp", addr); err == nil {
        for i := 0; i < 2; i++ {
            _, err := conn.Write([]byte("hello"))
            if err != nil {
                fmt.Println(err)
                conn.Close()
                break
            } else {
                fmt.Println("ok")
            }
            // sleep 10 seconds and re-send
            time.Sleep(10*time.Second)
        }
    } else {
        panic(err)
    }

}

输出:

> hello
ok
ok

客户端向服务器发送了两次写请求。第一次读取后,服务器立即关闭连接,但客户端等待10秒钟,然后使用相同的已关闭连接对象(conn)重新向服务器发送写请求。
为什么第二次写入可以成功(返回错误为nil)?
有人能帮忙解答吗?
PS:
为了检查系统缓冲功能是否影响第二次写入的结果,我对客户端进行了编辑,但它仍然成功了。
// Client
if conn, err := net.Dial("tcp", addr); err == nil {
    _, err := conn.Write([]byte("hello"))
    if err != nil {
        fmt.Println(err)
        conn.Close()
        return
    } else {
        fmt.Println("ok")
    }
    // sleep 10 seconds and re-send
    time.Sleep(10*time.Second)

    b := make([]byte, 400000)
    for i := range b {
        b[i] = 'x'
    }
    n, err := conn.Write(b)
    if err != nil {
        fmt.Println(err)
        conn.Close()
        return
    } else {
        fmt.Println("ok", n)
    }
    // sleep 10 seconds and re-send
    time.Sleep(10*time.Second)
} else {
    panic(err)
}

这是截图: 附件

1
“连接对象”并非“已关闭”。连接已被对等方关闭,但本地TCP在本地应用程序执行另一个I/O之前不会注意到这一点。此时,连接“对象”尚未“已关闭”。 - user207421
@EJP 是的,它进行了第二次写入(这可能是另一个I/O),即使使用大字节,它仍然会给出空错误 - 这意味着它没有检测到关闭操作。但为什么? - John
1个回答

9

你的方法存在几个问题。

前言

首先,你没有等待服务器 goroutine 完成。 在 Go 中,一旦 main() 函数因任何原因退出, 所有其他仍在运行的 goroutine 都会被强制终止。

你试图使用定时器来“同步”事物, 但这只适用于玩具情况,即使是这样, 也只有在某些时候才能起作用。

因此,让我们先修复你的代码:

package main

import (
    "fmt"
    "log"
    "net"
    "time"
)

func main() {
    addr := "127.0.0.1:8999"

    tcpaddr, err := net.ResolveTCPAddr("tcp4", addr)
    if err != nil {
        log.Fatal(err)
    }
    listener, err := net.ListenTCP("tcp", tcpaddr)
    if err != nil {
        log.Fatal(err)
    }

    // Server
    done := make(chan error)
    go func(listener net.Listener, done chan<- error) {
        for {
            conn, err := listener.Accept()
            if err != nil {
                done <- err
                return
            }
            go func(conn net.Conn) {
                var buffer [1024]byte
                n, err := conn.Read(buffer[:])
                if err != nil {
                    log.Println(err)
                } else {
                    log.Println(">", string(buffer[0:n]))
                }
                if err := conn.Close(); err != nil {
                    log.Println("error closing server conn:", err)
                }
            }(conn)
        }
    }(listener, done)

    // Client
    conn, err := net.Dial("tcp", addr)
    if err != nil {
        log.Fatal(err)
    }
    for i := 0; i < 2; i++ {
        _, err := conn.Write([]byte("hello"))
        if err != nil {
            log.Println(err)
            err = conn.Close()
            if err != nil {
                log.Println("error closing client conn:", err)
            }
            break
        }
        fmt.Println("ok")
        time.Sleep(2 * time.Second)
    }

    // Shut the server down and wait for it to report back
    err = listener.Close()
    if err != nil {
        log.Fatal("error closing listener:", err)
    }
    err = <-done
    if err != nil {
        log.Println("server returned:", err)
    }
}

我已经进行了一些小修补,例如使用log.Fatal(这是log.Print+os.Exit(1))而不是抛出异常,删除了无用的else语句以符合编码标准,将客户端的超时时间降低。我还添加了对套接字可能返回的Close错误的检查。
有趣的部分是,我们现在通过关闭监听器并等待服务器goroutine报告回来来正确关闭服务器(不幸的是,在这种情况下Go不会从net.Listener.Accept返回自定义类型的错误,因此我们不能真正检查是否因为我们关闭了监听器而导致Accept退出)。无论如何,我们的goroutines现在得到了正确的同步,并且没有未定义的行为,因此我们可以推断代码的工作原理。
剩余的问题
仍然存在一些问题。
最明显的问题是您做出了错误的假设,即TCP保留消息边界-也就是说,如果您向套接字的客户端端写入“hello”,则服务器会读回“hello”。这是不正确的:TCP认为连接的两端都产生和消耗不透明的字节流。这意味着,当客户端写入“hello”时,客户端的TCP堆栈可以自由地传送“he”并推迟发送“llo”,而服务器的堆栈可以自由地将“hell”传递给套接字上的read调用,并且仅在稍后的read中返回“o”(以及可能的其他数据)。
因此,要使代码“真实”,您需要在TCP协议之上以某种方式引入这些消息边界。在这种特殊情况下,最简单的方法是使用“消息”,其中包含一个固定长度和约定的字节序前缀,指示以下数据的长度,然后是字符串数据本身。然后,服务器将使用类似于以下序列:
var msg [4100]byte
_, err := io.ReadFull(sock, msg[:4])
if err != nil { ... }
mlen := int(binary.BigEndian.Uint32(msg[:4]))
if mlen < 0 {
  // handle error
}
if mlen == 0 {
  // empty message; goto 1
}
_, err = io.ReadFull(sock, msg[5:5+mlen])
if err != nil { ... }
s := string(msg[5:5+mlen])

另一种方法是约定消息不包含换行符,并以换行符(ASCII LF,\n,0x0a)终止每个消息。服务器端将使用类似于bufio.Scanner循环从套接字获取完整行。
你的方法剩下的问题是没有处理套接字上的Read返回值:注意,io.Reader.Read(这是套接字实现的,还有其他东西)可以在从底层流中读取一些数据的同时返回错误。在你的玩具示例中,这可能是无关紧要的,但是假设你正在编写一个类似于wget的工具,它能够恢复文件的下载:即使从服务器读取了一些数据和错误,你也必须先处理返回的数据块,然后再处理错误。
回到手头的问题
我认为,在你的设置中出现问题只是因为你的消息长度太小,导致了一些TCP缓冲问题。
在运行Linux 4.9/amd64的我的电脑上,有两件事可以可靠地“解决”这个问题:
  • 发送4000字节长度的消息:第二次调用Write会立即“看到”问题。
  • 进行更多的Write调用。
对于前者,请尝试类似于以下内容:
msg := make([]byte, 4000)
for i := range msg {
    msg[i] = 'x'
}
for {
    _, err := conn.Write(msg)
    ...

对于后者——类似以下内容:
for {
    _, err := conn.Write([]byte("hello"))
    ...
    fmt.Println("ok")
    time.Sleep(time.Second / 2)
}

在这两种情况下,降低发送内容之间的暂停时间是明智的。

有趣的是,第一个示例会出现write:connection reset by peer (在POSIX中为ECONNRESET)错误,而第二个示例会出现write:broken pipe (在POSIX中为EPIPE)。

这是因为当我们以4k字节的块发送数据时,流生成的一些数据包在服务器端关闭连接的信息传播到客户端之前就已经成为“在途”数据,这些数据包会撞到已经关闭的套接字并被拒绝,同时TCP标志位RST也被设置。 在第二个示例中,尝试发送另一块数据时,客户端已经知道连接已经断开并且不会进行发送。

简而言之

欢迎来到网络的精彩世界。 ;-)

我建议购买《TCP/IP详解》的副本,阅读并实验。 TCP(以及IP和其他在IP上层的协议)有时不像人们期望的那样通过他们的“常识”工作。


感谢您详细的回答,我想尽可能简单地阐述问题,因此我缩短了这些代码,以免注意细节。关于主要问题。是的,进行更多的写操作可以使套接字知道已关闭连接。但是在关闭的连接上写入时是否应该出现错误?这让我感到非常困惑。 - John
我再次测试代码,第二个请求中写入了相当多的字节:msg := make([]byte, 40000) for i := range msg { msg[i] = 'x' }但结果与之前相同:返回一个nil错误。因此,我不认为缓冲区会导致这样的结果。这也让我感到非常困惑。 - John
我将代码和关系截图发布在问题底部。 - John
1
@Claymore:正如详细解释的那样,在那一点上连接并没有关闭。正常关闭连接需要双方发送FIN-ACK。如果你想发送更多的数据,然后最终可能会收到RST和断开的管道错误,但你不能期望从错误的数据包中得到同步错误。 - JimB
@JimB 是的,为了使连接正常关闭(双方发送和接收FIN-ACK),第二个写入需要 等待10秒 发送其请求,但第二个写入无法检测到关闭,为什么?我甚至为第二个写入创建了一个大字节数组以立即发送,但仍然得到了“ok”,为什么? - John
1
@Claymore,你通过从连接读取数据来检测TCP关闭,而不是通过写入数据来实现。如果还无法理解,请花些时间学习底层TCP协议的工作原理。 - JimB

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