net.TCPConn允许在FIN数据包之后进行写操作。

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

net.TCPConn allowing Write after FIN packet

问题

我正在尝试为一些服务器端代码编写单元测试,但是在关闭测试用例时无法确定性地进行操作。似乎回环TCP连接没有正确处理干净的关闭。我在一个示例应用程序中重现了这个问题,该应用程序按照以下步骤进行操作:

  1. 创建客户端和服务器连接。
  2. 通过发送一条消息从客户端到服务器成功验证连接。
  3. 使用通道告诉服务器调用conn.Close()并等待该调用完成。
  4. (尝试)通过再次在客户端连接上调用Write来验证连接是否干净地断开。

第4步成功完成而没有错误。我尝试使用json.Encoder和直接调用TCPConn.Write。我使用WireShark检查了流量。服务器发送了一个FIN数据包,但客户端没有发送(即使有1秒的延迟)。服务器甚至在响应(4)时发送了一个RST数据包,但客户端的conn.Write仍然返回nil作为其错误。

这看起来完全荒谬。我是否漏掉了什么?当前运行的是Go v1.2.1/Darwin。

编辑:必要的重现代码

package main

import (
  "bufio"
  "fmt"
  "net"
)

var (
  loopback = make(chan string)
  shouldClose = make(chan struct{})
  didClose = make(chan struct{})
)

func serve(listener *net.TCPListener) {
  conn, err := listener.Accept()
  if err != nil {
    panic(err)
  }

  s := bufio.NewScanner(conn)
  if !s.Scan() {
    panic(fmt.Sprint("Failed to scan for line: ", s.Err()))
  }

  loopback <- s.Text() + "\n"

  <-shouldClose
  conn.Close()
  close(didClose)

  if s.Scan() {
    panic("Expected error reading from a socket closed on this side")
  }
}

func main() {
  listener, err := net.ListenTCP("tcp", &net.TCPAddr{})
  if err != nil {
    panic(err)
  }
  go serve(listener)

  conn, err := net.Dial("tcp", listener.Addr().String())
  if err != nil {
    panic(fmt.Sprint("Dialer got error ", err))
  }

  oracle := "Mic check\n"
  if _, err = conn.Write([]byte(oracle)); err != nil {
    panic(fmt.Sprint("Dialer failed to write oracle: ", err))
  }

  test := <-loopback
  if test != oracle {
    panic("Server did not receive the value sent by the client")
  }

  close(shouldClose)
  <-didClose

  // For giggles, I can also add a <-time.After(500 * time.Millisecond)
  if _, err = conn.Write([]byte("This should fail after active disconnect")); err == nil {
    panic("Sender 'successfully' wrote to a closed socket")
  }
}
英文:

I'm trying to write unit tests for some server-side code, but I'm having trouble being deterministic with my shutdown test cases. It seems a loopback TCP connection isn't correctly handling a clean shutdown. I've reprod this in a sample app which does the following in lockstep:

  1. Create a client & server connection.
  2. Verify connectivity by sending a message successfully from client to server.
  3. Use channels to tell the server to call conn.Close() and wait until that call has completed.
  4. (Try to) verify the connection is cleanly broken by calling Write on the client connection again.

Step 4 succeeds without error. I've tried using a json.Encoder and a bare call to TCPConn.Write. I checked the traffic with WireShark. The server sent a FIN packet, but the client never does (even with a 1s sleep) The server even sent a RST packet in response to (4) and the client conn.Write still returned nil for its error.

This seems totally bonkers. Am I missing something here? Currently running Go v1.2.1/Darwin

Edit: Obligatory repro

