案例:并发的echo服务
clock服务器每一个连接都会起一个goroutine。在本节中我们会创建一个echo服务器,这个服务在每个连接中会有多个goroutine。大多数echo服务仅仅会返回他们读取到的内容,就像下面这个简单的handleConn函数所做的一样:
func handleConn(c net.Conn) {
io.Copy(c, c) // NOTE: ignoring errors
c.Close()
}
一个更有意思的echo服务应该模拟一个实际的echo的“回响”,并且一开始要用大写HELLO来表示“声音很大”,之后经过一小段延迟返回一个有所缓和的Hello,然后一个全小写字母的hello表示声音渐渐变小直至消失,像下面这个版本的handleConn
服务端代码:
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
"time"
)
func main() {
listener, err := net.Listen("tcp", "127.0.0.1:8001")
if err != nil {
log.Fatal(err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err)
continue
}
//处理客户端业务
handleConn(conn)
}
}
func echo(c net.Conn, outstr string, delay time.Duration) {
fmt.Fprintln(c, strings.ToUpper(outstr))
time.Sleep(delay)
fmt.Fprintln(c, outstr)
time.Sleep(delay)
fmt.Fprintln(c, strings.ToLower(outstr))
}
func handleConn(c net.Conn) {
//input是一个Scanner类型,该类型可以通过Scan方法依次迭代从io设备中读数据,知道遇到eof为止
input := bufio.NewScanner(c)
//Scan方法 如果缓冲有数据会返回true,否则返回false
for input.Scan() {
//如果有数据 input.Text()可以取出
echo(c, input.Text(), 1*time.Second)
}
c.Close()
}
客户端代码:
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:8001")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
go dealRecvData(os.Stdout, conn)
//从客户端输入,将客户端标输入的数据发给客户端套接字
dealRecvData(conn, os.Stdin)
}
这样启动服务器,再启动客户端,输入几句话,服务端会为每句话响应3次,分别是大写,正常,小写。
Itcast (客户端输入)
ITCAST (服务端第1次响应)
Itcast (服务端第2次响应)
itcast (服务端第3次响应)
Hello Go Are you there! (客户端输入)
HELLO GO ARE YOU THERE! (服务端第1次响应)
Xixi (客户端输入)
Hello Go Are you there! (服务端第2次响应)
hello go are you there! (服务端第3次响应)
XIXI (服务端第1次响应)
Xixi (服务端第2次响应)
xixi (服务端第3次响应)
注意客户端的第三次喊话在前一个喊话处理完成之前一直没有被处理,这貌似看起来不是特别“现实”。真实世界里的回响应该是会由三次喊话的回声组合而成的。为了模拟真实世界的回响,我们需要更多的goroutine来做这件事情。这样我们就再一次地需要go这个关键词了,这次我们用它来调用echo:
服务器代码:
func handleConn(c net.Conn) {
//input是一个Scanner类型,该类型可以通过Scan方法依次迭代从io设备中读数据,知道遇到eof为止
input := bufio.NewScanner(c)
//Scan方法 如果缓冲有数据会返回true,否则返回false
for input.Scan() {
//如果有数据 input.Text()可以取出
go echo(c, input.Text(), 1*time.Second)
}
c.Close()
}
go后跟的函数的参数会在go语句自身执行时被求值;因此input.Text()会在main goroutine中被求值。 现在回响是并发并且会按时间来覆盖掉其它响应了:
Is there anybody there?
IS THERE ANYBODY THERE?
Yooo-hooo!
Is there anybody there?
YOOO-HOOO!
is there anybody there?
Yooo-hooo!
yooo-hooo!
让服务使用并发不只是处理多个客户端的请求,甚至在处理单个连接时也可能会用到,就像我们上面的两个go关键词的用法。然而在我们使用go关键词的同时,需要慎重地考虑net.Conn中的方法在并发地调用时是否安全,事实上对于大多数类型来说也确实不安全。我们会在接下来详细地探讨并发安全性。