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相关用法:
net.ResolveUDPAddr: 用于解析UDP地址字符串;
func ResolveUDPAddr(network, address string) (*UDPAddr, error)
net.DialUDP: 创建一个UDP连接,并返回一个UDPConn对象,用于发送和接收UDP数据;
func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)
-
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)
net.ListenUDP: 创建一个UDP监听器,用于接收UDP数据包。
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)
服务端
客户端
结束语
同时用python 和 go实现了客户端和服务端,可以尝试用python客户端发送数据到go的服务端。无论用什么语言,底层都是通过udp协议来实现数据的发送和接收。 不够完善的地方:
在客户端发送完数据后,仅记录了数据的发送情况。没有再对3次发送失败的数据再进行后续的处理。