Go简单的网络编程 | 青训营笔记

79 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第8天

OSI七层模型简化

物理层

双绞线, 光纤,无线电波等..规定了网络的电气特性

数据链路层

单纯的0,1并无意义,按照以太网Ethernet的协议规定,一组电信号组成一个数据包,帧Frame:标头Head和数据Data

标头固定18字节; 数据最短46,最长1500. 帧在64~1518字节之间

数据包根据MAC地址为发送地址和接收地址 ,长度为48个二进制位,即12位十六进制, 广播发送

网络层

由于以太网广播的缺陷,设置了网络层, 先区分子网 ,再在子网内广播.

网络层出现后,计算机又有了一个网络地址,规定的协议就是IP协议

IP数据包标头20~60字节 ,总长度最大65535字节

传输层

区分某个数据包时哪个程序的,就需要一个参数 ,即端口port(065535 ,16位二进制,01023是系统端口)

因此必须在数据包加入端口信息, 需要新的协议 ,最简单的是UDP协议,几乎就是在数据前加上端口号

UDP包标头主要是发出端口和接收端口,数据就是具体内容 ,标头8字节,总长最大65535字节,正好是一个IP包

为了提高网络可靠性,TCP协议诞生了

应用层

数据接收后解包,应用层就是规定应用程序使用的数据格式 ,如TCP协议之上的Email,HTTP,FTP等


socket 套接字

Socket是应用层与TCP/IP协议族通信的中间软件抽象层, 把复杂的TCP/IP协议族隐藏在Socket后面,对用户来说只需要调用Socket规定的相关函数,让Socket去组织符合指定的协议数据然后进行通信。

Go语言的TCP通信

服务端server

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

  1. 监听端口

  2. 接收客户端请求建立链接

  3. 创建goroutine处理链接。

// tcp/server/main.go

// TCP server端

// 处理函数
func process(conn net.Conn) {
        defer conn.Close() // 关闭连接
        for {
                reader := bufio.NewReader(conn)
                var buf [128]byte
                n, err := reader.Read(buf[:]) // 读取数据
                if err != nil {
                        fmt.Println("read from client failed, err:", err)
                        break
                }
                recvStr := string(buf[:n])
                fmt.Println("收到client端发来的数据:", recvStr)
                conn.Write([]byte(recvStr)) // 发送数据
        }}
func main() {
        listen, err := net.Listen("tcp", "127.0.0.1:20000")
        if err != nil {
                fmt.Println("listen failed, err:", err)
                return
        }
        for {
                conn, err := listen.Accept() // 建立连接
                if err != nil {
                        fmt.Println("accept failed, err:", err)
                        continue
                }
                go process(conn) // 启动一个goroutine处理连接
        }}

客户端

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

  1. 建立与服务端的链接

  2. 进行数据收发

  3. 关闭链接

// tcp/client/main.go
func main() {
        conn, err := net.Dial("tcp", "127.0.0.1:20000")
        if err != nil {
                fmt.Println("err :", err)
                return
        }
        defer conn.Close() // 关闭连接
        inputReader := bufio.NewReader(os.Stdin)
        for {
                input, _ := inputReader.ReadString('\n') // 读取用户输入
                inputInfo := strings.Trim(input, "\r\n")
                if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
                        return
                }
                _, err = conn.Write([]byte(inputInfo)) // 发送数据
                if err != nil {
                        return
                }
                buf := [512]byte{}
                n, err := conn.Read(buf[:])
                if err != nil {
                        fmt.Println("recv failed, err:", err)
                        return
                }
                fmt.Println(string(buf[:n]))
        }}

TCP粘包

TCP数据传递是流模式,“粘包”可发生在发送端也可发生在接收端:

  1. 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。

  2. 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。

解决方法

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

封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。

包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

// socket_stick/proto/proto.gopackage proto

//数据包前4字节为包头,储存为发送数据的长度

import (
        "bufio"
        "bytes"
        "encoding/binary")

// Encode 将消息编码
func Encode(message string) ([]byte, error) {
        // 读取消息的长度,转换成int32类型(占4个字节)
        var length = int32(len(message))
        var pkg = new(bytes.Buffer)
        // 写入消息头
        err := binary.Write(pkg, binary.LittleEndian, length)
        if err != nil {
                return nil, err
        }
        // 写入消息实体
        err = binary.Write(pkg, binary.LittleEndian, []byte(message))
        if err != nil {
                return nil, err
        }
        return pkg.Bytes(), nil}
        
// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
        // 读取消息的长度
        lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
        lengthBuff := bytes.NewBuffer(lengthByte)
        var length int32
        err := binary.Read(lengthBuff, binary.LittleEndian, &length)
        if err != nil {
                return "", err
        }
        // Buffered返回缓冲中现有的可读取的字节数。
        if int32(reader.Buffered()) < length+4 {
                return "", err
        }
        // 读取真正的消息数据
        pack := make([]byte, int(4+length))
        _, err = reader.Read(pack)
        if err != nil {
                return "", err
        }
        return string(pack[4:]), nil}

Go的UDP通信

服务端(net包实现)

// UDP/server/main.go
func main() {
        listen, err := net.ListenUDP("udp", &net.UDPAddr{
                IP:   net.IPv4(0, 0, 0, 0),
                Port: 30000,
        })
        if err != nil {
                fmt.Println("listen failed, err:", err)
                return
        }
        defer listen.Close()
        for {
                var data [1024]byte
                n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
                if err != nil {
                        fmt.Println("read udp failed, err:", err)
                        continue
                }
                fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
                _, err = listen.WriteToUDP(data[:n], addr) // 发送数据
                if err != nil {
                        fmt.Println("write to udp failed, err:", err)
                        continue
                }
        }}

客户端

func main() {
        socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
                IP:   net.IPv4(0, 0, 0, 0),
                Port: 30000,
        })
        if err != nil {
                fmt.Println("连接服务端失败,err:", err)
                return
        }
        defer socket.Close()
        sendData := []byte("Hello server")
        _, err = socket.Write(sendData) // 发送数据
        if err != nil {
                fmt.Println("发送数据失败,err:", err)
                return
        }
        data := make([]byte, 4096)
        n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
        if err != nil {
                fmt.Println("接收数据失败,err:", err)
                return
        }
        fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)}