这部分我们将使用 TCP 协议和在 14 章讲到的协程范式编写一个简单的客户端 - 服务器应用,一个(web)服务器应用需要响应众多客户端的并发请求:go 会为每一个客户端产生一个协程用来处理请求。我们需要使用 net 包中网络通信的功能。它包含了用于 TCP/IP 以及 UDP 协议、域名解析等方法。

    服务器代码,单独的一个文件:

    示例 15.1 server.go

    1. package main
    2. import (
    3. "fmt"
    4. "net"
    5. )
    6. func main() {
    7. fmt.Println("Starting the server ...")
    8. // 创建 listener
    9. listener, err := net.Listen("tcp", "localhost:50000")
    10. if err != nil {
    11. fmt.Println("Error listening", err.Error())
    12. return //终止程序
    13. }
    14. // 监听并接受来自客户端的连接
    15. for {
    16. conn, err := listener.Accept()
    17. if err != nil {
    18. fmt.Println("Error accepting", err.Error())
    19. return // 终止程序
    20. }
    21. go doServerStuff(conn)
    22. }
    23. }
    24. func doServerStuff(conn net.Conn) {
    25. for {
    26. buf := make([]byte, 512)
    27. len, err := conn.Read(buf)
    28. if err != nil {
    29. fmt.Println("Error reading", err.Error())
    30. return //终止程序
    31. }
    32. fmt.Printf("Received data: %v", string(buf[:len]))
    33. }
    34. }

    我们在 main() 创建了一个 net.Listener 的变量,他是一个服务器的基本函数:用来监听和接收来自客户端的请求(来自 localhost 即 IP 地址为 127.0.0.1 端口为 50000 基于 TCP 协议)。这个 Listen() 函数可以返回一个 error 类型的错误变量。用一个无限 for 循环的 listener.Accept() 来等待客户端的请求。客户端的请求将产生一个 net.Conn 类型的连接变量。然后一个独立的协程使用这个连接执行 doServerStuff(),开始使用一个 512 字节的缓冲 data 来读取客户端发送来的数据并且把它们打印到服务器的终端,len 获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。

    客户端代码写在另外一个文件 client.go 中:

    示例 15.2 client.go

    1. package main
    2. import (
    3. "bufio"
    4. "fmt"
    5. "net"
    6. "os"
    7. "strings"
    8. )
    9. func main() {
    10. //打开连接:
    11. conn, err := net.Dial("tcp", "localhost:50000")
    12. if err != nil {
    13. //由于目标计算机积极拒绝而无法创建连接
    14. fmt.Println("Error dialing", err.Error())
    15. return // 终止程序
    16. }
    17. inputReader := bufio.NewReader(os.Stdin)
    18. fmt.Println("First, what is your name?")
    19. clientName, _ := inputReader.ReadString('\n')
    20. // fmt.Printf("CLIENTNAME %s", clientName)
    21. trimmedClient := strings.Trim(clientName, "\r\n") // Windows 平台下用 "\r\n",Linux平台下使用 "\n"
    22. // 给服务器发送信息直到程序退出:
    23. for {
    24. fmt.Println("What to send to the server? Type Q to quit.")
    25. input, _ := inputReader.ReadString('\n')
    26. trimmedInput := strings.Trim(input, "\r\n")
    27. // fmt.Printf("input:--s%--", input)
    28. // fmt.Printf("trimmedInput:--s%--", trimmedInput)
    29. if trimmedInput == "Q" {
    30. return
    31. }
    32. _, err = conn.Write([]byte(trimmedClient + " says: " + trimmedInput))
    33. }
    34. }

    客户端通过 net.Dial 创建了一个和服务器之间的连接

    它通过无限循环中的 os.Stdin 接收来自键盘的输入直到输入了 “Q”。注意使用 \r\n 换行符分割字符串(在 windows 平台下使用 \r\n)。接下来分割后的输入通过 connectionWrite 方法被发送到服务器。

    当然,服务器必须先启动好,如果服务器并未开始监听,客户端是无法成功连接的。

    如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息:对tcp 127.0.0.1:50000发起连接时产生错误:由于目标计算机的积极拒绝而无法创建连接

    打开控制台并转到服务器和客户端可执行程序所在的目录,Windows 系统下输入 server.exe(或者只输入 server),Linux 系统下输入 ./server。

    接下来控制台出现以下信息:Starting the server ...

    在 Windows 系统中,我们可以通过 CTRL/C 停止程序。

    然后开启 2 个或者 3 个独立的控制台窗口,然后分别输入 client 回车启动客户端程序

    以下是服务器的输出:

    1. Starting the Server ...
    2. Received data: IVO says: Hi Server, what's up ?
    3. Received data: CHRIS says: Are you busy server ?
    4. Received data: MARC says: Don't forget our appointment tomorrow !

    当客户端输入 Q 并结束程序时,服务器会输出以下信息:

    1. 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

    1. // make a connection with www.example.org:
    2. package main
    3. import (
    4. "fmt"
    5. "net"
    6. "os"
    7. )
    8. func main() {
    9. conn, err := net.Dial("tcp", "192.0.32.10:80") // tcp ipv4
    10. checkConnection(conn, err)
    11. conn, err = net.Dial("udp", "192.0.32.10:80") // udp
    12. checkConnection(conn, err)
    13. conn, err = net.Dial("tcp", "[2620:0:2d0:200::10]:80") // tcp ipv6
    14. checkConnection(conn, err)
    15. }
    16. func checkConnection(conn net.Conn, err error) {
    17. if err != nil {
    18. fmt.Printf("error %v connecting!", err)
    19. os.Exit(1)
    20. }
    21. fmt.Printf("Connection is made with %v\n", conn)
    22. }

    下边也是一个使用 net 包从 socket 中打开,写入,读取数据的例子:

    示例 15.4 socket.go

    1. package main
    2. import (
    3. "fmt"
    4. "io"
    5. "net"
    6. )
    7. func main() {
    8. var (
    9. host = "www.apache.org"
    10. port = "80"
    11. remote = host + ":" + port
    12. msg string = "GET / \n"
    13. data = make([]uint8, 4096)
    14. read = true
    15. count = 0
    16. )
    17. // 创建一个socket
    18. con, err := net.Dial("tcp", remote)
    19. // 发送我们的消息,一个http GET请求
    20. io.WriteString(con, msg)
    21. // 读取服务器的响应
    22. for read {
    23. count, err = con.Read(data)
    24. read = (err == nil)
    25. fmt.Printf(string(data[0:count]))
    26. }
    27. con.Close()
    28. }

    练习 15.1

    编写新版本的客户端和服务器(client1.go / server1.go):

    • 增加一个检查错误的函数 checkError(error);讨论如下方案的利弊:为什么这个重构可能并没有那么理想?看看在示例 15.14 中它是如何被解决的
    • 使客户端可以通过发送一条命令 SH 来关闭服务器
    • 让服务器可以保存已经连接的客户端列表(他们的名字);当客户端发送 WHO 指令的时候,服务器将显示如下列表:
    1. This is the client list: 1:active, 0=inactive
    2. User IVO is 1
    3. User MARC is 1
    4. User CHRIS is 1

    注意:当服务器运行的时候,你无法编译 / 连接同一个目录下的源码来产生一个新的版本,因为 server.exe 正在被操作系统使用而无法被替换成新的版本。

    下边这个版本的 simple_tcp_server.go 从很多方面优化了第一个 tcp 服务器的示例 server.go 并且拥有更好的结构,它只用了 80 行代码!

    示例 15.5 simple_tcp_server.go

    1. // Simple multi-thread/multi-core TCP server.
    2. package main
    3. import (
    4. "flag"
    5. "fmt"
    6. "net"
    7. "os"
    8. )
    9. const maxRead = 25
    10. func main() {
    11. flag.Parse()
    12. if flag.NArg() != 2 {
    13. panic("usage: host port")
    14. }
    15. hostAndPort := fmt.Sprintf("%s:%s", flag.Arg(0), flag.Arg(1))
    16. listener := initServer(hostAndPort)
    17. for {
    18. conn, err := listener.Accept()
    19. checkError(err, "Accept: ")
    20. go connectionHandler(conn)
    21. }
    22. }
    23. func initServer(hostAndPort string) *net.TCPListener {
    24. serverAddr, err := net.ResolveTCPAddr("tcp", hostAndPort)
    25. checkError(err, "Resolving address:port failed: '"+hostAndPort+"'")
    26. listener, err := net.Listen("tcp", serverAddr)
    27. checkError(err, "ListenTCP: ")
    28. println("Listening to: ", listener.Addr().String())
    29. return listener
    30. }
    31. func connectionHandler(conn net.Conn) {
    32. connFrom := conn.RemoteAddr().String()
    33. println("Connection from: ", connFrom)
    34. sayHello(conn)
    35. for {
    36. var ibuf []byte = make([]byte, maxRead+1)
    37. length, err := conn.Read(ibuf[0:maxRead])
    38. ibuf[maxRead] = 0 // to prevent overflow
    39. switch err {
    40. case nil:
    41. handleMsg(length, err, ibuf)
    42. case os.EAGAIN: // try again
    43. continue
    44. default:
    45. goto DISCONNECT
    46. }
    47. }
    48. DISCONNECT:
    49. err := conn.Close()
    50. println("Closed connection: ", connFrom)
    51. checkError(err, "Close: ")
    52. }
    53. func sayHello(to net.Conn) {
    54. obuf := []byte{'L', 'e', 't', '\'', 's', ' ', 'G', 'O', '!', '\n'}
    55. wrote, err := to.Write(obuf)
    56. checkError(err, "Write: wrote "+string(wrote)+" bytes.")
    57. }
    58. func handleMsg(length int, err error, msg []byte) {
    59. if length > 0 {
    60. print("<", length, ":")
    61. for i := 0; ; i++ {
    62. if msg[i] == 0 {
    63. break
    64. }
    65. fmt.Printf("%c", msg[i])
    66. }
    67. print(">")
    68. }
    69. }
    70. func checkError(error error, info string) {
    71. if error != nil {
    72. panic("ERROR: " + info + " " + error.Error()) // terminate program
    73. }
    74. }

    (译者注:应该是由于 go 版本的更新,会提示 os.EAGAIN undefined , 修改后的代码:simple_tcp_server_v1.go)

    都有哪些改进?

    • 服务器地址和端口不再是硬编码,而是通过命令行传入参数并通过 flag 包来读取这些参数。这里使用了 flag.NArg() 检查是否按照期望传入了 2 个参数:
    1. if flag.NArg() != 2{
    2. panic("usage: host port")
    3. }

    传入的参数通过 fmt.Sprintf 函数格式化成字符串

    1. hostAndPort := fmt.Sprintf("%s:%s", flag.Arg(0), flag.Arg(1))
    • initServer 函数中通过 net.ResolveTCPAddr 指定了服务器地址和端口,这个函数最终返回了一个 *net.TCPListener
    • 每一个连接都会以协程的方式运行 connectionHandler 函数。这些开始于当通过 conn.RemoteAddr() 获取到客户端的地址
    • 它使用 conn.Write 发送改进的 go-message 给客户端
    • 它使用一个 25 字节的缓冲读取客户端发送的数据并一一打印出来。如果读取的过程中出现错误,代码会进入 switch 语句的 default 分支关闭连接。如果是操作系统的 EAGAIN 错误,它会重试。
    • 所有的错误检查都被重构在独立的函数 ‘checkError’ 中,用来分发出现的上下文错误。

    在命令行中输入 simple_tcp_server localhost 50000 来启动服务器程序,然后在独立的命令行窗口启动一些 client.go 的客户端。当有两个客户端连接的情况下服务器的典型输出如下,这里我们可以看到每个客户端都有自己的地址:

    1. E:\Go\GoBoek\code examples\chapter 14>simple_tcp_server localhost 50000
    2. Listening to: 127.0.0.1:50000
    3. Connection from: 127.0.0.1:49346
    4. <25:Ivo says: Hi server, do y><12:ou hear me ?>
    5. Connection from: 127.0.0.1:49347
    6. <25:Marc says: Do you remembe><25:r our first meeting serve><2:r?>

    net.Error:
    这个 net 包返回错误的错误类型,下边是约定的写法,不过 net.Error 接口还定义了一些其他的错误实现,有些额外的方法。

    1. package net
    2. type Error interface{
    3. Timeout() bool // 错误是否超时
    4. Temporary() bool // 是否是临时错误
    5. }

    通过类型断言,客户端代码可以用来测试 net.Error,从而区分哪些临时发生的错误或者必然会出现的错误。举例来说,一个网络爬虫程序在遇到临时发生的错误时可能会休眠或者重试,如果是一个必然发生的错误,则他会放弃继续执行。

    1. // in a loop - some function returns an error err
    2. if nerr, ok := err.(net.Error); ok && nerr.Temporary(){
    3. time.Sleep(1e9)
    4. continue // try again
    5. }
    6. if err != nil{
    7. log.Fatal(err)
    8. }