064-Channel
Posted --Allen--
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了064-Channel相关的知识,希望对你有一定的参考价值。
Golang 里 Channel 是一种数据类型。
在 《goroutine 和 chan》 一文我们就探讨过 channel 的特性,它非常像线程安全的阻塞队列,只不过 Golang 里原生支持了它。
在 Golang 里,chan 用的最多的地方就是用于 goroutine 之间的通信。很久以前你在学习多线程的时候,线程与线程之间通信的办法,一般都使用共享内存,互斥锁和条件变量这些手段,而且极易出错。但是在 Golang 里,chan 帮你屏蔽了这些细节。
上一节我们编写了一个并发的 echo server,并使用 netcat 工具向echo server 发送数据。这一节,我们就写一个小小的客户端程序吧。
1. netcat 客户端(version 1.0)
看到 version 1.0 你心里应该就能感觉到,这个版本可能有问题,先看代码。
// nc01.go
package main
import (
"io"
"log"
"net"
"os"
)
func mustCopy(w io.Writer, r io.Reader)
buf := [64]byte
for
n, err := r.Read(buf[:])
if err != nil
log.Println(err)
break
_, err = w.Write(buf[0:n])
if err != nil
log.Println(err)
break
func main()
conn, err := net.Dial("tcp", "localhost:8001")
if err != nil
log.Println(err)
return
// conn -> os.Stdout
go func()
mustCopy(os.Stdout, conn)
log.Println("done")
()
mustCopy(conn, os.Stdin)
conn.Close()
log.Println("exit")
启动你的 echo server,运行 go run nc01.go
. 随意输入一些字符,最后按 CTRL D
结束。(按下 CTRL D
后,从 stdin 会读取到 EOF)
图1 测试 echo server
注意,上面的 netcat 程序有 bug。看起来似乎是 log.Println("done")
没有执行。因为屏幕没有打印这一行。
仔细分析一下程序结束的过程:
- CTRL D 按下,
mustCopy(conn, os.Stdin)
在执行 Read 的时候,读取到 EOF 错误(这是正常的错误) - 执行
conn.Close()
- 执行
log.Prinln("exit")
这三个动作几乎一气呵成,以致于协程没有机会正常退出。
有没有办法让协程也能正常退出呢?答案是有的。还记得我们曾经使用 channal 的例子吗?当读取一个空的 channel 时,程序会阻塞。
好了,机会来了。看 Version 2.0
2. netcat 客户端 (version 2.0)
// nc02.go
// 重复代码已经省略了
func main()
conn, err := net.Dial("tcp", "localhost:8001")
if err != nil
log.Println(err)
return
// 创建一个 struct 类型的 channel
done := make(chan struct)
go func()
mustCopy(os.Stdout, conn)
log.Println("done")
// goroutine 执行完成,发送一个空对象到 channel
done <- struct
()
mustCopy(conn, os.Stdin)
conn.Close()
// 读取 channel 数据并丢弃。
<-done
log.Println("exit")
图2 测试 echo server
不过似乎又引起了新的问题。一个客户端有这么难写吗?是的,网络编程的确比你想的要复杂的多。再来分析一下图 2 的结果:
- 从 stdin 读取到 EOF 后,执行
conn.Close()
- 主协程(main 所在的协程)执行
<-done
,主程阻塞 - 子协程从 Read 调用中返回一个错误:read 错误,使用了已关闭的网络连接
- 子协程打印 done,然后执行
done<-struct
- 主协程
<-done
返回 - 程序退出
虽然我们解决了一个 bug,但是却引来另一个 bug.
Golang 里,直接调用调用 Close 关闭连接,会导致 conn 变得不可读也不可写,否则会返回图 2 里那样的错误。
那怎么办?要不把 <-done
和 conn.Close()
这两行换个顺序?这肯定不行,这样程序就死锁了(想想为什么?)
稍微懂一点 tcp 协议的同学都知道,tcp 是一个全双工通信协议。你可以只关闭 tcp 通道的写端,也可以只关闭 tcp 通道的读端。当然,也可以两者都关闭。
一个比较优雅的做法就是关闭 tcp 通道的写端,而不关闭读端。因此我们可以改善一下我们的程序,看 version 3.0
3. netcat 客户端 (version 3.0)
func main()
co, err := net.Dial("tcp", "localhost:8001")
if err != nil
log.Println(err)
return
// 使用类型断言,拿到 *net.TCPConn 的值
conn := co.(*net.TCPConn)
done := make(chan struct)
go func()
mustCopy(os.Stdout, conn)
log.Println("done")
done <- struct
()
mustCopy(conn, os.Stdin)
// 调用写关闭。只有 *net.TCPConn 才有这个方法,这是类型断言的目的。
conn.CloseWrite()
<-done
log.Println("exit")
图3 测试 echo server
这样一来,程序就完美啦!
4. 总结
- 掌握使用通道进行协程同步的方法
本文虽然是讲 Channel,但是却花了大量笔墨在网络编程上。实际上,这一节只是讲解了 Channel 的一个简单应用。关于 Channel 的功能,相信你在读完 《goroutine 和 chan》 早已经了如指掌,因此重复这些知识点完全没有必要。
后面几篇文章也基本上是关于 Channel 的应用和更多的关于 Channel 的语法。
以上是关于064-Channel的主要内容,如果未能解决你的问题,请参考以下文章