# 15.1 tcp 服务器 这部分我们将使用 TCP 协议和在 14 章讲到的协程范式编写一个简单的客户端-服务器应用,一个 (web) 服务器应用需要响应众多客户端的并发请求:Go 会为每一个客户端产生一个协程用来处理请求。我们需要使用 net 包中网络通信的功能。它包含了处理 TCP/IP 以及 UDP 协议、域名解析等方法。 服务器端代码是一个单独的文件: 示例 15.1 [server.go](examples/chapter_15/server.go) ```go package main import ( "fmt" "net" ) func main() { fmt.Println("Starting the server ...") // 创建 listener listener, err := net.Listen("tcp", "localhost:50000") if err != nil { fmt.Println("Error listening", err.Error()) return //终止程序 } // 监听并接受来自客户端的连接 for { conn, err := listener.Accept() if err != nil { fmt.Println("Error accepting", err.Error()) return // 终止程序 } go doServerStuff(conn) } } func doServerStuff(conn net.Conn) { for { buf := make([]byte, 512) len, err := conn.Read(buf) if err != nil { fmt.Println("Error reading", err.Error()) return //终止程序 } fmt.Printf("Received data: %v", string(buf[:len])) } } ``` 在 `main()` 中创建了一个 `net.Listener` 类型的变量 `listener`,他实现了服务器的基本功能:用来监听和接收来自客户端的请求(基于 TCP 协议下,位于 IP 地址为 127.0.0.1、端口为 50000 的 localhost)。`Listen()` 函数可以返回一个 `error` 类型的错误变量。用一个无限 `for` 循环的 `listener.Accept()` 来等待客户端的请求。客户端的请求将产生一个 `net.Conn` 类型的连接变量。然后一个独立的协程使用这个连接执行 `doServerStuff()`,开始使用一个 512 字节的缓冲 `data` 来读取客户端发送来的数据,并且把它们打印到服务器的终端,`len()` 获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。 客户端代码写在另一个文件 client.go 中: 示例 15.2 [client.go](examples/chapter_15/client.go) ```go package main import ( "bufio" "fmt" "net" "os" "strings" ) func main() { //打开连接: conn, err := net.Dial("tcp", "localhost:50000") if err != nil { //由于目标计算机积极拒绝而无法创建连接 fmt.Println("Error dialing", err.Error()) return // 终止程序 } inputReader := bufio.NewReader(os.Stdin) fmt.Println("First, what is your name?") clientName, _ := inputReader.ReadString('\n') // fmt.Printf("CLIENTNAME %s", clientName) trimmedClient := strings.Trim(clientName, "\r\n") // Windows 平台下用 "\r\n",Linux平台下使用 "\n" // 给服务器发送信息直到程序退出: for { fmt.Println("What to send to the server? Type Q to quit.") input, _ := inputReader.ReadString('\n') trimmedInput := strings.Trim(input, "\r\n") // fmt.Printf("input:--%s--", input) // fmt.Printf("trimmedInput:--%s--", trimmedInput) if trimmedInput == "Q" { return } _, err = conn.Write([]byte(trimmedClient + " says: " + trimmedInput)) } } ``` 客户端通过 `net.Dial()` 创建了一个和服务器之间的连接。 它通过无限循环从 `os.Stdin` 接收来自键盘的输入,直到输入了“Q”。注意裁剪 `\r` 和 `\n` 字符(仅 Windows 平台需要)。裁剪后的输入被 `connection` 的 `Write()` 方法发送到服务器。 当然,服务器必须先启动好,如果服务器并未开始监听,客户端是无法成功连接的。 如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息: ``` dial tcp [::1]:xxxx: connectex: No connection could be made because the target machine actively refused it. ``` 打开命令提示符并转到服务器和客户端代码所在的目录,输入 `go run server.go`,接下来控制台出现以下信息:`Starting the server ...` 在 Windows 系统中,我们可以通过 CTRL+C 停止程序。 然后开启 2 个或者 3 个独立的控制台窗口,分别启动客户端程序 以下是服务器的输出: ``` Starting the Server ... Received data: IVO says: Hi Server, what's up ? Received data: CHRIS says: Are you busy server ? Received data: MARC says: Don't forget our appointment tomorrow ! ``` 当客户端输入“Q”并结束程序时,服务器会输出以下信息: ``` Error reading WSARecv tcp 127.0.0.1:50000: The specified network name is no longer available. ``` 在网络编程中 `net.Dial()` 函数是非常重要的,一旦你连接到远程系统,函数就会返回一个 `Conn` 类型的接口,我们可以用它发送和接收数据。`Dial()` 函数简洁地抽象了网络层和传输层。所以不管是 IPv4 还是 IPv6,TCP 或者 UDP 都可以使用这个公用接口。 以下示例先使用 TCP 协议连接远程 80 端口,然后使用 UDP 协议连接,最后使用 TCP 协议连接 IPv6 地址: 示例 15.3 [dial.go](examples/chapter_15/dial.go) ```go // make a connection with www.example.org: package main import ( "fmt" "net" "os" ) func main() { conn, err := net.Dial("tcp", "192.0.32.10:80") // tcp ipv4 checkConnection(conn, err) conn, err = net.Dial("udp", "192.0.32.10:80") // udp checkConnection(conn, err) conn, err = net.Dial("tcp", "[2620:0:2d0:200::10]:80") // tcp ipv6 checkConnection(conn, err) } func checkConnection(conn net.Conn, err error) { if err != nil { fmt.Printf("error %v connecting!", err) os.Exit(1) } fmt.Printf("Connection is made with %v\n", conn) } ``` 下边也是一个使用 `net` 包从 socket 中打开,写入,读取数据的例子: 示例 15.4 [socket.go](examples/chapter_15/socket.go) ```go package main import ( "fmt" "io" "net" ) func main() { var ( host = "www.apache.org" port = "80" remote = host + ":" + port msg string = "GET / \n" data = make([]uint8, 4096) read = true count = 0 ) // 创建一个 socket con, err := net.Dial("tcp", remote) // 发送我们的消息,一个 http GET 请求 io.WriteString(con, msg) // 读取服务器的响应 for read { count, err = con.Read(data) read = (err == nil) fmt.Printf(string(data[0:count])) } con.Close() } ``` **练习 15.1** 编写新版本的客户端和服务器 ([client1.go](exercises/chapter_15/client1.go) / [server1.go](exercises/chapter_15/server1.go)): * 增加一个检查错误的函数 `checkError(error)`;讨论如下方案的利弊:为什么这个重构可能并没有那么理想?看看在 [示例 15.14](examples/chapter_15/template_validation.go) 中它是如何被解决的 * 使客户端可以通过发送一条命令 SH 来关闭服务器 * 让服务器可以保存已经连接的客户端列表(他们的名字);当客户端发送 `WHO` 指令的时候,服务器将显示如下列表: ``` This is the client list: 1:active, 0=inactive User IVO is 1 User MARC is 1 User CHRIS is 1 ``` 注意:当服务器运行的时候,你无法编译/连接同一个目录下的源码来产生一个新的版本,因为 `server.exe` 正在被操作系统使用而无法被替换成新的版本。 下边这个版本的 simple_tcp_server.go 从很多方面优化了第一个 tcp 服务器的示例 server.go 并且拥有更好的结构,它只用了 80 行代码! 示例 15.5 [simple_tcp_server.go](examples/chapter_15/simple_tcp_server.go): ```go // Simple multi-thread/multi-core TCP server. package main import ( "flag" "fmt" "net" "os" ) const maxRead = 25 func main() { flag.Parse() if flag.NArg() != 2 { panic("usage: host port") } hostAndPort := fmt.Sprintf("%s:%s", flag.Arg(0), flag.Arg(1)) listener := initServer(hostAndPort) for { conn, err := listener.Accept() checkError(err, "Accept: ") go connectionHandler(conn) } } func initServer(hostAndPort string) *net.TCPListener { serverAddr, err := net.ResolveTCPAddr("tcp", hostAndPort) checkError(err, "Resolving address:port failed: '"+hostAndPort+"'") listener, err := net.ListenTCP("tcp", serverAddr) checkError(err, "ListenTCP: ") println("Listening to: ", listener.Addr().String()) return listener } func connectionHandler(conn net.Conn) { connFrom := conn.RemoteAddr().String() println("Connection from: ", connFrom) sayHello(conn) for { var ibuf []byte = make([]byte, maxRead+1) length, err := conn.Read(ibuf[0:maxRead]) ibuf[maxRead] = 0 // to prevent overflow switch err { case nil: handleMsg(length, err, ibuf) case os.EAGAIN: // try again continue default: goto DISCONNECT } } DISCONNECT: err := conn.Close() println("Closed connection: ", connFrom) checkError(err, "Close: ") } func sayHello(to net.Conn) { obuf := []byte{'L', 'e', 't', '\'', 's', ' ', 'G', 'O', '!', '\n'} wrote, err := to.Write(obuf) checkError(err, "Write: wrote "+string(wrote)+" bytes.") } func handleMsg(length int, err error, msg []byte) { if length > 0 { print("<", length, ":") for i := 0; ; i++ { if msg[i] == 0 { break } fmt.Printf("%c", msg[i]) } print(">") } } func checkError(error error, info string) { if error != nil { panic("ERROR: " + info + " " + error.Error()) // terminate program } } ``` (**译者注:应该是由于 Go 版本的更新,会提示 os.EAGAIN undefined,修改后的代码:[simple_tcp_server_v1.go](examples/chapter_15/simple_tcp_server_v1.go)**) 都有哪些改进? * 服务器地址和端口不再是硬编码,而是通过命令行参数传入,并通过 `flag` 包来读取这些参数。这里使用了 `flag.NArg()` 检查是否按照期望传入了 2 个参数: ```go if flag.NArg() != 2 { panic("usage: host port") } ``` 传入的参数通过 `fmt.Sprintf()` 函数格式化成字符串 ```go hostAndPort := fmt.Sprintf("%s:%s", flag.Arg(0), flag.Arg(1)) ``` * 在 `initServer()` 函数中通过 `net.ResolveTCPAddr()` 得到了服务器地址和端口,这个函数最终返回了一个 `*net.TCPListener` * 每一个连接都会以协程的方式运行 `connectionHandler()` 函数。函数首先通过 `conn.RemoteAddr()` 获取到客户端的地址并显示出来 * 它使用 `conn.Write()` 发送 Go 推广消息给客户端 * 它使用一个 25 字节的缓冲读取客户端发送的数据并一一打印出来。如果读取的过程中出现错误,代码会进入 `switch` 语句 `default` 分支,退出无限循环并关闭连接。如果是操作系统的 `EAGAIN` 错误,它会重试。 * 所有的错误检查都被重构在独立的函数 `checkError` 中,当错误产生时,利用错误上下文来触发 panic。 在命令行中输入 `simple_tcp_server localhost 50000` 来启动服务器程序,然后在独立的命令行窗口启动一些 client.go 的客户端。当有两个客户端连接的情况下服务器的典型输出如下,这里我们可以看到每个客户端都有自己的地址: ``` E:\Go\GoBoek\code examples\chapter 14>simple_tcp_server localhost 50000 Listening to: 127.0.0.1:50000 Connection from: 127.0.0.1:49346 <25:Ivo says: Hi server, do y><12:ou hear me ?> Connection from: 127.0.0.1:49347 <25:Marc says: Do you remembe><25:r our first meeting serve><2:r?> ``` **net.Error:** `net` 包返回的错误类型遵循惯例为 `error`,但有些错误实现包含额外的方法,他们被定义为 `net.Error` 接口: ```go package net type Error interface { Timeout() bool // 错误是否超时 Temporary() bool // 是否是临时错误 } ``` 通过类型断言,客户端代码可以测试 `net.Error`,从而区分是临时发生的还是必然会出现的错误。举例来说,一个网络爬虫程序在遇到临时发生的错误时可能会休眠或者重试,如果是一个必然发生的错误,则他会放弃继续执行。 ```go // in a loop - some function returns an error err if nerr, ok := err.(net.Error); ok && nerr.Temporary() { time.Sleep(1e9) continue // try again } if err != nil { log.Fatal(err) } ``` ## 链接 - [目录](directory.md) - 上一节:[网络、模版与网页应用](15.0.md) - 下一节:[一个简单的网页服务器](15.2.md)