【问题标题】:Apparent deadlock between server and client服务器和客户端之间的明显死锁
【发布时间】:2018-03-31 22:59:55
【问题描述】:

我有一个测试函数,它既可以创建一个服务器,又可以生成一个作为客户端的 goroutine。现在,只需从客户端向服务器发送一条消息就可以了,但是如果我想创建一个交换,它们似乎会死锁,因为测试永远不会运行到完成(如果没有设置 r/w 截止日期)。例如,我希望客户端向服务器发送一条消息,服务器复制该消息并将其发送回客户端,然后客户端验证接收到的消息是否相同。这是我的测试代码:

func TestSendAwait(t *testing.T) {
    m := "Hello World"

    go func() {
        conn, err := net.Dial("tcp", testingAddr)
        if err != nil {
            t.Fatal(err)
        }
        defer conn.Close()
        t.Log("client connected to server") // DEBUG

        conn.SetDeadline(time.Now().Add(2 * time.Second))
        conn.Write([]byte(m))

        conn.SetDeadline(time.Now().Add(2 * time.Second))
        buf, err := ioutil.ReadAll(conn)
        if err != nil {
            t.Fatal(err)
        }
        t.Log(string(buf))
    }()

    ln, err := net.Listen("tcp", testingAddr)
    if err != nil {
        t.Fatal(err)
    }
    defer ln.Close()
    t.Log("server started") // DEBUG

    conn, err := ln.Accept()
    if err != nil {
        t.Fatal(err)
    }
    defer conn.Close()
    t.Log("server received connection") // DEBUG

    buf, err := ioutil.ReadAll(conn)
    if err != nil {
        t.Fatal(err)
    }
    t.Logf("server read buffer: %v", buf) // DEBUG

    _, err = conn.Write(buf)
    if err != nil {
        t.Fatal(err)
    }
    t.Log("server wrote to connection") // DEBUG
}

在连接上设置了最后期限,否则死锁将是无限期的。输出如下:

    transmission_test.go:42: server started
    transmission_test.go:24: client connected to server
    transmission_test.go:49: server received connection
    transmission_test.go:32: read tcp 127.0.0.1:41164->127.0.0.1:9090: i/o timeout
    transmission_test.go:55: server read buffer: [72 101 108 108 111 32 87 111 114 108 100]
    transmission_test.go:61: server wrote to connection

Process finished with exit code 1

我不明白为什么客户端无法读取并退出,然后服务器才决定通过套接字发送数据?即使我增加客户端中的读取期限也会发生这种情况。

【问题讨论】:

  • 我开始怀疑这是因为客户端没有通知服务器它发送的消息已经结束。我不知道如何去测试这个理论。一个线程建议在消息中添加换行符,但没有奏效。

标签: go networking server client deadlock


【解决方案1】:

程序阻塞对 ioutil.ReadAll 的调用。此函数一直读取,直到返回 io.EOF 或其他错误。

一种解决方法是在将数据写入连接后关闭写入。这将导致对等端的读取返回 io.EOF 并让 ioutil.ReadAll 成功返回。

    conn.Write(data)
    cw, ok := conn.(interface{ CloseWrite() error })
    if !ok {
        // handle error
    }
    cw.CloseWrite()

playground example

问题中的程序不保证在拨打连接之前打开侦听器,或者客户端将打印接收到的消息。操场示例解决了这些问题。

另一种方法是以某种方式构建消息:

  • 在一个又一个消息中不允许写入换行符或其他字节序列。读取直到找到这个字节序列。
  • 在消息前写入消息长度。读取长度,然后指定字节数。

【讨论】:

  • 啊,谢谢,我使用ioutil.ReadAll 是为了方便避免制作缓冲区。我可以通过简单地删除对服务器中的调用并直接使用conn.Read() 来解决问题,它在从客户端发送消息后返回,而不是等待更多。
  • @jeanluc conn.Read() 可以在收到整个消息之前返回。
  • 是的,这可能是个问题,但由于我只是将它用作测试支架并且知道最大尺寸,所以应该没问题。在生产中,套接字将用gob 包裹,我认为它会处理消息大小
  • 但是由于这个问题的读者如果遇到类似的问题很可能不知道最大尺寸,所以您提供的建议很好
  • 知道最大大小可以在一次调用 Read 中吞下整个消息,但不能保证它会吞下整个消息。数据可用时读取返回。这可能与对等方对 Write 的调用有任何关系,也可能没有关系。如果您使用 gob,您将被覆盖,因为 gob 编码器帧值。
猜你喜欢
  • 2023-03-12
  • 2011-02-10
  • 1970-01-01
  • 2015-03-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-05-12
  • 1970-01-01
相关资源
最近更新 更多