美文网首页
【go语言学习】网络编程之TCP

【go语言学习】网络编程之TCP

作者: Every_dawn | 来源:发表于2020-10-08 21:06 被阅读0次

    一、go语言实现TCP通信

    TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。

    TCP通信的实现:

    1、socket 编程是对 tcp 通讯过程的封装,unix server 端网络编程过程为 Server->Bind->Listen->Accept,go 中直接使用 Listen + Accept
    2、client 与客户端建立好的请求可以被新建的 goroutine(go func) 处理 named connHandler
    3、goroutine 的处理过程其实是输入流/输出流的应用场景

    下面以一个简单的需求来实现go语言的TCP通信:

    socket编程实现客户端client服务端server进行通讯,通讯测试场景:
    1、client 发送 hello, server 返回 world
    2、client 发送 你好, server 返回 世界
    3、其余client发送内容, server 回显即可
    4、client 发送 exit,客户端退出

    二、TCP服务端

    TCP服务端程序的处理流程:

    1、监听端口
    2、接收客户端请求建立链接
    3、创建goroutine处理链接

    我们使用Go语言的net包实现的TCP服务端代码如下:

    // tcp\server\main.go
    package main
    
    import (
        "fmt"
        "net"
        "strings"
    )
    
    func main() {
        // 1.建立服务,监听端口
        listener, err := net.Listen("tcp", "127.0.0.1:3000")
        if err != nil {
            fmt.Println("net listen failed err: ", err)
            return
        }
        // 打印一句提示信息
        fmt.Println("listen at 127.0.0.1:3000")
        for {
            // 2.接收来自client的连接,一直阻塞直到有连接
            conn, err := listener.Accept()
            if err != nil {
                fmt.Println("listener accept failed err: ", err)
                return
            }
            // 3.起一个协程,处理来自客户端的连接(收发数据)
            go sConnHandler(conn)
        }
    }
    
    // 服务端处理连接函数
    func sConnHandler(c net.Conn) {
        // 关闭连接
        defer c.Close()
        // 1.判断conn是否有效
        if c == nil {
            fmt.Println("无效的连接。")
            return
        }
        // 2.存储接收到的数据
        buf := make([]byte, 1024*4)
        // 3.循环读取客户端发送的数据
        for {
            // 3.1 客户端发送的数据读入buf
            cn, err := c.Read(buf)
            // 3.2 数据读尽,发生错误 关闭连接
            if err != nil {
                return
            }
            // 3.3 根据接收的数据,进行逻辑处理
            // 3.3.1 buf数据去除两端空格
            inStr := strings.TrimSpace(string(buf[:cn]))
            fmt.Printf("来自%v客户端输入:%v\n", c.RemoteAddr(), inStr)
            // 3.3.2 switch选择结构处理
            switch inStr {
            case "hello":
                c.Write([]byte("world"))
            case "你好":
                c.Write([]byte("世界"))
            default:
                c.Write([]byte(inStr))
            }
        }
    }
    

    将上面的代码保存之后编译成server或server.exe可执行文件。

    三、TCP客户端

    一个TCP客户端进行TCP通信的流程如下:

    1、建立与服务端的链接
    2、进行数据收发
    3、关闭链接

    使用Go语言的net包实现的TCP客户端代码如下:

    // tcp\client\main.go
    package main
    
    import (
        "bufio"
        "fmt"
        "net"
        "os"
        "strings"
    )
    
    func main() {
        // 1.客户端发起连接
        conn, err := net.Dial("tcp", "127.0.0.1:3000")
        if err != nil {
            fmt.Println("net dial failed err: ", err)
            return
        }
        // 打印一句提示信息
        fmt.Println("dial 127.0.0.1:3000 success")
        // 2.处理连接
        cConnHandler(conn)
    }
    
    // 客户端处理连接函数
    func cConnHandler(c net.Conn) {
        // 关闭连接
        defer c.Close()
        // 1.接收控制台输入
        reader := bufio.NewReader(os.Stdin)
        // 2.缓存conn中的数据
        buf := make([]byte, 1024*4)
        //3.循环读写
        for {
            // 3.1 客户端输入
        label:
            input, _ := reader.ReadString('\n')
            // 去除两端空格
            input = strings.TrimSpace(input)
            // 处理无效输入
            if input == "" {
                fmt.Println("信息无效,请重新输入")
                goto label
            }
            // 3.2 输入exit就断开连接
            if strings.ToLower(input) == "exit" {
                fmt.Println("客户端断开连接")
                return
            }
            // 3.3 发送数据:客户端数据写入conn并传输
            _, err := c.Write([]byte(input))
            if err != nil {
                fmt.Println("c write failed err: ", err)
                return
            }
            // 3.4 接收数据:接收服务端返回的数据存入buf
            n, err := c.Read(buf)
            if err != nil {
                fmt.Println("c read failed err: ", err)
                return
            }
            // 3.5 显示服务端回传的数据
            fmt.Println("服务端返回:", string(buf[:n]))
        }
    }
    

    将上面的代码编译成client或client.exe可执行文件,先启动server端再启动client端,在client端输入任意内容回车之后就能够在server端看到client端发送的数据,并能够在client端显示server端返回的数据,从而实现TCP通信。

    四、TCP粘包问题

    1、粘包的定义:
    • 粘包是指网络通信中,发送方发送的多个数据包在接收方的缓冲区粘在一起,多个数据包首尾相连的现象。
    • 例如,基于tcp的socket实现的客户端向服务端上传文件时,内容往往是按照一段一段的字节流发送的,如果不做任何处理,从接收方来看,根本不知道该文件的字节流从何处开始,在何处结束。
    • 因此,所谓粘包问题主要是因为接收方不知道消息的边界,不知道一次提取多少个字节的数据造成的。
    2、产生原因

    粘包可发生在发送端也可发生在接收端:

    • 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
    • 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。

    UDP协议不会出现粘包:因为UDP是无连接,面向消息,提供高效服务的。无连接意味着当有数据要发送时,UDP会立即发送,数据包不会积压;面向消息意味着数据包一般很小,因此接收端处理也不会很耗时,一般不会由于接收端来不及处理消息而造成粘包。最重要的是,UDP不使用合并优化算法,每个消息都有单独的包头,即使出现很短时间内收到多个数据包的情况,接收方也能根据包头信息区分数据的边界。因此,UDP不会出现粘包,只可能会出现丢包。

    粘包示例代码:
    服务端代码:

    // tcp\server\main.go
    
    package main
    
    import (
        "fmt"
        "io"
        "net"
    )
    
    func main() {
        // 1.建立服务,监听端口
        listener, err := net.Listen("tcp", "127.0.0.1:3000")
        if err != nil {
            fmt.Println("net listen failed, err:", err)
            return
        }
        // 打印一句提示信息
        fmt.Println("服务器建立成功,监听端口:127.0.0.1:3000")
        for {
            // 2.监听来自客户端的连接
            conn, err := listener.Accept()
            if err != nil {
                fmt.Println("listener accept failed, err:", err)
                return
            }
            // 打印一句提示信息
            fmt.Printf("监听到来自%v的连接\n", conn.RemoteAddr())
            // 3.起一个协程,处理该连接,收发数据
            go sConnHandler(conn)
        }
    }
    
    // sConnHandler 服务端处理连接
    func sConnHandler(c net.Conn) {
        if c == nil {
            fmt.Println("无效的连接")
            return
        }
        for {
            data := make([]byte, 1024)
            n, err := c.Read(data[:])
            if err == io.EOF {
                break
            }
            if err != nil {
                fmt.Println("c read failed, err:", err)
                break
            }
            fmt.Println("来自客户端的消息:", string(data[:n]))
        }
    }
    

    客户端代码

    // tcp\client\
    package main
    
    import (
        "fmt"
        "net"
    )
    
    func main() {
        // 1.与服务端建立连接
        conn, err := net.Dial("tcp", "127.0.0.1:3000")
        if err != nil {
            fmt.Println("net dial failed, err:", err)
            return
        }
        // 打印一句提示信息
        fmt.Println("与服务器连接成功,端口号为:127.0.0.1:3000")
        // 2.处理连接
        cConnHandler(conn)
    }
    
    func cConnHandler(c net.Conn) {
        defer c.Close()
        for i := 0; i < 20; i++ {
            msg := "人生苦短,let`s go"
            _, err := c.Write([]byte(msg))
            if err != nil {
                fmt.Println("c write failed, err:", err)
                return
            }
        }
    }
    

    先启动服务端再启动客户端,可以看到服务端输出结果如下:

    服务器建立成功,监听端口:127.0.0.1:3000
    监听到来自127.0.0.1:1547的连接
    来自客户端的消息: 人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短 
    ,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    
    3、粘包的解决

    出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。

    封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

    我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。

    // tcp\proto\proto.go
    
    package proto
    
    import (
        "bufio"
        "bytes"
        "encoding/binary"
        "errors"
        "fmt"
    )
    
    // 这是一个自定义协议包,里面提供了两个工具函数:Encode和Decode
    // 这两个函数用于对收发数据进行编解码处理
    // 每次发送的数据包前4个字节用来记录数据的长度,后面才是记录数据
    // 这样一方面可以准确获知应该读取的长度,并且可以防止粘包现象的发生
    
    // Encode 编码
    func Encode(msg string) ([]byte, error) {
        // 1.获取消息长度,转换成int32类型(占4个字节)
        msgLength := int32(len(msg))
        // 2.创建一个数据包,用于存放数据
        dataBuf := bytes.NewBuffer([]byte{})
        // 3.将消息长度写入消息头
        err := binary.Write(dataBuf, binary.BigEndian, msgLength)
        if err != nil {
            fmt.Println("binary write failed, err: ", err)
        }
        // 4.将消息内容写入dataBuf
        err = binary.Write(dataBuf, binary.BigEndian, []byte(msg))
        if err != nil {
            fmt.Println("binary write failed, err: ", err)
            return nil, err
        }
        return dataBuf.Bytes(), nil
    }
    
    // Decode 解码
    func Decode(reader *bufio.Reader) (string, error) {
        // 1.读取前4个字节的数据,表示数据包长度的信息
        lengthData := make([]byte, 4)
        _, err := reader.Read(lengthData)
        if err != nil {
            fmt.Println("reader read failed, err:", err)
            return "", err
        }
        // 2.将前4个字节数据读入字节缓冲区
        lengthBuf := bytes.NewBuffer(lengthData)
        // 3.读取数据包长度
        var msgLength int32
        err = binary.Read(lengthBuf, binary.BigEndian, &msgLength)
        if err != nil {
            fmt.Println("binary read failed, err:", err)
            return "", nil
        }
        // 4.判断数据包的长度是否合法
        if int32(reader.Buffered()) < msgLength {
            return "", errors.New("数据长度不合法")
        }
        // 5.读取消息内容
        msgData := make([]byte, int(msgLength))
        _, err = reader.Read(msgData)
        if err != nil {
            return "", nil
        }
        msgBuffer := bytes.NewBuffer(msgData)
        var msg string
        err = binary.Read(msgBuffer, binary.BigEndian, msgData)
        if err != nil {
            fmt.Println("binary read failed, err:", err)
            return "", err
        }
        msg = string(msgData)
        return msg, nil
    }
    

    接下来在服务端和客户端分别使用上面定义的proto包的Decode和Encode函数处理数据。

    服务端代码如下:

    // tcp\server\main.go
    
    package main
    
    import (
        "bufio"
        "fmt"
        "go_project/tcp/proto"
        "net"
    )
    
    func main() {
        // 1.建立服务,监听端口
        listener, err := net.Listen("tcp", "127.0.0.1:3000")
        if err != nil {
            fmt.Println("net listen failed, err:", err)
            return
        }
        // 打印一句提示信息
        fmt.Println("服务器建立成功,监听端口:127.0.0.1:3000")
        for {
            // 2.监听来自客户端的连接
            conn, err := listener.Accept()
            if err != nil {
                fmt.Println("listener accept failed, err:", err)
                return
            }
            // 打印一句提示信息
            fmt.Printf("监听到来自%v的连接\n", conn.RemoteAddr())
            // 3.起一个协程,处理该连接,收发数据
            go sConnHandler(conn)
        }
    }
    
    // sConnHandler 服务端处理连接
    func sConnHandler(c net.Conn) {
        if c == nil {
            fmt.Println("无效的连接")
            return
        }
        reader := bufio.NewReader(c)
        for {
            msg, err := proto.Decode(reader)
            if err != nil {
                fmt.Println("proto decode failed, err:", err)
                return
            }
            fmt.Println("来自客户端的消息:", msg)
        }
    }
    

    客户端代码如下:

    // tcp\client\main.go
    
    package main
    
    import (
        "fmt"
        "go_project/tcp/proto"
        "net"
    )
    
    func main() {
        // 1.与服务端建立连接
        conn, err := net.Dial("tcp", "127.0.0.1:3000")
        if err != nil {
            fmt.Println("net dial failed, err:", err)
            return
        }
        // 打印一句提示信息
        fmt.Println("与服务器连接成功,端口号为:127.0.0.1:3000")
        // 2.处理连接
        cConnHandler(conn)
    }
    
    func cConnHandler(c net.Conn) {
        defer c.Close()
        for i := 0; i < 20; i++ {
            msg := "人生苦短,let`s go"
            data, err := proto.Encode(msg)
            if err != nil {
                fmt.Println("proto encode failed, err:", err)
                return
            }
            _, err = c.Write(data)
            if err != nil {
                fmt.Println("c write failed, err:", err)
                return
            }
        }
    }
    

    先启动服务端再启动客户端,可以看到服务端输出结果如下:

    服务器建立成功,监听端口:127.0.0.1:3000
    监听到来自127.0.0.1:1367的连接
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    来自客户端的消息: 人生苦短,let`s go
    reader read failed, err: EOF
    proto decode failed, err: EOF
    

    参考文章
    https://www.liwenzhou.com/posts/Go/15_socket/

    相关文章

      网友评论

          本文标题:【go语言学习】网络编程之TCP

          本文链接:https://www.haomeiwen.com/subject/mirxpktx.html