• Go Web---上


    Go Web---tcp服务器


    tcp 服务器

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

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

    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\n", string(buf[:len]))
    
    		//回显
    		conn.Write(buf)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    在 main() 中创建了一个 net.Listener 类型的变量 listener,他实现了服务器的基本功能:用来监听和接收来自客户端的请求(在 localhost 即 IP 地址为 127.0.0.1 端口为 50000 基于TCP协议)。

    Listen() 函数可以返回一个 error 类型的错误变量。用一个无限 for 循环的 listener.Accept() 来等待客户端的请求。

    客户端的请求将产生一个 net.Conn 类型的连接变量。然后一个独立的协程使用这个连接执行 doServerStuff(),开始使用一个 512 字节的缓冲 data 来读取客户端发送来的数据,并且把它们打印到服务器的终端,len 获取客户端发送的数据字节数;

    如果客户端发送数据大于512字节,那么服务端会分多次读取完毕。

    当客户端发送的所有数据都被读取完成时,协程就结束了。

    这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。

    客户端代码写在另一个文件 client.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')
    
    	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")
    
    		if trimmedInput == "Q" {
    			return
    		}
    
    		conn.Write([]byte(trimmedClient + " says: " + trimmedInput))
    
    		buf := make([]byte, 512)
    
    		num, err := conn.Read(buf)
    		if err != nil {
    			return
    		}
    
    		fmt.Printf("read from server : %s , data num : %d\n", string(buf), num)
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

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

    它通过无限循环从 os.Stdin 接收来自键盘的输入,直到输入了“Q”。注意裁剪 \r 和 \n 字符(仅 Windows 平台需要)。裁剪后的输入被 connection 的 Write 方法发送到服务器。

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

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

    在网络编程中 net.Dial 函数是非常重要的,一旦你连接到远程系统,函数就会返回一个 Conn 类型的接口,我们可以用它发送和接收数据。Dial 函数简洁地抽象了网络层和传输层。所以不管是 IPv4 还是 IPv6,TCP 或者 UDP 都可以使用这个公用接口。


    注意: conn.read默认会一直阻塞,直到有数据可读

    conn.Read(buf)
    
    • 1

    可以通过conn.SetReadDeadline(time.Now().Add(3 * time.Second))来设置读超时
    在这里插入图片描述


    以下示例先使用 TCP 协议连接远程 80 端口,然后使用 UDP 协议连接,最后使用 TCP 协议连接 IPv6 地址:

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

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

    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()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    优化版本

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

    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))
    	//初始化server
    	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(">")
    	}
    }
    
    //checkError 统一检查错误的方式
    func checkError(error error, info string) {
    	if error != nil {
    		panic("ERROR: " + info + " " + error.Error()) // terminate program
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    由于go版本的更新,会提示os.EAGAIN undefined

    都有哪些改进?

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

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

    hostAndPort := fmt.Sprintf("%s:%s", flag.Arg(0), flag.Arg(1))
    
    • 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?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    net.Error: net 包返回的错误类型遵循惯例为 error,但有些错误实现包含额外的方法,他们被定义为 net.Error 接口:

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

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

    // 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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  • 相关阅读:
    文字以打字样式展现形式
    Three.js 进阶之旅:物理效果-3D乒乓球小游戏 🏓
    Layui快速入门之第八节 表格渲染与属性的使用
    java学习第六步-常见类和api
    【OpenCV】红绿灯识别 轮廓识别 C++ OpenCV 案例实现
    Node.js 框架 star 星数量排名——NestJs跃居第二
    人工智能证书的作用
    阿里巴巴面试题- - -多线程&并发篇(三十六)
    hadoop复习
    【C++】运算符重载 ⑥ ( 一元运算符重载 | 后置运算符重载 | 前置运算符重载 与 后置运算符重载 的区别 | 后置运算符重载添加 int 占位参数 )
  • 原文地址:https://blog.csdn.net/m0_53157173/article/details/126389824