简单应用UDP模拟TCP的项目实现

UDP模拟TCP的项目实现报告

1. 引言

在网络协议中,传输控制协议 (TCP) 和用户数据报协议 (UDP)是两种代表性的设计。UDP是一种简洁、无连接的协议,提供了一种更快速、轻量的通信方式。而TCP是一种可靠、面向连接的协议,确保了数据传输的稳定性,TCP适用于网页浏览、文件传输和电子邮件等,UDP简单高效,适用于对传输延迟要求较高、可容忍一些数据丢失的应用场景,如DNS、SNMP,广播连接等。本文实现了UDP模拟TCP,旨在结合TCP的优点,提高UDP传输的稳定性。

TCP和UDP的区别

2.理论基础

2.1 TCP特性概述

传输控制协议TCP更加可靠,TCP确保数据包按顺序到达接收方,且不丢失,使用确认和重传机制来实现可靠性。它具有流量控制机制,以防止发送方过载接收方,以及拥塞控制机制,以监测和缓解网络拥塞。TCP是面向连接的,需要进行连接的建立和断开,同时使用数据校验来保证数据的完整性。

2.2 UDP特性概述

用户数据报协议(UDP)更加简单。UDP不提供可靠的数据传输,数据包可能丢失、重复或乱序到达,没有确认和重传机制。由于无连接和简单的特性,UDP具有较低的传输延迟,适用于实时应用。它支持广播和多播,允许将数据包同时发送给多个接收方。UDP将数据分割成数据报进行传输,每个数据报具有固定大小,但不提供数据校验机制。

2.3 UDP模拟TCP的前景

在游戏领域,将TCP的可靠性模拟到低延迟的UDP协议中,可以提高用户体验,UDP模拟TCP的机制可以提供必要的数据传输可靠性,同时保持低延迟的特性。

3. TCP协议机制

3.1 三次握手

首先,客户端发送一个带有同步序列号的数据包到服务器,请求连接开始。随后,服务器响应客户端的请求,发送一个同时包含同步确认序列号的数据包,回应客户端。客户端再发送一个带有确认序列号的数据包给服务器,建立连接过程。

3.2 超时重传

握手过程中,发送方启动计时器以监控ACK的接收,如果在设定的超时时间内未收到ACK,将触发数据包的重传。在连续超时的情况下,TCP通过逐步增加超时时间间隔来适应网络拥塞,并实施快速重传机制,同时根据网络的稳定性和动态性调整超时时间。

3.3 流量控制

在TCP通信中,接收方会告知发送方自己的可用接收窗口大小,表示能够接受的未确认数据的字节数。发送方根据接收窗口大小来控制发送速率,确保不会发送超过接收方能够处理的数据量,以避免数据丢失或缓冲区溢出的情况发生。

窗口大小可以动态调整,根据网络条件和接收方的处理速度进行适时的调整,提高数据传输的可靠性和效率。

3.4 拥塞控制

当网络中出现拥塞时,TCP采取一系列措施来避免过多的数据流量导致网络性能下降或崩溃。这包括减小发送速率,通过调整拥塞窗口大小来限制数据包的发送数量,并使用慢启动、拥塞避免和快速恢复等算法来逐步测试和调整网络的容量。

3.5 乱序数据处理

接收方接收到的数据包的序列号若不按顺序排列,它将把这些数据包放入一个缓存区中,等待后续的数据包到达,以便进行正确的排序。同时,接收方会要求重传,检测到丢失的数据包,再将缓存区的乱序数据包排序再交付给应用程序。

4. UDP模拟实现

要实现UDP实现TCP的要点,需要保证顺序性、可靠性、控制传输速率,包括但不限于以下内容,其中大部分要点都已实现:

  1. 传输机制:

    • 三次握手四次挥手
    • 引入序列号
  2. 重传机制:

    • 超时重传
    • 自适应RTO、动态RTT估计RTO
    • 快速重传
  3. 校验机制:

    • 设置缓冲区储存:乱序、丢失、损坏数据包
    • 检测和请求重传:丢失或者损坏数据包
  4. 流量控制:

    • 发送滑动窗口大小
    • 控制发送速率匹配窗口大小
  5. 拥塞控制:

    • 网络拥塞时减小窗口大小

    • 模拟慢启动、拥塞避免、快速恢复来动态调整窗口大小

