15.1 tcp服务器

这部分我们将使用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. }

链接