对话很便宜,因此我们在这里进行简单的代码:
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秒,然后使用相同的已关闭连接对象({{ 1}})。
为什么第二次写入成功(返回错误为nil)?
有人可以帮忙吗?
PS:
为了检查系统的缓冲功能是否影响第二次写入的结果,我像这样编辑了Client,但它仍然成功:
conn
这是屏幕截图: attachment
答案 0 :(得分:6)
您的方法存在几个问题。
第一个是您不必等待服务器goroutine
去完成。
在Go语言中,一旦main()
出于某种原因退出,
所有其他仍在运行的goroutines(如果有)只是
强行拆除。
您正在尝试使用计时器“同步”事物, 但这仅适用于玩具情况,即使如此 只是偶尔这样做。
因此,让我们先修复您的代码:
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
是否退出,因为我们已经关闭了侦听器)。
无论如何,我们的goroutine现在已正确同步,并且
没有不确定的行为,因此我们可以推断代码的工作方式。
仍然存在一些问题。
更明显的是您错误地假设TCP保留了
消息边界-即,如果您向客户端写“ hello”
套接字的末尾,服务器回读“ hello”。
这是不正确的:TCP会考虑连接的两端
产生和消耗不透明的字节流。
这意味着,当客户端写“ hello”时,客户端的
TCP堆栈可以自由传递“ he”,并可以延迟发送“ llo”,
并且服务器的堆栈可以自由地向read
产生“地狱”
调用套接字,仅返回“ o”(可能还返回其他一些
数据)在以后的read
中。
因此,要使代码“真实”,您需要以某种方式引入这些内容 消息边界进入TCP之上的协议。 在这种情况下,最简单的方法是 使用由固定长度和双方同意的“消息”组成 endianness前缀,指示以下内容的长度 数据,然后是字符串数据本身。 然后,服务器将使用类似
的序列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
loop
套接字上的整行。
您的方法剩下的问题是不处理
套接字上的Read
返回什么:请注意,io.Reader.Read
(这是套接字实现的功能)
从读取了一些数据后返回错误
基础流。在您的玩具示例中,这可能是正确的
无关紧要,但是假设您正在编写类似wget
能够恢复下载文件的工具:即使
从服务器读取返回的 some 数据和错误,您
必须先处理返回的块,然后再处理
处理错误。
我相信问题中提出的问题仅是因为在您的设置中由于消息的长度太短而遇到了一些TCP缓冲问题。
在运行Linux 4.9 / amd64的机器上,两件事可靠地“修复” 问题:
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字节的数据块时,
为流生成的一些数据包设法成为
连接的服务器端进行管理之前的“运行中”
将其关闭信息传播给客户端,
那些数据包撞到已经关闭的套接字并被拒绝
设置了RST
TCP标志。
在第二个示例中,尝试发送另一个数据块
看到客户端已经知道连接
已被拆除,并且发送失败时没有“碰
电线”。
欢迎来到网络的美好世界。 ;-)
我建议购买“ TCP / IP Illustrated”的副本, 阅读并进行实验。 TCP(以及IP和IP之上的其他协议) 有时工作不像人们期望的那样通过应用 他们的“常识”。