4.1 UDP模拟TCP的传输机制

模拟三次握手四次挥手

在握手过程中,双方各自初始化序列号,这对于后续处理至关重要,能保证数据包的顺序,也能检测丢失、重复的数据,也方便流量控制和拥塞控制。在UDP中引入序列号,使得每个UDP数据包都可以被追踪。

客户端发送数据时附带序列号
data = f"{client_seq + seq}:{message}:{checksum(message)}"

握手步骤握手过程
SYN客户端发送一个包含“SYN”的数据包以及选择一个“初始序列号ISN”到服务器,以开始建立连接
SYN-ACK若服务器收到SYN后,发送一个包含“SYN-ACK”标记的数据包和“客户端的ISN确认号”作为响应。
ACK客户端收到“SYN-ACK“后,发送一个“ACK”标记和“服务器的ISN确认号”的数据包。

我通过UDP协议模拟了TCP的三次握手和四次挥手机制。在三次握手中,客户端首先发送一个带有序列号的SYN包,服务器回应一个SYN-ACK包,最后客户端发送一个ACK包来确认连接。客户端和服务器分别发送FIN和ACK包以关闭连接,四次挥手使得连接终止。

服务器三次握手四次挥手代码示例:

第一步:接收客户端的SYN
syn_data, client_address = server_socket.recvfrom(1024)
if syn_data.decode().startswith('SYN'):
第二步:发送SYN-ACK给客户端
server_socket.sendto(f'SYN-ACK:{server_seq}'.encode(), client_address)
第三步:等待客户端的ACK
ack_data, client_address = server_socket.recvfrom(1024)

客户端向服务器发送FIN
client_socket.sendto(f"FIN:{client_seq + len(messages)}".encode(), (ip, port))
等待服务器的FIN-ACK
fin_ack, _ = client_socket.recvfrom(1024)

4.2 UDP模拟TCP的超时重传

UDP协议发送数据包时开启超时计时器,到接收到对应ACK的时间,发送方可以测量往返时间RTT,监控ACK的接收情况。设置一个超时时间,在预定超时时间内未收到ACK时,触发数据包的重传。

client_socket.settimeout(estimated_rtt)# 设置超时时间

client_socket.sendto(data.encode(), (ip, port))# 超时后重传

时间估计优化

这里我使用了RTT动态估计RTO,降低了延迟对超时时间的影响。

RTT估计RTO函数示例:
def calculate_rto(sample_rtt, estimated_rtt, dev_rtt):
alpha = 0.125
beta = 0.25
estimated_rtt = (1 - alpha) * estimated_rtt + alpha * sample_rtt
dev_rtt = (1 - beta) * dev_rtt + beta * abs(sample_rtt - estimated_rtt)
return estimated_rtt + 4 * dev_rtt

当网络稳定时,使用预设的固定值,固定延迟的值可以在连接建立时测量几次RTT,然后取平均值作为固定延迟估计。

在不稳定的网络环境中,使用加权平均方法对RTT进行平滑处理,使用指数加权移动平均来减少暂时性网络波动的影响。

快速重传优化

快速重传机制,如果接收方收到一个乱序的数据包,它可以立即发送一个对最后按顺序接收的数据包的ACK。如果发送方收到对同一数据包的三个冗余ACK,它可能会立即重传该数据包,而不是等待超时。

#当重复数等于三时触发快速重传

if duplicate_acks == 3:

连续超时优化

指数退避机制,在连续的超时情况下,UDP可以通过增加超时时间间隔来模拟TCP的指数退避策略。在连续超时的情况下,超时时间间隔可以每次翻倍,这称为。它有助于适应网络拥塞状况,避免在网络已经拥塞的情况下过多地发送数据。

同时等待时间不会无限增大,最终的超时时间应该包括一个额外的边际值,以应对不可预见的延迟波动。

4.3 UDP模拟TCP的缓冲区

检测和请求重传

在服务器端,我设置了一个缓冲区来处理乱序、丢失或损坏的数据包,并利用简单的校验函数来判断,若检测到数据包损坏或丢失时忽略该包,等待重传。

检测与忽略代码示例:
buffer = {} # 缓存乱序到达的数据包
while expected_seq in buffer:
buffered_data = buffer.pop(expected_seq)
if checksum(data) != chksum:
print(f"Packet {packet_seq} corrupted. Ignored.")

