课后作业 go网络编程1 udp | 豆包MarsCode AI刷题

96 阅读4分钟

UDP socket实现ack,感知丢包重传

目标

UDP socket 实现ack,感知丢包重传

Tips

  • 学会UDP socket编程
  • 简单ack学习,客户端等待ack再发包
  • 什么时候客户端认为是丢包?
  • 重传怎么考虑效率?
  • 能不能不阻塞只传丢掉的中间的段?

针对上述的问题,逐步设计解决:

  • 由于udp并不需要服务端和客户端连接,所以没有自动的ACK机制。为了实现类似的效果,在发送包之前,首先发送一个链接包。需要服务端返回结果后再进行数据包的发送。
  • 如果超时未收到服务端的ack信息,即认为是丢包了
  • 重传可以设定重传次数。同时可以动态调整超时时间
  • 如果是串行发送一段数据,如果说中间遇到了丢包,那么就需要重新发送丢的这一段,直到发送成功或者达到最大发送次数。这就会形成阻塞。因此,为了避免因丢包导致的阻塞,在发送数据时采用多线程的方式并行发送,即使是遇到了丢包,也不影响其他包的发送。

实现 Python

服务端

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import socket
import random

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 绑定端口:
s.bind(("127.0.0.1", 9999))

print("Bind UDP on 9999...")
received_packets = []
while True:
    # 接收数据:
    data, addr = s.recvfrom(1024)
    print("Received from %s:%s." % addr)
    packet_num, packet_data = data.split(b':',1)
    packet_num = int(packet_num.decode("utf-8"))
    # 如果接受的是第一次链接的包
    # 就需要获取到数据包的长度
    
    if packet_data.decode("utf-8") == "connection request":
        reply = "ack"
        # 随机模拟丢包
        if random.random() > 0.6:
            continue
        s.sendto(reply.encode("utf-8"), addr)
        received_packets = [None for _ in range(packet_num)]
    # 下面的逻辑并不完善,如果直接没有遇到链接请求就直接来了数据
    # 是会报错的
    else:
        if random.random() > 0.3:
            continue
        received_packets[packet_num] = packet_data
        reply = f"ack:{packet_num}"
        s.sendto(reply.encode("utf-8"), addr)
        if None not in received_packets:
            print("all data have been received")

客户端

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import socket
import threading

total_packets = 3  # 计算分段数量
acknowledged = [False] * total_packets
def send_packet(packet_num, packet_data):
    max_retry = 3
    retry = 0
    while not acknowledged[packet_num] and retry < max_retry:  # 直到收到ACK才停止发送
        try:
            # 包装数据段:格式为"包号:数据"
            start_time = time.time()
            message = f"{packet_num}:".encode() + packet_data
            s.sendto(message, ("127.0.0.1", 9999))
            print(f"Sent packet {packet_num}")
            
            # 等待ACK
            ack, _ = s.recvfrom(1024)
            end_time = time.time()
            ack_num = int(ack.decode().split(":")[1])
            
            # 更新超时时间
            sample_rtt = end_time - start_time
            estimated_rtt = (1 - alpha) * estimated_rtt + alpha * sample_rtt
            s.settimeout(estimated_rtt * 2)
            if ack_num == packet_num:
                acknowledged[packet_num] = True  # 标记为已确认
                print(f"Received ACK for packet {packet_num}")
        except socket.timeout:
            retry += 1
            if retry >= 3:
                print(f"Max iteration time reached. Failed to send {packet_num} ")
                continue
            print(f"Timeout for packet {packet_num}, resending...")


s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
estimated_rtt = 2
s.settimeout(estimated_rtt)

rep = ""
times = 0
# 首先要建立链接
# 不成功就一直尝试
while rep != "ack":
    s.sendto("3:connection request".encode("utf-8"), ("127.0.0.1", 9999))
    try:
        rep = s.recv(1024).decode("utf-8")
    except socket.timeout:
        times += 1
        print("Time out. Retry. %s times" % times)

print("Connected to 127.0.0.1:9999")
i = 0
threads = []
# 模拟要发送的包有3个
# 如果是串行发送,就有可能会阻塞
for data in [b"Michael", b"Tracy", b"Sarah"]:
    # 新建线程用于发送包
    thread = threading.Thread(target=send_packet, args=(i, data))
    i += 1
    threads.append(thread)
    thread.start()

# 等待所有线程结束
for thread in threads:
    thread.join()

s.close()

API 函数名称不同:例如 recv() 和 Read(),send() 和 Write()。 连接创建方式不同:Go 使用 net.Dial,Python 使用 socket.socket 创建

实现 GO

GO 实现网络链接是通过net包,写法和python的socket有所区别。但是本质上流程是一致的。

  • API 函数名称不同:例如 recv() 和 Read(),send() 和 Write()。
  • 连接创建方式不同:Go 使用 net.Dial,Python 使用 socket.socket 创建

UDP相关用法:

  1. net.ResolveUDPAddr: 用于解析UDP地址字符串;
func ResolveUDPAddr(network, address string) (*UDPAddr, error)
  1. net.DialUDP: 创建一个UDP连接,并返回一个UDPConn对象,用于发送和接收UDP数据;
func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)
  1. UDPConn: 代表UDP连接,提供了用于发送和接收数据的方法,以及获取本地远程地址和关闭连接的各种方法;

    • UDPConn.Write: 发送UDP数据;
    • UDPConn.Read: 接收UDP数据;
    • UDPConn.LocalAddr: 获取本地地址;
    • UDPConn.RemoteAddr: 获取远程地址;
    • UDPConn.Close: 关闭UDP连接;
    • UDPConn.ReadFromUDP: 从UDP连接中接收数据,并返回发送方的地址信息;
    • UDPConn.WriteToUDP: 向指定的地址发送UDP数据。
func (c *UDPConn) Write(b []byte) (int, error)
func (c *UDPConn) Read(b []byte) (int, error)
func (c *UDPConn) LocalAddr() Addr
func (c *UDPConn) RemoteAddr() Addr
func (c *UDPConn) Close() error
func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error)
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
  1. net.ListenUDP: 创建一个UDP监听器,用于接收UDP数据包。
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)

参考

服务端

客户端

结束语

同时用python 和 go实现了客户端和服务端,可以尝试用python客户端发送数据到go的服务端。无论用什么语言,底层都是通过udp协议来实现数据的发送和接收。 不够完善的地方:

在客户端发送完数据后,仅记录了数据的发送情况。没有再对3次发送失败的数据再进行后续的处理。