实践课后作业 UDPsocket 实现 ack| 青训营

97 阅读2分钟

1. 定义数据段

每个数据包应该有一个唯一的序号,以便于识别和确认。

2. 使用滑动窗口

滑动窗口是一种流量控制协议,允许发送方在没有收到确认的情况下发送多个数据包。窗口大小定义了发送方可以发送多少个未确认的数据包。

3. 跟踪丢失的段

可以通过一个数据结构来跟踪哪些包已经发送但尚未确认。

4. 超时和重传

对于每个已发送但未确认的数据包,可以设置一个超时计时器。如果超时没有收到 ack,则重新发送该数据包。

server.go

package main

import (
	"fmt"
	"net"
	"strconv"
	"strings"
)

func main() {
	flag := true
	// 绑定 UDP 地址
	addr, err := net.ResolveUDPAddr("udp", "localhost:12345")
	if err != nil {
		panic(err)
	}
	conn, err := net.ListenUDP("udp", addr)
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	for {
		buffer := make([]byte, 1024)
		n, clientAddr, err := conn.ReadFromUDP(buffer)
		if err != nil {
			fmt.Println(err)
			continue
		}
		message := string(buffer[:n])
		fmt.Printf("Received: %s\n", message)
		num, err := strconv.Atoi(strings.Split(message, " ")[1])
		if err != nil {
			panic(err)
		}

		// 模拟超时

		if flag && num == 6 {
			flag = false
			continue
		}
		ack := fmt.Sprintf("Ack %d", num)
		// 发送 ack
		_, err = conn.WriteToUDP([]byte(ack), clientAddr)
		if err != nil {
			fmt.Println(err)
		}
	}
}

client.go

package main

import (
    "fmt"
    "net"
    "time"
)

const (
    serverAddr  = "localhost:12345"
    bufferSize  = 1024
    windowSize  = 4
    timeout     = 5 * time.Second
)

func main() {
    conn, err := net.Dial("udp", serverAddr)
    if err != nil {
        fmt.Println("Error connecting to server:", err)
        return
    }
    defer conn.Close()

    unackedPackets := make(map[int]time.Time)
    nextPacketNumber := 0

    // 开启一个协程来接收ACK
    go func() {
        ack := make([]byte, bufferSize)
        for {
            conn.SetReadDeadline(time.Now().Add(timeout))
            _, err := conn.Read(ack)
            if err != nil {
                continue
            }

            packetNumber, err := parsePacketNumber(ack)
            if err == nil {
                fmt.Printf("Received ACK for packet %d\n", packetNumber)
                delete(unackedPackets, packetNumber)
            }
        }
    }()

    for nextPacketNumber < 10 || len(unackedPackets) > 0 {
        // 如果未确认的数据包数量未达窗口大小,发送新数据包
        for len(unackedPackets) < windowSize && nextPacketNumber < 10 {
            message := fmt.Sprintf("Packet %d", nextPacketNumber)
            fmt.Printf("Sending: %s\n", message)
            _, err := conn.Write([]byte(message))
            if err != nil {
                fmt.Println(err)
                continue
            }
            unackedPackets[nextPacketNumber] = time.Now()
            nextPacketNumber++
        }

        // 检查超时并重发数据包
        for packetNumber, sentTime := range unackedPackets {
            if time.Since(sentTime) > timeout {
                message := fmt.Sprintf("Packet %d", packetNumber)
                fmt.Printf("Resending: %s\n", message)
                _, err := conn.Write([]byte(message))
                if err != nil {
                    fmt.Println(err)
                    continue
                }
                unackedPackets[packetNumber] = time.Now()
            }
        }

        time.Sleep(100 * time.Millisecond)
    }
}

func parsePacketNumber(ack []byte) (int, error) {
    var packetNumber int
    _, err := fmt.Sscanf(string(ack), "Ack %d", &packetNumber)
    if err != nil {
        return 0, err
    }
    return packetNumber, nil
}

使用一个未确认数据包列表unackedPackets和一个下一个数据包编号nextPacketNumber来实现滑动窗口。它首先发送窗口大小允许的数据包,并将它们添加到未确认数据包列表中。然后,它等待ACK或超时。如果超时,则重传未确认数据包。如果收到ACK,则从未确认数据包列表中删除已确认的数据包。程序重复这个过程,直到所有数据包都已发送并确认。

利用滑动窗口

  • 在接收ACK时使用了一个协程,允许程序同时发送和接收数据包。
  • 没有使用单一的超时信号量,而是通过检查每个未确认数据包的发送时间来确定是否超时。
  • 使用map来跟踪未确认的数据包和它们的发送时间,允许我们同时处理多个未确认的数据包。
  • 如果未确认的数据包数量小于窗口大小,新数据包会被发送出去。