4.4 UDP模拟TCP的滑动窗口

UDP协议中接收方通过发送窗口大小信息告知其当前能够接收的数据量,发送方据此调整发送速率,匹配接收方的接受能力,以实现流量控制。

显式窗口大小反馈

UDP缺乏内部的流量控制机制,需要设置一种显式的反馈窗口,告知发送方当前的接受能力。

# 更新当前窗口大小

current_window_size = min(congestion_window, MAX_WINDOW_SIZE)

控制发送速率

通过动态调整窗口大小来控制数据的发送速率,窗口大小应该与网络条件和缓存区情况有关,若缓冲区满或者延迟高时,应减小窗口大小

4.5 UDP模拟TCP的拥塞窗口

通过调整数据发送速率响应网络拥塞情况,模拟TCP的拥塞控制。在丢包时减少发送速率,在无丢包时逐渐增加。同时模仿TCP的慢启动、拥塞避免、快速恢复的策略,通过调整拥塞窗口大小来控制数据发送速率。

拥塞控制状态转换代码示例:
# 拥塞控制状态调整 if congestion_state == "slow_start": congestion_window += 1 if congestion_window >= THRESHOLD: congestion_state = "congestion_avoidance" elif congestion_state == "congestion_avoidance": congestion_window += 1 / congestion_window elif congestion_state == "fast_recovery": congestion_window = THRESHOLD / 2 congestion_state = "slow_start"

快速恢复代码示例:
# 进入快速恢复状态 if duplicate_acks == 3: congestion_state = "fast_recovery" THRESHOLD = current_window_size // 2 current_window_size = THRESHOLD + 3

5. 结果分析

这里我实现了三次握手和四次挥手的连接机制、超时重传、动态RTT估计、快速重传、流量控制、拥塞控制以及缓冲区管理等TCP特性。通过模拟TCP的机制,在保持UDP低延迟特性的同时,显著增强了传输过程的可靠性和稳定性。

6. 附录

6.1 实验环境与工具

运行环境:python 3.9.10

导入库:socket

6.2 完整代码

运行方式:打开两个命令行,分别运行Server.py和Client.py。

Server.py
# 服务器

import socket
import time
import random

# 生成随机的初始序列号
def generate_initial_seq():
    return random.randint(0, 10000)

# 简单校验和函数
def checksum(data):
    return sum(data.encode()) % 256


