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来跟踪未确认的数据包和它们的发送时间,允许我们同时处理多个未确认的数据包。
- 如果未确认的数据包数量小于窗口大小,新数据包会被发送出去。