案例:并发的clock时钟服务
络编程是并发大显身手的一个领域,由于服务器是最典型的需要同时处理很多连接的程序,这些连接一般来自于彼此独立的客户端。在本小节中,我们会讲解go语言的net包,这个包提供编写一个网络客户端或者服务器程序的基本组件,无论两者间通信是使用TCP,UDP或者Unix domain sockets。在第一章中我们使用过的net/http包里的方法,也算是net包的一部分。
我们的第一个例子是一个顺序执行的时钟服务器,它会每隔一秒钟将当前时间写到客户端:
package main
import (
"io"
"log"
"net"
"time"
)
func handleConn(c net.Conn) {
defer c.Close()
for {
//直接向客户端套接字,写当前系统时间
_, err := io.WriteString(c, time.Now().Format("15:04:05\n"))
if err != nil {
log.Print(err)
return // 由于defer语法,c.Close()会在return之前被调用
}
time.Sleep(time.Second * 1)
}
}
func main() {
listener, err := net.Listen("tcp", "localhost:8000")
if err != nil {
//err != nil 说明err 有值,说明有错误
log.Fatal(err)
}
//for 语句如果没有任何条件,表示一直循环,直到遇见break退出
for {
//得到新的客户端链接
conn, err := listener.Accept()
if err != nil {
log.Print(err)
continue
}
//处理客户端链接
handleConn(conn)
}
}
go run test2_clock_server_demo.go
再开一个终端
nc 127.0.0.1 8000
会得到时间信息
Listen函数创建了一个net.Listener的对象,这个对象会监听一个网络端口上到来的连接,在这个例子里我们用的是TCP的localhost:8000端口。listener对象的Accept方法会直接阻塞,直到一个新的连接被创建,然后会返回一个net.Conn对象来表示这个连接。
handleConn函数会处理一个完整的客户端连接。在一个for死循环中,用time.Now()获取当前时刻,然后写到客户端。由于net.Conn实现了io.Writer接口,我们可以直接向其写入内容。这个死循环会一直执行,直到写入失败。最可能的原因是客户端主动断开连接。这种情况下handleConn函数会用defer调用关闭服务器侧的连接,然后返回到主函数,继续等待下一个连接请求。
time.Time.Format方法提供了一种格式化日期和时间信息的方式。它的参数是一个格式化模板,标识如何来格式化时间,而这个格式化模板限定为Mon Jan 2 03:04:05PM 2006 UTC-0700。有8个部分(周几,月份,一个月的第几天,等等)。可以以任意的形式来组合前面这个模板;出现在模板中的部分会作为参考来对时间格式进行输出。在上面的例子中我们只用到了小时、分钟和秒。time包里定义了很多标准时间格式,比如time.RFC1123。在进行格式化的逆向操作time.Parse时,也会用到同样的策略。(译注:这是go语言和其它语言相比比较奇葩的一个地方。。你需要记住格式化字符串是1月2日下午3点4分5秒零六年UTC-0700,而不像其它语言那样Y-m-d H:i:s一样,当然了这里可以用1234567的方式来记忆,倒是也不麻烦)
客户端将服务器发来的时间显示了出来,我们用Control+C来中断客户端的执行,在Unix系统上,你会看到^C这样的响应。如果你的系统没有装nc这个工具,你可以用telnet来实现同样的效果,或者也可以用我们下面的这个用go写的简单的telnet程序,用net.Dial就可以简单地创建一个TCP连接:
package main
import (
"io"
"log"
"net"
"os"
)
func dealRecvData(dst io.Writer, src io.Reader) {
//将读缓冲 拷贝到写缓冲
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
//os.Stdout标准输出,作为写io设备, conn客户端套接字作为读io设备
dealRecvData(os.Stdout, conn)
}
运行这个客户端,依然可以向netcat一样执行。
但是如果在第一个客户端已经执行的情况下,再打开一个客户端。
第二个客户端必须等待第一个客户端完成工作,这样服务端才能继续向后执行;因为我们这里的服务器程序同一时间只能处理一个客户端连接。我们这里对服务端程序做一点小改动,使其支持并发:在handleConn函数调用的地方增加go关键字,让每一次handleConn的调用都进入一个独立的goroutine。
package main
import (
"io"
"log"
"net"
"time"
)
func handleConn(c net.Conn) {
defer c.Close()
for {
//直接向客户端套接字,写当前系统时间
_, err := io.WriteString(c, time.Now().Format("15:04:05\n"))
if err != nil {
log.Print(err)
return // 由于defer语法,c.Close()会在return之前被调用
}
time.Sleep(time.Second * 1)
}
}
func main() {
listener, err := net.Listen("tcp", "localhost:8000")
if err != nil {
//err != nil 说明err 有值,说明有错误
log.Fatal(err)
}
//for 语句如果没有任何条件,表示一直循环,直到遇见break退出
for {
//得到新的客户端链接
conn, err := listener.Accept()
if err != nil {
log.Print(err)
continue
}
//处理客户端链接
go handleConn(conn)
}
}
现在两个客户端就可以同时连接服务器了。