def udp_server(ip, port):
    INITIAL_WINDOW_SIZE = 1  # 滑动窗口的初始大小
    MAX_WINDOW_SIZE = 1000  # 滑动窗口的最大大小
    THRESHOLD = 64  # 初始拥塞窗口阈值
    congestion_window = 1  # 初始拥塞窗口大小
    duplicate_acks = 0 # 用于跟踪重复ACK的计数器
    last_ack_received = -1

    server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server_socket.bind((ip, port))
    print("UDP Server listening on port:", port)

    while True:
        # 三次握手过程
        # 第一步:接收客户端的SYN
        syn_data, client_address = server_socket.recvfrom(1024)
        if syn_data.decode().startswith('SYN'):
            server_seq = generate_initial_seq()  # 随机生成服务器端的初始序列号
            print(f"SYN received from client. Sending SYN-ACK with ISN: {server_seq}...")
            # 第二步:发送SYN-ACK给客户端
            server_socket.sendto(f'SYN-ACK:{server_seq}'.encode(), client_address)
            # 第三步:等待客户端的ACK,建立连接
            ack_data, client_address = server_socket.recvfrom(1024)
            if ack_data.decode().startswith('ACK'):
                print("ACK received from client. Connection established.")
                server_seq += 1  # 服务器端的序列号增加

                # 数据接收、乱序处理和确认发送
                buffer = {}  # 缓存乱序到达的数据包
                expected_seq = server_seq + 1  # 期望接收的序列号
                current_window_size = INITIAL_WINDOW_SIZE  # 当前窗口大小
                duplicate_acks = 0  # 重复ACK计数
                congestion_state = "slow_start"  # 拥塞状态:slow_start, congestion_avoidance, fast_recovery

                while True:
                    packet_data, client_address = server_socket.recvfrom(1024)
                    if packet_data.decode().startswith('FIN'):
                        print("FIN received from client. Sending FIN-ACK...")
                        server_socket.sendto(f'FIN-ACK'.encode(), client_address)
                          # 最后的ACK确认
                        last_ack_data, client_address = server_socket.recvfrom(1024)
                        if last_ack_data.decode().startswith('ACK'):
                            print("Final ACK received. Connection terminated.")
                            break  # 跳出接收循环

                    packet_seq,data, chksum = packet_data.decode().split(':', 2)
                    packet_seq = int(packet_seq)
                    chksum = int(chksum)
                    # 检查校验
                    if checksum(data) != chksum:
                        print(f"Packet {packet_seq} corrupted. Ignored.")
                        continue
                    
                    # 正确顺序的数据包
                    if packet_seq == expected_seq:
                        print(f"Received correct packet {packet_seq}: {data}")
                        expected_seq += 1 # 更新期望的序列号
                        duplicate_acks = 0 # 重置重复ACK计数
                        # 发送ACK
                        server_socket.sendto(f"ACK:{expected_seq}".encode(), client_address)

                        # 处理缓存中的数据包
                        while expected_seq in buffer:
                            buffered_data = buffer.pop(expected_seq)
                            print(f"Received buffered packet {expected_seq}: {buffered_data}")
                            expected_seq += 1
                            server_socket.sendto(f"ACK:{expected_seq}".encode(), client_address)

                    # 乱序到达的数据包
                    elif packet_seq > expected_seq:
                        print(f"Received out-of-order packet {packet_seq}. Buffering...")
                        buffer[packet_seq] = data
                        server_socket.sendto(f"ACK:{expected_seq - 1}".encode(), client_address)  # 发送重复ACK

                        # 若ACK不同,增加计数
                        if expected_seq - 1 == last_ack_received:
                            duplicate_acks += 1
                        
                        if duplicate_acks == 3:
                            # 触发快速重传机制
                            if expected_seq in buffer:
                                data_to_resend = buffer[expected_seq]
                                server_socket.sendto(f"{expected_seq}:{data_to_resend}:{checksum(data_to_resend)}".encode(), client_address)
                                print(f"Resent packet {expected_seq}: {data_to_resend}")
                            else:
                                print(f"Packet {expected_seq} not found in buffer for retransmission.")
                            duplicate_acks = 0

                            # 进入快速恢复状态
                            congestion_state = "fast_recovery"
                            THRESHOLD = current_window_size // 2
                            current_window_size = THRESHOLD + 3
                        else:
                            # 收到的是新的ACK,重置重复ACK计数器
                            duplicate_acks = 1
                        last_ack_received = expected_seq - 1

                    # 拥塞控制:根据状态调整窗口大小
                    if congestion_state == "slow_start":
                        congestion_window += 1
                        if current_window_size >= THRESHOLD:
                            congestion_state = "congestion_avoidance"

                    elif congestion_state == "congestion_avoidance":
                        congestion_window += 1 / congestion_window

                    elif congestion_state == "fast_recovery":
                        congestion_window = THRESHOLD / 2
                        congestion_state = "slow_start"
                        
                     # 更新当前窗口大小
                    current_window_size = min(congestion_window, MAX_WINDOW_SIZE)
                            
            # 四次挥手过程
            print("FIN received from client. Sending FIN-ACK...")
            server_socket.sendto(f'FIN-ACK'.encode(), client_address)

            # 最后的ACK确认
            last_ack_data, client_address = server_socket.recvfrom(1024)
            if last_ack_data.decode().startswith('ACK'):
                print("Final ACK received. Connection terminated.")

        server_socket.close()


# 获取服务器的主机名和端口号
# host = socket.gethostname()
host = '127.0.0.1'
port = 8888
# 启动服务器
udp_server(host, port)

Client.py
# 客户端

import socket
import time
import random

# 生成随机的初始序列号
def generate_initial_seq():
    return random.randint(0, 10000)

# 简单校验函数
def checksum(data):
    return sum(data.encode()) % 256

# 计算RTO
def calculate_rto(sample_rtt, estimated_rtt, dev_rtt):
    alpha = 0.125
    beta = 0.25
    estimated_rtt = (1 - alpha) * estimated_rtt + alpha * sample_rtt
    dev_rtt = (1 - beta) * dev_rtt + beta * abs(sample_rtt - estimated_rtt)
    return estimated_rtt + 4 * dev_rtt


