diff --git a/main.py b/main.py index 9bc8b6d..ad66824 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,12 @@ import re # 获取本机信息 def get_local_info(): + """ + 获取本机的主机名、MAC地址和IP地址列表。 + + Returns: + tuple: 包含主机名、MAC地址和IP地址列表的元组。 + """ hostname = socket.gethostname() local_ips = socket.gethostbyname_ex(hostname)[-1] mac = ':'.join(re.findall('..', '%012x' % uuid.getnode())) @@ -15,16 +21,46 @@ def get_local_info(): # 验证 IP 地址或域名 def validate_ip_or_domain(value): + """ + 验证输入值是否为有效的IP地址或域名。 + + Args: + value (str): 待验证的IP地址或域名。 + + Returns: + bool: 如果输入值有效返回True,否则返回False。 + """ ip_pattern = re.compile(r'^\d{1,3}(\.\d{1,3}){3}$') domain_pattern = re.compile(r'^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$') return ip_pattern.match(value) or domain_pattern.match(value) # 验证端口号 def validate_port(port): + """ + 验证输入的端口号是否有效。 + + Args: + port (str): 待验证的端口号。 + + Returns: + bool: 如果端口号有效返回True,否则返回False。 + """ return port.isdigit() and 1 <= int(port) <= 65535 # 构造 IP 头部 def construct_ip_header(src_ip, dest_ip, payload_length, protocol): + """ + 构造IP头部。 + + Args: + src_ip (str): 源IP地址。 + dest_ip (str): 目标IP地址。 + payload_length (int): 负载长度。 + protocol (int): 协议号。 + + Returns: + bytes: 构造的IP头部。 + """ version = 4 ihl = 5 tos = 0 @@ -45,6 +81,12 @@ def construct_ip_header(src_ip, dest_ip, payload_length, protocol): # 构造 ICMP 报文 def construct_icmp(): + """ + 构造ICMP报文。 + + Returns: + bytes: 构造的ICMP报文。 + """ icmp_header = struct.pack('!BBHHH', 8, 0, 0, 1, 1) data = b'Ping' checksum = calculate_checksum(icmp_header + data) @@ -53,11 +95,31 @@ def construct_icmp(): # 构造 UDP 报文 def construct_udp(src_port, dest_port): + """ + 构造UDP报文。 + + Args: + src_port (int): 源端口号。 + dest_port (int): 目标端口号。 + + Returns: + bytes: 构造的UDP报文。 + """ udp_header = struct.pack('!HHHH', src_port, dest_port, 8, 0) return udp_header # 构造 TCP 报文(SYN) def construct_tcp(src_port, dest_port): + """ + 构造TCP报文(SYN)。 + + Args: + src_port (int): 源端口号。 + dest_port (int): 目标端口号。 + + Returns: + bytes: 构造的TCP报文。 + """ seq = 0 ack_seq = 0 offset = 5 @@ -75,6 +137,15 @@ def construct_tcp(src_port, dest_port): # 计算校验和 def calculate_checksum(data): + """ + 计算校验和。 + + Args: + data (bytes): 待计算校验和的数据。 + + Returns: + int: 计算得到的校验和。 + """ checksum = 0 n = len(data) for i in range(0, n - 1, 2): @@ -88,6 +159,9 @@ def calculate_checksum(data): # 发送报文 def send_packet(): + """ + 发送报文。 + """ try: src_ip = ip_combobox.get() dest_host = dest_entry.get() diff --git a/代码解释.md b/代码解释.md index 937d805..c6cc60d 100644 --- a/代码解释.md +++ b/代码解释.md @@ -1,48 +1,90 @@ -下面是任务 2 代码的逐行详细解释: ---- +该代码是一个使用 `Tkinter` 库制作的网络通信软件,通过图形界面(GUI)允许用户发送不同类型的网络报文(如 ICMP、UDP、TCP)。以下是逐行解释代码: + +### 导入必要的库 ```python import tkinter as tk -from tkinter import messagebox +from tkinter import ttk, messagebox import socket import struct -import re import time +import uuid +import re ``` -1. **`tkinter`**:Python 的内置 GUI 库,用于创建图形用户界面。 -2. **`messagebox`**:`tkinter` 模块中的组件,用于显示错误或提示信息。 -3. **`socket`**:提供网络编程接口,用于构造和发送报文。 -4. **`struct`**:用于将数据打包成特定格式(二进制结构),例如构造报文头。 -5. **`re`**:正则表达式模块,用于验证 IP 地址的格式。 -6. **`time`**:用于记录和计算时间,测量报文往返的时间。 +- `tkinter` 用于创建 GUI 界面。 +- `ttk` 提供更丰富的部件,如下拉框(`Combobox`)。 +- `messagebox` 用于显示消息框。 +- `socket` 用于创建网络连接。 +- `struct` 用于处理二进制数据的打包和解包(比如构造报文头)。 +- `time` 和 `uuid` 用于获取当前时间戳和生成唯一的 MAC 地址。 +- `re` 用于正则表达式操作,主要用于验证 IP 地址或域名的格式。 ---- +### 获取本机信息 +```python +def get_local_info(): + hostname = socket.gethostname() + local_ips = socket.gethostbyname_ex(hostname)[-1] + mac = ':'.join(re.findall('..', '%012x' % uuid.getnode())) + return hostname, mac, local_ips +``` +- `gethostname()` 获取当前计算机的主机名。 +- `gethostbyname_ex(hostname)` 返回主机名对应的 IP 地址列表,选择最后一个(所有可用的 IP 地址)。 +- 使用 `uuid.getnode()` 获取 MAC 地址,使用正则表达式将其格式化为 `XX:XX:XX:XX:XX:XX` 的格式。 + +### 验证 IP 地址或域名 +```python +def validate_ip_or_domain(value): + ip_pattern = re.compile(r'^\d{1,3}(\.\d{1,3}){3}$') + domain_pattern = re.compile(r'^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$') + return ip_pattern.match(value) or domain_pattern.match(value) +``` +- 使用正则表达式验证输入的值是否是有效的 IP 地址或域名。 + +### 验证端口号 +```python +def validate_port(port): + return port.isdigit() and 1 <= int(port) <= 65535 +``` +- 确保端口号是一个数字并在合法范围(1-65535)内。 + +### 构造 IP 头部 +```python +def construct_ip_header(src_ip, dest_ip, payload_length, protocol): + version = 4 + ihl = 5 + tos = 0 + total_length = 20 + payload_length + identification = 54321 + flags_offset = 0 + ttl = 64 + header_checksum = 0 + + src_ip_bytes = socket.inet_aton(src_ip) + dest_ip_bytes = socket.inet_aton(dest_ip) + ip_header = struct.pack('!BBHHHBBH4s4s', + (version << 4) + ihl, tos, total_length, + identification, flags_offset, ttl, protocol, + header_checksum, src_ip_bytes, dest_ip_bytes) + + return ip_header +``` +- 构造一个 IPv4 头部,使用 `struct.pack()` 将数据打包为二进制格式。 +- `socket.inet_aton()` 用于将 IP 地址转换为二进制格式。 +- 该函数生成一个标准的 IPv4 头部,包含源 IP、目的 IP 和协议类型等信息。 ### 构造 ICMP 报文 ```python def construct_icmp(): icmp_header = struct.pack('!BBHHH', 8, 0, 0, 1, 1) - checksum = ~sum(icmp_header) & 0xFFFF + data = b'Ping' + checksum = calculate_checksum(icmp_header + data) icmp_header = struct.pack('!BBHHH', 8, 0, checksum, 1, 1) - return icmp_header + return icmp_header + data ``` -1. **构造 ICMP 报文的头部**: - - 使用 `struct.pack` 打包数据为二进制格式。 - - `!` 表示网络字节序(大端模式)。 - - `BBHHH` 定义字段类型:2 个 8 位整数,3 个 16 位整数。 - - **8** 表示 ICMP 类型(8 = 回显请求)。 - - **0** 表示代码字段。 - - **0** 表示校验和(暂时填充 0)。 - - **1, 1** 表示标识符和序列号。 -2. **计算校验和**: - - 使用 `~sum(...)` 计算报文的校验和并取反,确保数据完整性。 - - 结果为 16 位,使用 `& 0xFFFF` 截断为无符号整数。 -3. **重新打包报文**: - - 将计算后的校验和填入头部。 -4. **返回构造好的 ICMP 报文**。 - ---- +- ICMP 报文的类型为 8(Echo 请求)。 +- 使用 `struct.pack()` 构造 ICMP 头部,并计算校验和。 +- 校验和通过 `calculate_checksum()` 函数来计算,确保报文的完整性。 ### 构造 UDP 报文 ```python @@ -50,142 +92,164 @@ def construct_udp(src_port, dest_port): udp_header = struct.pack('!HHHH', src_port, dest_port, 8, 0) return udp_header ``` -1. **打包 UDP 报文头**: - - `HHHH` 表示 4 个 16 位整数(源端口、目标端口、长度、校验和)。 - - **src_port**:源端口号。 - - **dest_port**:目标端口号。 - - **8**:UDP 报文长度(仅头部时固定为 8 字节)。 - - **0**:校验和(暂时不计算,系统会自动填充)。 -2. **返回 UDP 报文头**。 +- 构造一个简单的 UDP 报文头,包含源端口、目标端口、长度和校验和。 ---- - -### 验证 IP 地址 +### 构造 TCP 报文(SYN) ```python -def validate_ip(ip): - pattern = re.compile(r'^\d{1,3}(\.\d{1,3}){3}$') - return pattern.match(ip) is not None -``` -1. **正则表达式验证**: - - 匹配 IP 地址格式:四段数字,每段 0-255 范围。 - - `^\d{1,3}`:每段由 1 到 3 位数字组成。 - - `(\.\d{1,3}){3}`:重复三次的“点+数字”模式。 -2. **返回结果**: - - 如果匹配成功,返回 `True`。 - - 如果失败,返回 `False`。 +def construct_tcp(src_port, dest_port): + seq = 0 + ack_seq = 0 + offset = 5 + reserved = 0 + flags = 0b000010 # SYN flag + window = socket.htons(5840) + checksum = 0 + urgent_ptr = 0 ---- + tcp_header = struct.pack('!HHLLBBHHH', + src_port, dest_port, seq, ack_seq, + (offset << 4) + reserved, flags, window, + checksum, urgent_ptr) + return tcp_header +``` +- 构造 TCP 头部,设置 SYN 标志位以启动连接。 +- 包含源端口、目标端口、序列号等信息。 + +### 计算校验和 +```python +def calculate_checksum(data): + checksum = 0 + n = len(data) + for i in range(0, n - 1, 2): + chunk = (data[i] << 8) + data[i + 1] + checksum += chunk + if n % 2 == 1: + checksum += data[-1] << 8 + checksum = (checksum >> 16) + (checksum & 0xFFFF) + checksum += (checksum >> 16) + return ~checksum & 0xFFFF +``` +- 计算校验和,保证数据的完整性。 +- 将数据按 2 字节分组求和,最终计算出一个 16 位的校验和。 ### 发送报文 ```python def send_packet(): try: - target_ip = ip_entry.get() - target_port = int(port_entry.get()) + src_ip = ip_combobox.get() + dest_host = dest_entry.get() + dest_port = port_entry.get() packet_type = var.get() - if not validate_ip(target_ip): - raise ValueError("目标 IP 地址无效") -``` -1. **获取用户输入**: - - `ip_entry.get()`:从文本框中获取目标 IP 地址。 - - `port_entry.get()`:从文本框中获取目标端口号并转为整数。 - - `var.get()`:从单选框中获取选择的报文类型。 -2. **验证 IP 地址**: - - 使用 `validate_ip` 函数检查 IP 格式。 - - 如果无效,抛出 `ValueError` 异常。 ---- + # 验证目标地址 + if not validate_ip_or_domain(dest_host): + raise ValueError("目标地址无效,必须为有效的 IP 或域名") -```python - sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP if packet_type == "ICMP" else socket.IPPROTO_UDP) -``` -1. **创建套接字**: - - `socket.AF_INET`:使用 IPv4 地址。 - - `socket.SOCK_RAW`:使用原始套接字,允许手动构造报文。 - - `socket.IPPROTO_ICMP`:ICMP 协议。 - - `socket.IPPROTO_UDP`:UDP 协议。 + # 验证端口号 + if not validate_port(dest_port): + raise ValueError("端口号无效,必须是有效的正整数(1-65535)") ---- + dest_ip = socket.gethostbyname(dest_host) + src_port = 12345 + dest_port = int(dest_port) -```python + # 构造报文并发送 if packet_type == "ICMP": + sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) packet = construct_icmp() elif packet_type == "UDP": - packet = construct_udp(12345, target_port) -``` -1. **选择报文类型**: - - 如果选择 ICMP,则调用 `construct_icmp` 构造报文。 - - 如果选择 UDP,则调用 `construct_udp`,源端口固定为 12345。 + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + packet = construct_udp(src_port, dest_port) + elif packet_type == "TCP": + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((dest_ip, dest_port)) + packet = b"Hello, TCP!" # TCP 是面向连接的,这里发送简单的字符串 + sock.sendall(packet) + sock.close() + result.set(f"TCP 连接成功,数据已发送!") + return # 结束函数 ---- - -```python - start_time = time.time() - sock.sendto(packet, (target_ip, target_port)) - end_time = time.time() -``` -1. **发送报文并测量时间**: - - 记录发送开始时间 `start_time`。 - - 使用 `sendto` 方法将报文发送到目标地址和端口。 - - 记录发送结束时间 `end_time`。 - ---- - -```python - result.set(f"报文发送成功!类型: {packet_type}, 往返时间: {end_time - start_time:.2f} 秒") + # 发送报文 + sock.sendto(packet, (dest_ip, dest_port)) + result.set(f"报文发送成功!类型: {packet_type}") + except socket.gaierror: + messagebox.showerror("网络错误", "无法解析目标地址,请检查输入的 IP 或域名是否正确。") + except socket.error as e: + messagebox.showerror("网络错误", f"网络错误: {e.strerror}") + except ValueError as e: + messagebox.showerror("输入错误", f"输入无效: {e}") except Exception as e: - messagebox.showerror("错误", f"报文发送失败: {e}") + messagebox.showerror("未知错误", f"发送报文时发生未知错误: {e}") ``` -1. **显示结果**: - - 使用 `result.set` 更新界面上的结果显示。 - - 如果发送成功,显示报文类型和往返时间。 -2. **错误处理**: - - 捕获异常并通过 `messagebox` 提示错误信息。 +- 根据用户选择的目标地址、端口号和报文类型,构造并发送相应的网络报文。 +- 对地址和端口进行验证,若无误则根据选择的报文类型(ICMP、UDP、TCP)构造报文并发送。 +- 如果是 TCP 报文,使用 `socket.SOCK_STREAM` 进行连接并发送数据。 +- 若发送成功,通过 `messagebox.showerror()` 显示错误信息。 ---- - -### GUI 界面 +### GUI 界面设计 ```python window = tk.Tk() window.title("网络通信软件") +``` +- 创建一个 Tkinter 窗口,设置标题为“网络通信软件”。 -tk.Label(window, text="目标 IP 地址:").grid(row=0, column=0) -ip_entry = tk.Entry(window) -ip_entry.grid(row=0, column=1) +```python +hostname, mac, local_ips = get_local_info() +``` +- 获取本机的主机名、MAC 地址和可用的 IP 地址。 -tk.Label(window, text="目标端口号:").grid(row=1, column=0) +```python +tk.Label(window, text="本机信息:").grid(row=0, column=0) +info_text = tk.Text(window, height=4, width=40) +info_text.grid(row=0, column=1, columnspan=2) +info_text.insert(tk.END, f"主机名: {hostname}\nMAC 地址: {mac}\n可用 IP: {', '.join(local_ips)}") +info_text.config(state=tk.DISABLED) +``` +- 显示本机的相关信息,如主机名、MAC 地址和可用 IP 地址。 + +```python +tk.Label(window, text="本机 IP 地址:").grid(row=1, column=0) +ip_combobox = ttk.Combobox(window, values=local_ips) +ip_combobox.grid(row=1, column=1) +ip_combobox.current(0) +``` +- 提供一个下拉框,显示本机的 IP 地址供用户选择。 + +```python +tk.Label(window, text="目标地址:").grid(row=2, column=0) +dest_entry = tk.Entry(window) +dest_entry.grid(row=2, column=1) +``` +- 提供一个文本框供用户输入目标地址。 + +```python +tk.Label(window, text="目标端口:").grid(row=3, column=0) port_entry = tk.Entry(window) -port_entry.grid(row=1, column=1) +port_entry.grid(row=3, column=1) +``` +- 提供一个文本框供用户输入目标端口。 -tk.Label(window, text="报文类型:").grid(row=2, column=0) +```python +tk.Label(window, text="报文类型:").grid(row=4, column=0) var = tk.StringVar(value="ICMP") -tk.Radiobutton(window, text="ICMP", variable=var, value="ICMP").grid(row=2, column=1) -tk.Radiobutton(window, text="UDP", variable=var, value="UDP").grid(row=2, column=2) +tk.Radiobutton(window, text="ICMP", variable=var, value="ICMP").grid(row=4, column=1) +tk.Radiobutton(window, text="UDP", variable=var, value="UDP").grid(row=4, column=2) +tk.Radiobutton(window, text="TCP", variable=var, value="TCP").grid(row=4, column=3) +``` +- 提供单选按钮供用户选择报文类型(ICMP、UDP、TCP)。 +```python result = tk.StringVar() -tk.Label(window, textvariable=result).grid(row=4, column=0, columnspan=3) +tk.Label(window, textvariable=result).grid(row=6, column=0, columnspan=3) +``` +- 显示报文发送结果。 -tk.Button(window, text="发送报文", command=send_packet).grid(row=3, column=1) +```python +tk.Button(window, text="发送报文", command=send_packet).grid(row=5, column=1) +``` +- 创建一个按钮,点击后调用 `send_packet()` 函数发送报文。 + +```python window.mainloop() ``` -1. **窗口初始化**: - - 创建 `window` 窗口对象。 - - 设置窗口标题为“网络通信软件”。 -2. **界面布局**: - - 使用 `grid` 方法设置控件布局,分别添加: - - 文本标签:提示用户输入目标 IP 和端口号。 - - 输入框:用于输入目标 IP 和端口号。 - - 单选框:选择报文类型(ICMP 或 UDP)。 - - 按钮:点击后调用 `send_packet` 函数发送报文。 - - 结果标签:显示发送结果或错误信息。 -3. **事件循环**: - - `window.mainloop()` 启动 GUI 主循环,等待用户交互。 - ---- - -### 总结 -这段代码实现了一个功能完备的网络通信工具,具备以下特点: -- **GUI 界面**:直观的输入与反馈。 -- **多协议支持**:可发送 ICMP 和 UDP 报文。 -- **输入验证**:保证用户输入的有效性。 -- **异常处理**:确保程序运行稳定。 \ No newline at end of file +- 启动 GUI 窗口的主循环,使窗口保持显示状态。 \ No newline at end of file