udpserver.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import socket
  2. import sys
  3. import os
  4. def start_server(ip, port):
  5. server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  6. addr = (ip, port)
  7. server_socket.bind(addr)
  8. print(f"\033[42m[服务端]\033[0m {ip}:{port},等待连接……")
  9. while True:
  10. # 建立连接 (模拟握手)
  11. (data, client_addr) = server_socket.recvfrom(1024)
  12. (client_ip, client_port) = client_addr
  13. if data.decode('utf-8', errors='ignore') == "SYN":
  14. print(f"\033[42m[服务端]\033[0m 来自客户端 {client_ip}:{client_port} 的连接请求")
  15. server_socket.sendto("SYN-ACK".encode('utf-8'), client_addr)
  16. else:
  17. continue
  18. # 接收元数据 META、文件名、大小
  19. data, client_addr = server_socket.recvfrom(2048)
  20. try:
  21. _, filename, filesize = data.decode('utf-8').split('|')
  22. filesize = int(filesize)
  23. save_filename = f"{os.path.basename(filename)}" #######
  24. print(f"\033[42m[服务端]\033[0m 接收数据:{filename}({filesize} 字节)……")
  25. server_socket.sendto("META-ACK".encode('utf-8'), client_addr)
  26. except:
  27. continue
  28. # 传输数据
  29. received = 0
  30. with open(save_filename, 'wb') as f:
  31. while received < filesize:
  32. chunk, _ = server_socket.recvfrom(2048)
  33. if chunk == b"FIN": break
  34. f.write(chunk)
  35. received += len(chunk)
  36. print(f"\033[42m[服务端]\033[0m 保存文件:{save_filename}")
  37. # 释放连接(模拟挥手)
  38. data, _ = server_socket.recvfrom(1024)
  39. if data.decode('utf-8', errors='ignore') == "FIN":
  40. print("\033[42m[服务端]\033[0m 断开连接")
  41. server_socket.sendto("FIN-ACK".encode('utf-8'), client_addr)
  42. print(f"\033[42m[服务端]\033[0m {ip}:{port},等待连接……")
  43. # break # 可加可不加
  44. if __name__ == "__main__":
  45. (ip, port) = "0.0.0.0", 7474
  46. if len(sys.argv) > 1:
  47. try:
  48. (ip, port) = sys.argv[1].split(':')
  49. port = int(port)
  50. except ValueError:
  51. print("【错误】地址格式应为 IP:端口")
  52. sys.exit(1)
  53. start_server(ip, port)
  54. """
  55. # 代码详解与运行环境说明
  56. ## 1. 代码功能概述
  57. 本程序 `udpserver.py` 是一个基于 UDP 协议的文件接收服务端。它不仅仅是一个简单的 UDP 数据包接收器,更是一个模拟了 TCP 可靠传输机制(如三次握手、四次挥手)的应用层协议实现。该程序旨在演示如何在不可靠的 UDP 传输层之上,构建一个具备基本连接管理和文件传输功能的应用层协议。
  58. 主要功能包括:
  59. 1. **UDP Socket 监听**:在指定的 IP 地址和端口上监听来自客户端的数据报文。
  60. 2. **模拟 TCP 三次握手**:在正式传输数据前,通过 SYN 和 SYN-ACK 报文交互,确认双方的连接意愿,建立逻辑上的“连接”。
  61. 3. **可靠的元数据接收**:接收包含文件名和文件大小的元数据,并发送确认(ACK),确保接收端做好了接收文件的准备。
  62. 4. **文件数据流接收**:循环接收文件数据块,并将其写入本地磁盘,直到接收到的字节数达到预期的文件大小。
  63. 5. **模拟 TCP 四次挥手**:在数据传输完成后,通过 FIN 和 FIN-ACK 报文交互,优雅地关闭连接,释放资源。
  64. 6. **多客户端支持(串行)**:程序采用无限循环结构,在处理完一个客户端的传输任务后,会自动重置状态,等待下一个客户端的连接请求。
  65. ## 2. 代码逻辑深度解析
  66. ### 2.1 模块导入与环境准备
  67. 代码首先导入了 `socket`、`sys` 和 `os` 三个核心模块。
  68. * `socket` 模块是网络编程的基础,提供了创建套接字、绑定地址、发送和接收数据的方法。
  69. * `sys` 模块用于处理命令行参数,使得程序可以灵活地从命令行接收 IP 和端口配置。
  70. * `os` 模块用于文件路径操作,例如从接收到的元数据中提取纯文件名,防止路径遍历攻击。
  71. ### 2.2 `start_server` 函数详解
  72. 这是程序的核心函数,封装了服务端的所有逻辑。
  73. #### 2.2.1 Socket 创建与绑定
  74. ```python
  75. server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  76. server_socket.bind((ip, port))
  77. ```
  78. 这里创建了一个 IPv4 (`AF_INET`) 的 UDP (`SOCK_DGRAM`) 套接字。与 TCP 不同,UDP 是无连接的,因此不需要 `listen()` 和 `accept()` 调用。`bind()` 方法将套接字绑定到指定的 IP 和端口,使其能够接收发往该地址的数据包。
  79. #### 2.2.2 连接建立阶段(模拟握手)
  80. 程序进入一个无限循环 `while True`,首先处于“等待连接”状态。
  81. ```python
  82. (data, client_addr) = server_socket.recvfrom(1024)
  83. if data.decode(...) == "SYN":
  84. server_socket.sendto("SYN-ACK".encode(...), client_addr)
  85. ```
  86. 这一步模拟了 TCP 的第一次和第二次握手。
  87. * **接收 SYN**:服务端阻塞等待,直到收到一个数据包。如果数据包的内容解码后是 "SYN",则认为这是一个连接请求。
  88. * **发送 SYN-ACK**:服务端记录下客户端的地址 (`client_addr`),并立即回复一个 "SYN-ACK" 报文。这表示服务端同意建立连接。
  89. * **设计考量**:这里使用 1024 字节的缓冲区,对于仅包含控制指令的报文来说绰绰有余。
  90. #### 2.2.3 元数据接收阶段
  91. 连接建立后,服务端并不立即接收文件内容,而是先等待文件的元数据。
  92. ```python
  93. data, client_addr = server_socket.recvfrom(2048)
  94. _, filename, filesize = data.decode(...).split('|')
  95. ```
  96. * **协议格式**:应用层协议定义元数据格式为 `META|文件名|文件大小`。
  97. * **解析与确认**:服务端解析出文件名和大小,并发送 "META-ACK" 确认。这一步至关重要,它起到了同步的作用,确保服务端在开始接收大量数据流之前,已经知道了文件的基本信息(如存哪里、存多大)。
  98. #### 2.2.4 数据传输阶段
  99. 这是最核心的数据接收循环。
  100. ```python
  101. with open(save_filename, 'wb') as f:
  102. while received < filesize:
  103. chunk, _ = server_socket.recvfrom(2048)
  104. f.write(chunk)
  105. ```
  106. * **二进制写入**:使用 `'wb'` 模式打开文件,确保无论是文本文件还是图片、视频等二进制文件都能正确保存。
  107. * **循环接收**:通过 `while received < filesize` 控制循环,确保接收到的字节数精确等于文件大小。
  108. * **缓冲区大小**:这里使用了 2048 字节的缓冲区。虽然以太网 MTU 通常为 1500 字节,但应用层缓冲区稍大一些可以确保完整接收经过 IP 分片重组后的数据包,或者容纳稍大的 UDP 载荷。
  109. #### 2.2.5 连接释放阶段(模拟挥手)
  110. 文件接收完毕后,服务端进入等待断开状态。
  111. ```python
  112. data, _ = server_socket.recvfrom(1024)
  113. if data.decode(...) == "FIN":
  114. server_socket.sendto("FIN-ACK".encode(...), client_addr)
  115. ```
  116. * **接收 FIN**:客户端发送 "FIN" 报文表示数据发送完毕,请求断开。
  117. * **发送 FIN-ACK**:服务端回复确认,并打印断开连接的日志。
  118. * **循环重置**:完成这一步后,程序回到外层 `while True` 循环的开头,重新进入“等待连接”状态,准备服务下一个用户。
  119. ## 3. 运行环境与配置
  120. ### 3.1 操作系统
  121. 本程序基于 Python 标准库开发,具有极强的跨平台性。
  122. * **Linux (推荐)**:如 Ubuntu, CentOS, Debian。Linux 的网络栈性能优异,且命令行工具丰富,非常适合运行此类网络服务。
  123. * **Windows**:完全兼容。Windows 的 Socket API 与 Linux 基本一致。
  124. * **macOS**:完全兼容。
  125. ### 3.2 Python 版本
  126. * **推荐版本**:Python 3.6 及以上。
  127. * **依赖库**:仅依赖 Python 标准库 (`socket`, `sys`, `os`),无需安装任何第三方 pip 包。这使得程序非常轻量,易于部署。
  128. ### 3.3 网络配置
  129. * **IP 地址**:
  130. * `0.0.0.0`:绑定到所有网络接口。这意味着局域网内的其他机器、本机(localhost)都可以访问该服务。这是服务端的默认推荐配置。
  131. * `127.0.0.1`:仅绑定到本地回环接口。只有本机可以访问,安全性更高,适合开发测试。
  132. * `特定局域网 IP`(如 `192.168.1.100`):仅允许通过该特定网卡访问。
  133. * **端口号**:
  134. * 默认端口为 `7474`。
  135. * 端口范围应在 1024-65535 之间,避免与系统保留端口(0-1023)冲突。
  136. * 确保防火墙(如 `iptables`, `ufw` 或 Windows 防火墙)允许 UDP 流量通过该端口。
  137. ## 4. 背景知识:UDP 与 TCP 的抉择
  138. ### 4.1 为什么选择 UDP?
  139. 用户可能疑惑,既然我们要模拟 TCP 的握手和挥手,为什么不直接用 TCP?
  140. * **学习目的**:本实验的核心目的在于深入理解网络协议的底层机制。直接使用 TCP,操作系统内核会帮我们处理所有的连接管理、重传、流控,开发者只能看到一个“流”。而使用 UDP,我们需要自己去思考“如何建立连接”、“如何界定消息边界”、“如何保证可靠性”,这是学习网络编程的最佳途径。
  141. * **性能与开销**:UDP 头部仅 8 字节,而 TCP 头部至少 20 字节。在某些对实时性要求极高、对丢包容忍度较高的场景(如视频直播、在线游戏),UDP 是更好的选择。
  142. * **灵活性**:基于 UDP,我们可以设计出符合特定需求的私有协议(如 QUIC 协议就是基于 UDP 实现的可靠传输协议)。
  143. ### 4.2 本程序的局限性
  144. 虽然本程序模拟了 TCP 的部分行为,但它仍然是一个简化的模型,缺乏 TCP 的许多关键特性:
  145. * **丢包重传**:真正的 TCP 有超时重传机制(RTO)。本程序在数据传输阶段如果发生丢包,接收端会一直阻塞在 `recvfrom`,导致死锁。
  146. * **乱序重排**:UDP 不保证包的到达顺序。真正的 TCP 有序列号(Seq)机制,接收端可以对乱序到达的包进行重组。本程序假设包是按序到达的。
  147. * **流量控制**:真正的 TCP 有滑动窗口机制,防止发送方发得太快淹没接收方。本程序仅在客户端通过 `time.sleep(0.001)` 进行了极其简单的速率限制。
  148. * **拥塞控制**:真正的 TCP 会根据网络拥塞程度动态调整发送窗口(慢启动、拥塞避免)。本程序没有此功能。
  149. ## 5. 扩展思考:如何实现可靠 UDP (RUDP)?
  150. 如果要将本程序升级为一个真正的工业级可靠文件传输工具,我们需要在应用层实现以下机制:
  151. 1. **序列号 (Sequence Number)**:给每个数据包编号(Packet 1, Packet 2...)。接收端根据编号判断是否有包丢失或乱序。
  152. 2. **确认机制 (ACK)**:接收端每收到一个包(或一组包),就回复一个 ACK 包,告诉发送端“我收到了”。
  153. 3. **超时重传 (Retransmission)**:发送端发出包后启动定时器。如果在规定时间内没收到 ACK,就重发该包。
  154. 4. **校验和 (Checksum)**:虽然 UDP 头部有校验和,但应用层可以增加更强的校验(如 CRC32 或 MD5),确保文件内容在传输过程中没有一位比特发生错误。
  155. ## 6. 总结
  156. `udpserver.py` 展示了一个基于 UDP 的简易文件传输协议的服务端实现。它通过应用层的握手和挥手机制,赋予了无连接的 UDP 以“连接”的概念;通过元数据交换,实现了文件的定界。虽然它在可靠性上无法与 TCP 媲美,但它是一个极佳的教学案例,帮助开发者理解网络协议设计的核心要素:连接管理、状态同步、数据封装与解封装。通过阅读和修改此代码,你可以亲手触摸到网络通信的脉搏。
  157. """