package main
import (
&quot;bufio&quot;
&quot;fmt&quot;
&quot;net&quot;
)
var (
loopback = make(chan string)
shouldClose = make(chan struct{})
didClose = make(chan struct{})
)
func serve(listener *net.TCPListener) {
conn, err := listener.Accept()
if err != nil {
panic(err)
}
s := bufio.NewScanner(conn)
if !s.Scan() {
panic(fmt.Sprint(&quot;Failed to scan for line: &quot;, s.Err()))
}
loopback &lt;- s.Text() + &quot;\n&quot;
&lt;-shouldClose
conn.Close()
close(didClose)
if s.Scan() {
panic(&quot;Expected error reading from a socket closed on this side&quot;)
}
}
func main() {
listener, err := net.ListenTCP(&quot;tcp&quot;, &amp;net.TCPAddr{})
if err != nil {
panic(err)
}
go serve(listener)
conn, err := net.Dial(&quot;tcp&quot;, listener.Addr().String())
if err != nil {
panic(fmt.Sprint(&quot;Dialer got error &quot;, err))
}
oracle := &quot;Mic check\n&quot;
if _, err = conn.Write([]byte(oracle)); err != nil {
panic(fmt.Sprint(&quot;Dialer failed to write oracle: &quot;, err))
}
test := &lt;-loopback
if test != oracle {
panic(&quot;Server did not receive the value sent by the client&quot;)
}
close(shouldClose)
&lt;-didClose
// For giggles, I can also add a &lt;-time.After(500 * time.Millisecond)
if _, err = conn.Write([]byte(&quot;This should fail after active disconnect&quot;)); err == nil {
panic(&quot;Sender &#39;successfully&#39; wrote to a closed socket&quot;)
}
}

答案1

得分: 4

这是一个TCP连接主动关闭的工作原理。当客户端检测到服务器已关闭时,它应该关闭连接的一半。

在你的情况下,你没有关闭客户端,而是发送了更多的数据。这导致服务器发送一个RST数据包来强制关闭连接,因为接收到的消息不是有效的。

如果你还不确定,这里有一个等效的Python客户端+服务器,显示相同的行为(我发现使用Python很有帮助,因为它紧密遵循底层的BSD套接字API,而不使用C)。

服务器:

import socket, time

server = socket.socket()
server.bind(("127.0.0.1", 9999))
server.listen(1)
sock, addr = server.accept()
msg = sock.recv(1024)
print(msg)
print("closing")
sock.close()
time.sleep(3)
print("done")

客户端:

import socket, time

sock = socket.socket()
sock.connect(("127.0.0.1", 9999))
sock.send("test\n")
time.sleep(1)
print("sending again!")
sock.send("no error here")
time.sleep(1)
print("sending one last time")
sock.send("broken pipe this time")

为了正确检测连接的远程关闭,你应该使用Read()函数,并检查返回的io.EOF错误。

// 我们技术上需要尝试读取至少一个字节,
// 否则即使连接没有关闭,我们也会得到一个EOF错误。
buff := make([]byte, 1)
if _, err := conn.Read(buff); err != io.EOF {
    panic("connection not closed")
}
英文:

This is how an active close of a TCP connection works. When the client detects that the server has closed, it is then expected to close its half of the connection.

In your case, instead of closing the client you're sending more data. This causes the server to send an RST packet to force the connection closed since the message received isn't valid.

If you're still unsure, here's and equivalent python client+server which displays the same behavior. (I find using python helpful, since it closely follows the underlying BSD socket API, without using C)

Server:

import socket, time
server = socket.socket()
server.bind((&quot;127.0.0.1&quot;, 9999))
server.listen(1)
sock, addr = server.accept()
msg = sock.recv(1024)
print msg
print &quot;closing&quot;
sock.close()
time.sleep(3)
print &quot;done&quot;

Client:

import socket, time
sock = socket.socket()
sock.connect((&quot;127.0.0.1&quot;, 9999))
sock.send(&quot;test\n&quot;)
time.sleep(1)
print &quot;sending again!&quot;
sock.send(&quot;no error here&quot;)
time.sleep(1)
print &quot;sending one last time&quot;
sock.send(&quot;broken pipe this time&quot;)

To properly detect a remote close on the connection, you should do Read(), and look for an io.EOF error in return.

// we technically need to try and read at least one byte, 
// or we will get an EOF even if the connection isn&#39;t closed.
buff := make([]byte, 1)
if _, err := conn.Read(buff); err != io.EOF {
panic(&quot;connection not closed&quot;)
}

huangapple
  • 本文由 发表于 2014年5月27日 07:35:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/23879000.html
匿名

发表评论

匿名网友

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

确定