在封闭的net.Conn上写入,但返回nil错误

Joh*_*ohn 2 sockets connection tcp go

对话很便宜,因此这里我们使用简单的代码:

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)
    }

}
Run Code Online (Sandbox Code Playgroud)

输出:

> hello
ok
ok
Run Code Online (Sandbox Code Playgroud)

客户端两次写入服务器。第一次读取后,服务器立即关闭连接,但是客户端休眠10秒钟,然后使用相同的已关闭连接object(conn)重新写入服务器。

为什么第二次写入成功(返回错误为nil)?

有人可以帮忙吗?

PS:

为了检查系统的缓冲功能是否影响第二次写入的结果,我像这样编辑了Client,但它仍然成功:

// 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)
}
Run Code Online (Sandbox Code Playgroud)

这是屏幕截图: 附件

kos*_*tix 7

您的方法存在几个问题。

序言

第一个是您不必等待服务器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)
    }
}
Run Code Online (Sandbox Code Playgroud)

我已经花了几个小问题,例如使用log.Fatal( log.Print+ os.Exit(1))而不是恐慌,删除了无用的else子句以遵守将主流保持在其所属位置的编码标准,并降低了客户端的超时时间。我还添加了Close对套接字可能返回的错误的检查。

有趣的是,我们现在通过关闭侦听器,然后等待服务器goroutine报告正确,从而正确关闭了服务器(不幸的是,net.Listener.Accept在这种情况下,Go不会返回自定义类型的错误,因此我们无法真正检查Accept退出,因为我们已经关闭了侦听器)。无论如何,我们的goroutine现在已正确同步,并且没有未定义的行为,因此我们可以推断代码的工作方式。

仍然存在的问题

仍然存在一些问题。

更明显的是您错误地假设TCP会保留消息边界,也就是说,如果您向套接字的客户端写入“ hello”,则服务器会回读“ hello”。这是不正确的:TCP将连接的两端都视为产生和消耗不透明的字节流。 这意味着,当客户端写“ hello”时,客户端的TCP堆栈可以自由传递“ he”并推迟发送“ llo”,而服务器的堆栈可以自由地read 向套接字上的调用产生“ hell”,而仅返回“ o”(可能还有其他一些数据)read。

因此,要使代码“真实”,您需要以某种方式将这些消息边界引入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])
Run Code Online (Sandbox Code Playgroud)

另一种方法是就消息不包含换行符达成一致,并以换行符结束每个消息(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)
    ...
Run Code Online (Sandbox Code Playgroud)

对于后者-就像

for {
    _, err := conn.Write([]byte("hello"))
    ...
    fmt.Println("ok")
    time.Sleep(time.Second / 2)
}
Run Code Online (Sandbox Code Playgroud)

(在两种情况下都可以降低发送内容之间的间隔时间)。

这是有趣的是,前者例如撞击 write: connection reset by peer(ECONNRESET在POSIX)错误,而第二个安打write: broken pipe (EPIPE在POSIX)。

这是因为当我们以4k字节的大小发送数据块时,为流生成的一些数据包在服务器的连接端设法将其关闭信息传播给客户端之前就变得“处于运行状态”,而那些数据包到达已经关闭的套接字,并被RST设置为TCP标志而被拒绝。在第二个示例中,尝试发送另一个数据块的过程表明,客户端已经知道连接已断开,并且发送失败而没有“触碰电线”。

TL; DR,底线

欢迎来到网络的美好世界。;-)

我建议购买一份“ TCP / IP Illustrated”,阅读并进行试验。TCP(以及IP和IP之上的其他协议)有时无法像人们期望的那样通过应用其“常识”来工作。