def udp_client(ip, port, messages):
    INITIAL_WINDOW_SIZE = 1  # 滑动窗口的初始大小
    MAX_WINDOW_SIZE = 1000  # 滑动窗口的最大大小
    MAX_TIMEOUT_RETRIES = 5 # 最大超时重传次数

    client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    client_seq = generate_initial_seq()  # 客户端的序列号
    # 三次握手过程
    # 第一步:发送SYN到服务器
    client_socket.sendto(f'SYN:{client_seq}'.encode(), (ip, port))
    print(f"SYN:{client_seq} sent to server.")

    # 第二步:等待服务器的SYN-ACK
    try:
        client_socket.settimeout(2.0) # 设置超时时间 
        syn_ack, _ = client_socket.recvfrom(1024)
        server_seq = int(syn_ack.decode().split(':')[1])
        if syn_ack.decode().startswith('SYN-ACK') and server_seq == client_seq + 1:
            print("SYN-ACK received from server. Sending ACK...")
            # 第三步:发送ACK到服务器
            client_seq += 1
        client_socket.sendto(f'ACK:{client_seq}'.encode(), (ip, port))
        print(f"ACK:{client_seq} sent to server. Connection established.")

        # 初始化RTO计算参数
        estimated_rtt = 0
        dev_rtt = 0

        # 数据发送和确认接收
        window_size = INITIAL_WINDOW_SIZE # 初始化窗口大小
        for seq, message in enumerate(messages):
            data = f"{client_seq + seq}:{message}:{checksum(message)}"
            client_socket.sendto(data.encode(), (ip, port))
            print(f"Sent: {data}")

            timeout_count = 0  # 超时计数器

            while True:
                try:
                    start_time = time.time()# 等待确认
                    ack_data, _ = client_socket.recvfrom(1024)
                    end_time = time.time()
                    sample_rtt = end_time - start_time

                    if ack_data.decode().startswith("ACK"):
                        ack_seq = int(ack_data.decode().split(':')[1])
                        if ack_seq >= client_seq + seq:
                            print(f"ACK received for packet {client_seq + seq}")
                            # 更新RTO
                            estimated_rtt = calculate_rto(sample_rtt, estimated_rtt, dev_rtt)
                            client_socket.settimeout(estimated_rtt)
                            break
                    elif ack_data.decode().startswith("WINDOW"):
                        window_size = int(ack_data.decode().split(':')[1])
                        congestion_window = min(window_size, congestion_window + 1)
                        print(f"Updated window size: {window_size}")
                

                except socket.timeout:
                    print(f"Timeout. Resending packet {client_seq + seq}.")
                    # 重传
                    client_socket.sendto(data.encode(), (ip, port))
                    timeout_count += 1
                    if timeout_count > MAX_TIMEOUT_RETRIES:
                        print("Max retries exceeded. Connection closed.")
                        return
                # 等待一段时间或检查窗口大小,然后发送下一个数据包
            time.sleep(max(0, 1 - window_size / MAX_WINDOW_SIZE))

        # 四次挥手过程
        # 发送FIN
        client_socket.sendto(f"FIN:{client_seq + len(messages)}".encode(), (ip, port))
        print(f"FIN sent to server.")

        # 等待服务器的FIN-ACK
        try:
            fin_ack, _ = client_socket.recvfrom(1024)
            if fin_ack.decode().startswith("FIN-ACK"):
                print("FIN-ACK received from server.")
                # 发送ACK
                client_socket.sendto(f"ACK:{client_seq + len(messages) + 1}".encode(), (ip, port))
                print("ACK sent to server. Connection terminating.")
                # 等待服务器的最后ACK
                try:
                    final_ack, _ = client_socket.recvfrom(1024)
                    if final_ack.decode().startswith("ACK"):
                        print("Final ACK received from server. Connection terminated.")
                except socket.timeout:
                    print("Final ACK from server not received. Connection forcefully closed.")
        except socket.timeout:
            print("Server did not respond to FIN. Connection forcefully closed.")

    except socket.timeout:
        print("Connection timeout. Server did not respond.")

    client_socket.close()



# 客户端的主机名和端口号
# host = socket.gethostname()
host = '127.0.0.1'
port = 8888
messages = ["Test1", "Test2222", "Test3333333333"]
udp_client(host, port, messages)

  • 40
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值