udpclient.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import socket
  2. import sys
  3. import os
  4. import time
  5. def start_client(ip, port, path):
  6. if not os.path.exists(path): return
  7. client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  8. addr = (ip, port)
  9. (filename, filesize) = os.path.basename(path), os.path.getsize(path)
  10. try:
  11. print(f"\033[46m[客户端]\033[0m {ip}:{port},建立连接")
  12. while True:
  13. try:
  14. client_socket.sendto(b"SYN", addr)
  15. if client_socket.recv(1024) == b"SYN-ACK": break
  16. except socket.timeout: continue
  17. print(f"\033[46m[客户端]\033[0m 发送元数据:{filename}({filesize} 字节)")
  18. while True:
  19. try:
  20. client_socket.sendto(f"META|{filename}|{filesize}".encode(), addr)
  21. if client_socket.recv(1024) == b"META-ACK": break
  22. except socket.timeout: continue
  23. print("\033[46m[客户端]\033[0m 传输数据……")
  24. with open(path, 'rb') as f:
  25. while True:
  26. chunk = f.read(1400)
  27. if not chunk: break
  28. client_socket.sendto(chunk, addr)
  29. time.sleep(0.001) # 简单的流控制
  30. print("\033[46m[客户端]\033[0m 释放连接")
  31. for _ in range(3):
  32. try:
  33. client_socket.sendto(b"FIN", addr)
  34. if client_socket.recv(1024) == b"FIN-ACK": break
  35. except socket.timeout: continue
  36. print("\033[46m[客户端]\033[0m 连接关闭")
  37. except KeyboardInterrupt: pass
  38. finally: client_socket.close()
  39. if __name__ == "__main__":
  40. if len(sys.argv) < 3:
  41. print("【用法】python3 file_sender.py <IP>:<端口> <文件路径>")
  42. sys.exit(1)
  43. try:
  44. (ip, port) = sys.argv[1].split(':')
  45. start_client(ip, int(port), sys.argv[2])
  46. except ValueError:
  47. print("【错误】地址格式应为 IP:端口")
  48. """
  49. # 代码详解与运行环境说明
  50. ## 1. 代码功能概述
  51. 本程序 `udpclient.py` 是一个基于 UDP 协议的文件发送客户端。它与配套的服务端 (`udpserver.py`) 协同工作,共同实现了一个模拟 TCP 行为的可靠文件传输系统。该程序不仅负责将本地文件读取并发送到网络,还负责在应用层层面管理连接状态,确保发送过程的有序性。
  52. 主要功能包括:
  53. 1. **命令行参数解析**:灵活解析用户输入的服务器地址(IP:Port)和待发送的文件路径。
  54. 2. **模拟 TCP 三次握手**:主动发起连接请求(SYN),并等待服务端的确认(SYN-ACK),确保服务端在线且准备就绪。
  55. 3. **元数据封装与发送**:自动提取文件名和文件大小,封装成特定格式的协议报文发送给服务端,并等待确认(META-ACK)。
  56. 4. **文件分块读取与发送**:高效地以二进制模式分块读取大文件,通过 UDP 数据报文逐块发送,支持任意类型的文件(文本、图片、视频等)。
  57. 5. **简单的流控制**:在发送数据包之间引入微小的延时,防止因发送速率过快导致接收端缓冲区溢出或网络拥塞。
  58. 6. **模拟 TCP 四次挥手**:数据发送完毕后,主动发送断开请求(FIN),并等待服务端确认(FIN-ACK),确保连接的优雅关闭。
  59. 7. **超时重传机制**:在关键的控制信令交互阶段(握手、元数据、挥手)引入了超时重试逻辑,提高了程序的健壮性。
  60. ## 2. 代码逻辑深度解析
  61. ### 2.1 模块导入与环境准备
  62. * `socket`:构建网络通信的基石。
  63. * `sys`:用于获取命令行参数 (`sys.argv`)。
  64. * `os`:用于检查文件是否存在 (`os.path.exists`)、获取文件名 (`os.path.basename`) 和获取文件大小 (`os.path.getsize`)。
  65. * `time`:用于实现流控制 (`time.sleep`),这是 UDP 编程中防止丢包的一个简单而有效的技巧。
  66. ### 2.2 `start_client` 函数详解
  67. 这是客户端的核心逻辑函数。
  68. #### 2.2.1 Socket 初始化
  69. ```python
  70. client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  71. ```
  72. 创建一个 UDP 套接字。注意,客户端通常不需要调用 `bind()`,操作系统会自动为其分配一个临时的源端口。
  73. #### 2.2.2 连接建立阶段(模拟握手)
  74. ```python
  75. while True:
  76. try:
  77. client_socket.sendto(b"SYN", addr)
  78. if client_socket.recv(1024) == b"SYN-ACK": break
  79. except socket.timeout: continue
  80. ```
  81. * **主动发起**:客户端发送 "SYN" 报文,这是 TCP 三次握手的第一步。
  82. * **超时重传**:这里使用了一个 `while True` 循环配合 `socket.timeout`。如果发送了 SYN 后,在指定时间内(默认 socket 可能是阻塞的,但在本程序中建议设置超时)没有收到 SYN-ACK,客户端会认为包丢失了,并自动重发 SYN。这大大提高了连接建立的成功率。
  83. * **收到确认**:一旦收到 "SYN-ACK",循环 `break`,连接建立成功。
  84. #### 2.2.3 元数据发送阶段
  85. ```python
  86. client_socket.sendto(f"META|{filename}|{filesize}".encode(), addr)
  87. ```
  88. * **协议封装**:将文件名和大小用 `|` 分隔,编码为 bytes 发送。
  89. * **等待确认**:同样采用了“发送-等待确认-超时重传”的逻辑。只有收到 "META-ACK",客户端才会进入下一个阶段。这防止了服务端还没准备好接收文件内容,客户端就开始狂发数据的情况。
  90. #### 2.2.4 数据传输阶段
  91. ```python
  92. with open(path, 'rb') as f:
  93. while True:
  94. chunk = f.read(1400)
  95. if not chunk: break
  96. client_socket.sendto(chunk, addr)
  97. time.sleep(0.001)
  98. ```
  99. * **块大小选择 (1400 bytes)**:这是一个精心选择的数值。以太网 MTU 通常为 1500 字节,减去 20 字节 IP 头和 8 字节 UDP 头,最大载荷为 1472 字节。选择 1400 字节留有余地,确保数据包不会在 IP 层被分片。IP 分片会显著降低 UDP 传输的可靠性(一个分片丢失导致整个包作废)。
  100. * **流控制 (Flow Control)**:`time.sleep(0.001)` 是一个简易的流控制机制。UDP 发送速度极快,如果没有这个延时,发送端可能会瞬间填满接收端的缓冲区,导致大量丢包。这个微小的延时给了接收端处理数据和写入磁盘的时间。
  101. #### 2.2.5 连接释放阶段(模拟挥手)
  102. ```python
  103. for _ in range(3):
  104. try:
  105. client_socket.sendto(b"FIN", addr)
  106. if client_socket.recv(1024) == b"FIN-ACK": break
  107. except socket.timeout: continue
  108. ```
  109. * **有限重试**:与握手阶段的无限重试不同,挥手阶段采用了 `range(3)` 有限重试。这是因为如果数据已经发完了,即使挥手失败,后果也相对可控。
  110. * **发送 FIN**:告知服务端“我发完了”。
  111. * **等待 FIN-ACK**:确认服务端也知道“传输结束”并关闭了连接。
  112. ## 3. 运行环境与配置
  113. ### 3.1 操作系统
  114. * **Linux/macOS/Windows** 全平台通用。
  115. * 在 Linux 环境下,可以使用 `tcpdump` 或 `wireshark` 抓包工具清晰地观察到 SYN, SYN-ACK, META, DATA, FIN, FIN-ACK 的交互过程,是学习网络协议的绝佳实验环境。
  116. ### 3.2 Python 版本
  117. * Python 3.x。
  118. * 无需额外依赖。
  119. ### 3.3 参数配置
  120. * **IP 地址**:
  121. * 如果是本地测试,使用 `127.0.0.1`。
  122. * 如果是跨机器测试,使用服务端的局域网 IP(如 `192.168.1.x`)。
  123. * **端口**:必须与服务端监听的端口一致(默认 7474)。
  124. * **文件路径**:支持绝对路径和相对路径。支持任何格式的文件。
  125. ## 4. 背景知识:Socket 编程中的“坑”与“解”
  126. ### 4.1 粘包与拆包
  127. 在 TCP 编程中,常见的难题是“粘包”(多个小包被合并成一个大包接收)和“拆包”。
  128. * **UDP 的优势**:UDP 是基于**消息(Datagram)**的协议,它保留了消息的边界。发送端调用一次 `sendto` 发送 100 字节,接收端调用一次 `recvfrom` 就刚好收到这 100 字节(如果缓冲区够大)。因此,本程序不需要像 TCP 那样处理粘包问题,代码逻辑大大简化。
  129. ### 4.2 缓冲区溢出
  130. * **现象**:发送端狂发数据,接收端处理不过来,操作系统内核的 UDP 接收缓冲区(Receive Buffer)满了,后续到达的包直接被内核丢弃。
  131. * **本程序的解法**:
  132. 1. **应用层流控**:`time.sleep(0.001)` 降低发送速率。
  133. 2. **系统调优(进阶)**:在生产环境中,可以通过 `sysctl -w net.core.rmem_max=26214400` 等命令增大内核缓冲区。
  134. ### 4.3 MTU 与 IP 分片
  135. * **MTU (Maximum Transmission Unit)**:链路层(如以太网)限制了一次能传输的最大帧大小(通常 1500 字节)。
  136. * **IP 分片**:如果 UDP 包大于 MTU,IP 层会将其切片。
  137. * **危害**:任何一个分片丢失,整个 UDP 包都无法重组,必须全部重传。且分片重组消耗 CPU。
  138. * **本程序的解法**:我们在代码中硬编码了读取块大小为 `1400` 字节,严格控制 UDP 包大小小于 MTU,从而避免了 IP 分片,提高了传输效率和可靠性。
  139. ## 5. 扩展思考:如何让 Client 更智能?
  140. 1. **动态 RTT 估算**:目前的超时时间是固定的。更智能的客户端可以计算往返时间(RTT),动态调整超时时间。
  141. 2. **滑动窗口**:目前的机制是“停等协议”(Stop-and-Wait)的变种(虽然数据阶段是流式的,但控制阶段是停等的)。实现滑动窗口可以允许同时有多个未确认的包在途,大幅提高带宽利用率。
  142. 3. **拥塞感知**:如果发现丢包率上升,自动增大 `sleep` 的时间;如果网络状况好,自动减小 `sleep` 时间。
  143. ## 6. 总结
  144. `udpclient.py` 是一个麻雀虽小五脏俱全的网络程序。它演示了如何使用 Python 的 Socket API 进行网络通信,更重要的是,它展示了如何在应用层“重新发明轮子”——在不可靠的 UDP 上模拟可靠的连接和传输。这种“造轮子”的过程,是深入理解 TCP/IP 协议栈设计思想(如握手、确认、分片、流控)的必经之路。通过运行和调试这个程序,你将不再把网络看作一个黑盒,而是能清晰地看到数据包在网线中穿梭的轨迹。
  145. """