笔者在转到 golang 之后使用最多的就是 Grpc 的库,这次裸写 tcp 的 client ,由于 client 的 write 阻塞间接导致了代码死锁,在此处记录下。
client write 的分类
写成功
「写成功」指的是 write 调用返回的 n 与预期要写入的数据长度相等,且 error 为 nil 。函数原型如下:
func (c *TCPConn) Write(b []byte) (int, error)
写阻塞
tcp 连接建立后操作系统会为该连接保存数据缓冲区,当其中某一端调用 write 后,数据实际上是写入系统的缓冲区中,缓冲区分为发送缓冲区和接收缓冲区。当发送方将对方的接收缓冲区和自己的发送缓冲区均写满后,write 操作就会阻塞。笔者写了一个例子,效果见下图:
- server 端在开始的前 10 秒不会从缓冲区中读取任何数据,但 client 端在持续不断的将数据写入缓冲区,在双方的缓冲区均满了以后就会出现上述写阻塞的效果。
- server 端开始以 10s 的固定间隔读取数据,使缓冲区重新进入可写的状态,client 端就可以继续写入数据。
写入部分数据
write 存在发送方写入部分数据后被强制中断的情况,这种情况下接收方收到的就是发送方写入的部分数据,对于写入部分数据的情况接收方需要做特定的处理。
### 写入超时
笔者就是因为上述的写阻塞间接导致代码里产生了一个死锁,大致可描述为 client.Write 操作需要获取上读锁的资源,但是同时存在一个后台的 goroutine 定期的会去获取写锁更新该资源的状态,由于 client.Write 阻塞间接导致读锁的资源不会被释放,导致代码死锁。
解决上述的问题有几个方式:
- 一个是给 client.Write 操作加上一个超时
- 一个是在 client 和 server 端使用连接池,一个连接的缓冲区不够大的话,就是用多个呗,三个臭皮匠顶一个诸葛亮(这个大家都会,就不介绍了)
- server 一个消费者不能跟发送方的生产者匹配的话,也可以使用多个消费者同时消费
- 需要确认是否是线程安全的
- tcp 是字节流的多个消费者同时消费是否会导致消费的信息错乱
给 client.Write 操作加上一个超时,就是调用 SetWriteDeadLine方法,在 client.go 的 Write 之前加上一行 timeout 的设置代码:
conn.SetWriteDeadline(time.Now().Add(time.Microsecond * 10))
测试代码
server
package main
import (
"fmt"
"net"
"time"
)
func handle(conn net.Conn) {
defer conn.Close()
for {
//read data from connection
time.Sleep(10 * time.Second)
buf := make([]byte, 65536)
fmt.Println("begin read data")
n, err := conn.Read(buf)
if err != nil {
fmt.Printf("time %v, conn read %d bytes, error: %s", time.Now().Format(time.RFC3339), n, err)
continue
}
fmt.Printf("time %v, read %d bytes, content is %s\n", time.Now().Format(time.RFC3339), n, string(buf[:n]))
}
}
func main() {
l, err := net.Listen("tcp", ":9090")
if err != nil {
fmt.Println("error listen:", err)
return
}
fmt.Println("listen success")
for {
conn, err := l.Accept()
if err != nil {
fmt.Println("error accept", err)
return
}
go handle(conn)
}
}
client
package main
import (
"fmt"
"net"
"time"
)
func main() {
conn, err := net.Dial("tcp", ":9090")
if err != nil {
fmt.Println("error dial", err)
return
}
defer conn.Close()
fmt.Println("dial ok")
data := make([]byte, 65536)
var total int
for {
conn.SetWriteDeadline(time.Now().Add(time.Microsecond * 100))
n, err := conn.Write(data)
if err != nil {
total += n
fmt.Printf("time %v, write %d bytes, error: %s\n", time.Now().Format(time.RFC3339), n, err)
break
}
total += n
fmt.Printf("time %v, write %d bytes this time, total bytes is %d\n", time.Now().Format(time.RFC3339), n, total)
}
}
总结
学习知识重要的是举一反三的能力, write 操作有这么多种情况, 那么 read 操作呢? accept 操作呢?详细解释见参考资料 Go 语言 TCP Socket 编程