MST

星途 面试题库

面试题:Go语言跨网络数据发送与接收模拟及优化

假设你需要模拟一个基于Go语言的简单网络通信场景,一端发送数据,另一端接收数据。要求考虑网络延迟、丢包等现实因素,并且实现数据的可靠传输。阐述你的设计思路,包括数据结构、发送与接收逻辑,以及可能用到的优化策略,最后给出核心的示例代码。
28.4万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

设计思路

  1. 数据结构
    • 定义一个结构体来封装要发送的数据,例如包含数据内容字段以及用于确认的数据编号字段。
    type Packet struct {
        DataID  int
        Content []byte
    }
    
    • 为了处理网络延迟和丢包,需要维护发送和接收的状态。在发送端,可使用一个 map 来记录已发送但未确认的数据包,键为 DataID,值为 Packet 结构体。在接收端,可使用一个切片来存储接收到但顺序不正确的数据包,等待正确顺序的数据到来后再一起处理。
  2. 发送逻辑
    • 发送端按顺序发送数据包,并启动一个定时器,每个数据包发送后开始计时。如果在规定时间内没有收到确认消息,重发该数据包。
    • 发送函数如下:
    func sendData(conn net.Conn, data []byte, sendChan chan struct{}) {
        var dataID int
        unacknowledged := make(map[int]Packet)
        for {
            select {
            case <-sendChan:
                packet := Packet{DataID: dataID, Content: data}
                _, err := conn.Write(encodePacket(packet))
                if err != nil {
                    log.Println("Send error:", err)
                    return
                }
                unacknowledged[dataID] = packet
                dataID++
                go func(id int) {
                    timer := time.NewTimer(time.Second)
                    defer timer.Stop()
                    select {
                    case <-timer.C:
                        if _, ok := unacknowledged[id]; ok {
                            log.Println("Resending packet:", id)
                            _, err := conn.Write(encodePacket(unacknowledged[id]))
                            if err != nil {
                                log.Println("Resend error:", err)
                            }
                        }
                    }
                }(dataID - 1)
            }
        }
    }
    
  3. 接收逻辑
    • 接收端持续读取数据,解析接收到的数据包。如果数据包编号正确,将数据传递给上层应用处理,并发送确认消息;如果编号不正确,将数据包暂存到切片中。当接收到正确编号的数据包时,检查暂存切片中是否有后续顺序的数据包,如有则一起处理。
    • 接收函数如下:
    func receiveData(conn net.Conn, receiveChan chan []byte) {
        expectedID := 0
        outOfOrder := make([]Packet, 0)
        for {
            buffer := make([]byte, 1024)
            n, err := conn.Read(buffer)
            if err != nil {
                log.Println("Receive error:", err)
                return
            }
            packet := decodePacket(buffer[:n])
            if packet.DataID == expectedID {
                receiveChan <- packet.Content
                expectedID++
                for len(outOfOrder) > 0 && outOfOrder[0].DataID == expectedID {
                    receiveChan <- outOfOrder[0].Content
                    outOfOrder = outOfOrder[1:]
                    expectedID++
                }
                _, err := conn.Write(encodeAck(packet.DataID))
                if err != nil {
                    log.Println("Ack send error:", err)
                }
            } else {
                outOfOrder = append(outOfOrder, packet)
            }
        }
    }
    
  4. 优化策略
    • 批量发送:可以将多个小数据包合并成一个大数据包发送,减少网络传输次数,降低延迟。
    • 滑动窗口:在发送端维护一个窗口,窗口内的数据包可以无需等待确认就发送,提高发送效率。同时,接收端也相应调整处理逻辑。
    • 拥塞控制:根据网络拥塞情况动态调整发送速率,避免网络拥塞导致更多丢包。

核心示例代码

以下是完整的示例代码,包括数据包编码解码、确认消息处理等:

package main

import (
    "encoding/binary"
    "fmt"
    "log"
    "net"
    "time"
)

type Packet struct {
    DataID  int
    Content []byte
}

func encodePacket(packet Packet) []byte {
    data := make([]byte, 4+len(packet.Content))
    binary.BigEndian.PutUint32(data[:4], uint32(packet.DataID))
    copy(data[4:], packet.Content)
    return data
}

func decodePacket(data []byte) Packet {
    dataID := int(binary.BigEndian.Uint32(data[:4]))
    content := make([]byte, len(data)-4)
    copy(content, data[4:])
    return Packet{DataID: dataID, Content: content}
}

func encodeAck(dataID int) []byte {
    ack := make([]byte, 4)
    binary.BigEndian.PutUint32(ack, uint32(dataID))
    return ack
}

func sendData(conn net.Conn, data []byte, sendChan chan struct{}) {
    var dataID int
    unacknowledged := make(map[int]Packet)
    for {
        select {
        case <-sendChan:
            packet := Packet{DataID: dataID, Content: data}
            _, err := conn.Write(encodePacket(packet))
            if err != nil {
                log.Println("Send error:", err)
                return
            }
            unacknowledged[dataID] = packet
            dataID++
            go func(id int) {
                timer := time.NewTimer(time.Second)
                defer timer.Stop()
                select {
                case <-timer.C:
                    if _, ok := unacknowledged[id]; ok {
                        log.Println("Resending packet:", id)
                        _, err := conn.Write(encodePacket(unacknowledged[id]))
                        if err != nil {
                            log.Println("Resend error:", err)
                        }
                    }
                }
            }(dataID - 1)
        }
    }
}

func receiveData(conn net.Conn, receiveChan chan []byte) {
    expectedID := 0
    outOfOrder := make([]Packet, 0)
    for {
        buffer := make([]byte, 1024)
        n, err := conn.Read(buffer)
        if err != nil {
            log.Println("Receive error:", err)
            return
        }
        packet := decodePacket(buffer[:n])
        if packet.DataID == expectedID {
            receiveChan <- packet.Content
            expectedID++
            for len(outOfOrder) > 0 && outOfOrder[0].DataID == expectedID {
                receiveChan <- outOfOrder[0].Content
                outOfOrder = outOfOrder[1:]
                expectedID++
            }
            _, err := conn.Write(encodeAck(packet.DataID))
            if err != nil {
                log.Println("Ack send error:", err)
            }
        } else {
            outOfOrder = append(outOfOrder, packet)
        }
    }
}

func main() {
    // 模拟客户端和服务器通信
    go func() {
        lis, err := net.Listen("tcp", ":8080")
        if err != nil {
            log.Fatal(err)
        }
        defer lis.Close()
        conn, err := lis.Accept()
        if err != nil {
            log.Fatal(err)
        }
        defer conn.Close()
        receiveChan := make(chan []byte)
        go receiveData(conn, receiveChan)
        for content := range receiveChan {
            fmt.Println("Received:", string(content))
        }
    }()

    time.Sleep(time.Second)
    conn, err := net.Dial("tcp", ":8080")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    sendChan := make(chan struct{})
    go sendData(conn, []byte("Hello, World!"), sendChan)
    for i := 0; i < 5; i++ {
        sendChan <- struct{}{}
        time.Sleep(time.Second)
    }
    close(sendChan)
    time.Sleep(2 * time.Second)
}