一、功能描述:
本代码经实跑,在01Studio的K230开发板上能够正常运行,其主要功能包括:
1. 建立WiFi热点
名称K230
,密码12345678
2. 提供简单 DHCP 服务
服务器IP:192.168.9.99
,端口67
IP地址池:192.168.9.100-192.168.9.200
租期:8小时
3.主要函数说明:
IP_HEAD
及相关变量:定义了 IP 地址范围和租期等信息。ip_to_int
和int_to_ip
函数:用于 IP 地址和整数之间的转换。CreateDHCPpool
函数:初始化可用 IP 池和已分配 IP 的映射。allocate_ip
函数:根据客户端 MAC 地址分配未使用的 IP 地址。DHCP 消息类型
:定义了不同的 DHCP 消息类型常量。DHCP_frame
函数:解析接收到的 DHCP 帧数据。DHCP_Respone_frame
函数:根据客户端帧信息构建服务器的响应帧。SetupNetWork
函数:进行网络设置,包括开启无线接入点、配置网络参数、创建套接字并绑定。PintFrameData
函数:打印解析后的帧数据。start_dhcp_server
函数:主循环,接收客户端请求,处理并响应。包括处理超时、异常情况,以及根据客户端请求进行 IP 分配和发送响应。
二、程序流程:
- 在
__main__
部分调用start_dhcp_server
函数启动服务器。 - 首先进行网络设置和 IP 池初始化。
- 进入主循环,非阻塞地检查是否有客户端数据到达。
- 若有数据,解析帧并根据请求类型(如 Discover 或 Request)进行处理,分配 IP 并发送响应。
- 处理异常情况,如网络设置失败时进行重试,检测到 IDE 中断则退出。
- 若长时间无客户端请求导致租期超时,重新初始化 IP 池。
三、注意事项:
- 程序仅支持 IPv4。
- 异常处理主要集中在网络设置和数据接收部分,对于其他可能的错误情况可能需要进一步完善处理逻辑。
- 由于是简单的示例程序,可能在实际应用中需要更多的优化和功能扩展,例如更精细的 IP 管理、支持更多的 DHCP 选项等。
四、我整理的DHCP协议和通讯帧格式:
-
DHCP帧的打包位置:
以太网头 . IP头,UDP头,DHCP 帧 -
DHCP帧结构
位序头 0 10 20 30
位号 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| op (1) | htype (1) | hlen (1) | hops (1) |
+---------------+---------------+---------------+---------------+
| xid (4) |
+-------------------------------+-------------------------------+
| secs (2) | flags (2) |
+-------------------------------+-------------------------------+
| ciaddr (4) |
+---------------------------------------------------------------+
| yiaddr (4) |
+---------------------------------------------------------------+
| siaddr (4) |
+---------------------------------------------------------------+
| giaddr (4) |
+---------------------------------------------------------------+
| chaddr (16) |
+---------------------------------------------------------------+
| sname (64) |
+---------------------------------------------------------------+
| file (128) |
+---------------------------------------------------------------+
| options (variable) |
+---------------------------------------------------------------+
- 其中的
options
** 由magic cookes
和多个data Frame
构成:
-
magic cookes
: 长度4byte
值 b’\x63\x82\x53\x63’(固定) -
data Frame
格式为OpID + OpLen + OpValue
*Options_ID: 1byte 1 设置子网掩码选项。 3 设置网关地址选项。 6 设置DNS服务器地址选项。 12 设置DHCP客户端的主机名选项。 15 设置域名后缀选项。 33 设置静态路由选项。该选项中包含一组有分类静态路由(即目的地址的掩码固定为自然掩码,不能划分子网),客户端收到该选项后,将在路由表中添加这些静态路由。如果存在Option121,则忽略该选项。 44 设置NetBios服务器选项。 46 设置NetBios节点类型选项。 50 设置请求IP地址选项。 51 设置IP地址租约时间选项。 52 设置Option附加选项。 53 设置DHCP消息类型。 54 设置服务器标识。 55 设置请求选项列表。客户端利用该选项指明需要从服务器获取哪些网络配置参数。该选项内容为客户端请求的参数对应的选项值。 58 设置续约T1时间,一般是租期时间的50%。 59 设置续约T2时间。一般是租期时间的87.5%。 60 设置厂商分类信息选项,用于标识DHCP客户端的类型和配置。 61 设置客户端标识选项。 66 设置TFTP服务器名选项,用来指定为客户端分配的TFTP服务器的域名。 67 设置启动文件名选项,用来指定为客户端分配的启动文件名。 77 设置用户类型标识。 121 设置无分类路由选项。该选项中包含一组无分类静态路由(即目的地址的掩码为任意值,可以通过掩码来划分子网),客户端收到该选项后,将在路由表中添加这些静态路由。 说明:设备作为DHCP客户端支持DHCP服务器通过Option121下发的静态路由。 *Options_Length: 1byte *Options_Value: 不确定
-
DHCP 服务中使用的
data Frame
与交互流程
Options_ID是53
Options_Length值为1,
在服务流程中Options_Value如下所示:DHCP流程 客户 服务器 流程(1) Discover(0x01) 流程(2) Offer(0x02) 流程(3) Request(0x03) 流程(4) ACK(0x05) NAK(0x06) (其它,被本服务回应为ACK) Decline(0x04) Release(0x07) Inform(0x08) 各流程解释为: (0x01)Discover DHCP客户端在请求IP地址时并不知道DHCP服务器的位置,DHCP客户端会在本地网络内以广播的方式发送Discover请求报文,已发现网络中DHCP服务器,收到Discover报文的DHCP服务器都会发送应答报文,DHCP客户端局据此可以知道网络中存在的DHCP服务器的位置 (0x02)Offer DHCP服务器收到Discover报文后,就会在所配置的地址池中查找一个合适的IP地址,加上相应的租约期限和其他配置信息(网关、DNS服务器等),构成一个Offer报文,发送给DHCP客户端,告知用户本服务器可以为其提供IP地址,这个报文只是告诉DHCP客户端可以提供IP地址,还需要客户端通过ARP来检测该IP地址是否重复 (0x03)Request DHCP客户端可能会受到很多Offer请求报文,所以必须在这些应答中选择一个。通常是选择第一个Offer应答报文额服务器作为自己的目标服务器,并向该服务器发送一个广播的Request请求报文,通过选择的服务器,希望获得所分配的IP地址。DHCP客户端在成功获取IP地址后,在地址使用租期达到50%时,会向DHCP服务器发送单播Request请求报文请求延续租期,没有收到ACK报文的话,在租期达到87.5%时,会再次发送广播的Request请求报文以请求延续租约 (0x05)ACK DHCP服务器收到Request请求报文后,根据Request报文中携带的用户MAC来查找有没有相应的租约记录,如果有则发送ACK应答报文,通知用户可以使用分配的IP地址 (0x06)NAK 如果DHCP服务器收到Request请求报文后,没有发现有相应的租约记录或者由于某些原因无法正常分配IP地址,则向DHCP客户端发送NAK应答报文,通知用户无法分配合适的IP地址 (0x04)Decline DHCP客户端收到DHCP服务器ACK应答报文后,通过地址冲突检测发现服务器分配的地址冲突或者由于其他原因导致不能使用,则会向DHCP服务器发送Decline请求报文,通知服务器所分配的IP地址不可用,以期获得新的IP地址 (0x07)Release 当DHCP客户端不再需要使用分配IP地址时(一般出现在客户端关机,下线等状况)就会主动向DHCP服务器发送Release请求报文,告知服务器用户不再需要分配IP地址,请求DHCP服务器释放对应IP地址 (0x08)Inform DHCP客户端如果需要从DHCP服务器端获取更加详细的配置信息,则需要向DHCP服务器发送Inform请求报文;DHCP服务器在收到报文后,根据租约查找到相应的配置信息后,向DHCP客户端发送ACK应答报文
五、源代码如下
import socket
import struct
import os
import network,usocket,utime
import binascii
import select
# 设置IP地址范围
IP_HEAD = '192.168.9.'
IP_POOL_START = 100
IP_POOL_END = 200
LeaseTimeoutSeconds=28800 #IP地址租期秒数:8小时
# 将IP转换为整数
def ip_to_int(ip):
parts = ip.split('.')
num = 0
for i in range(4):
num += int(parts[i]) << (8 * (3 - i))
return num
# 将整数转换为 IP
def int_to_ip(n):
parts = []
for _ in range(4):
n, part = divmod(n, 256)
parts.append(str(part))
return '.'.join(parts[::-1])
def inet_aton(yiaddr):
parts = yiaddr.split('.')
return struct.pack('!BBBB', int(parts[0]), int(parts[1]), int(parts[2]), int(parts[3]))
# 初始化可用 IP 池
def CreateDHCPpool():
global available_ips,allocated_ips
available_ips = list(range(IP_POOL_START, IP_POOL_END + 1)) #可用IP地址池
allocated_ips = {} #MAC 地址到 IP 地址的映射
def allocate_ip(mac): # 选择一个未分配的 IP 地址
global available_ips,allocated_ips
if mac in allocated_ips:
return IP_HEAD+str(allocated_ips[mac])
if available_ips:
allocated_ip = available_ips.pop(0)
allocated_ips[mac] = allocated_ip
return IP_HEAD+str(allocated_ip)
return None
# DHCP 消息类型
DHCP_DISCOVER = 1
DHCP_OFFER = 2
DHCP_REQUEST = 3
DHCP_ACK = 5
DHCP_NAK = 6
DHCP_DECLINE = 4
DHCP_RELEASE = 7
DHCP_INFORM = 8
# 定义 DHCP 帧的格式
dhcp_frame_format = '!4BI2H4I16s64s128sIBBB'
#帧解析
def DHCP_frame(data):
( op, htype, hlen, hops,
xid,
secs, flags,
ciaddr, yiaddr, siaddr, giaddr,
chaddr,sname, bootFile,
magic_cookie,
op_Type,op_Len,op_Dhcp) = struct.unpack(dhcp_frame_format, data)
frame_info = {
"操作码": op,
"硬件类别": htype,
"mac长度": hlen,
"中继数": hops,
"传输编号": xid,
"过期秒数": secs,
"广播标志": flags,
"客户IP": ciaddr,
"分配IP": yiaddr,
"服务IP": siaddr,
"网关IP": giaddr,
"客户mac": chaddr,
"服务器名": sname,
"boot档案": bootFile,
"magic_cookie": magic_cookie,
"Options_ID": op_Type,
"Options_Length": op_Len,
"Options_Value": op_Dhcp
}
return frame_info
#服务器应答
def DHCP_Respone_frame(client_info):
# 参数:解析后的客户帧
op = 2 # 服务器总是回应
htype = client_info["硬件类别"]
hlen = client_info["mac长度"]
hops = 0 #0跳
xid = client_info["传输编号"]
secs = LeaseTimeoutSeconds #3600秒过期
flags = 0
ciaddr = client_info["客户IP"]
yiaddr = client_info["分配IP"] # 根据具体情况设置
siaddr = client_info["服务IP"] # 服务器的 IP 地址
giaddr = client_info["网关IP"]
chaddr = client_info["客户mac"]
sname = b'\x00' * 64
BootFile = b'\x00' * 128
magic_cookie = 0x63825363
OpID = 0x35
OpLen = 0x01
# 根据消息类型设置选项
if client_info["Options_Value"] == DHCP_DISCOVER:
OpValue = DHCP_OFFER
#elif:
# 删除IP等
else: #DHCP_ACK
OpValue = DHCP_ACK
frame_data = struct.pack(dhcp_frame_format,
op, htype, hlen, hops, #op (1) | htype (1) | hlen (1) | hops (1)
xid, #xid (4)
secs, flags, #secs (2) | flags(2)
ciaddr, yiaddr, siaddr, giaddr, #ciaddr(4) | yiaddr(4) | siaddr(4) | giaddr (4)
chaddr, sname, BootFile, #chaddr(16) | sname(64) | file(128)
magic_cookie, #options_magic_cookie(4)
OpID,OpLen,OpValue) #OpID(1) | OpLen(1) | OpValue(1)
return frame_data
def SetupNetWork():
global ap,server_socket
while True:
os.exitpoint() #检测IDE中断
try:
ap = network.WLAN(network.AP_IF)
ap.active(True)
ap.config(essid="K230", password="12345678")
# 设置 IP 地址和子网掩码
ap.ifconfig((IP_HEAD+'9', '255.255.255.0', IP_HEAD+'9', IP_HEAD+'9'))
#获取地址及端口号对应地址
ai = socket.getaddrinfo("0.0.0.0", 67)
addr = ai[0][-1]
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #建立socket
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #属性:允许重复
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) #属性:允许发送到广播地址
server_socket.bind(addr) #绑定
break
except Exception as e:
print('SetupNetWork Error',e)
utime.sleep(1) #延时
continue
server_socket.setblocking(False) #非阻塞模式
print("DHCP binded at UDP [%s:%d]" % ((ap.ifconfig()[0]),67))
utime.sleep(1) #延时
def PintFrameData(Fdat):
print(f'''\n\n客户帧 :
操作码 = {Fdat["操作码"]},
硬件类别 = {Fdat["硬件类别"]},
mac长度 = {Fdat["mac长度"]},
中继数 = {Fdat["中继数"]},
传输编号 = {Fdat["传输编号"]:08X},
过期秒数 = {Fdat["过期秒数"]},
广播标志 = {Fdat["广播标志"]},
客户IP = 0x{Fdat["客户IP"]:08X},
分配IP = 0x{Fdat["分配IP"]:08X},
服务IP = 0x{Fdat["服务IP"]:08X},
网关IP = 0x{Fdat["网关IP"]:08X},
客户mac = {Fdat["客户mac"]},
服务器名 = {Fdat["服务器名"]},
boot档案 = {Fdat["boot档案"]},
magic_cookie = 0x{Fdat["magic_cookie"]:08X},
Options_ID = 0x{Fdat["Options_ID"]:02X},
Options_Length = 0x{Fdat["Options_Length"]:02X},
Options_Value = 0x{Fdat["Options_Value"]:02X}
\n''')
# 启动 DHCP 服务器
def start_dhcp_server():
global ap,server_socket,available_ips,allocated_ips
SetupNetWork() #初始化网络
CreateDHCPpool() #初始化可用IP池
TimeOutSeconds = LeaseTimeoutSeconds*5
while True:
os.exitpoint() #检测IDE中断
try:
ready = select.select([server_socket], [], [], 0) # 0 表示非阻塞检查
if ready[0]:
data, addr = server_socket.recvfrom(1024) # 接收 UDP 数据
TimeOutSeconds = LeaseTimeoutSeconds*5
else:
utime.sleep_ms(200)
TimeOutSeconds -= 1
if TimeOutSeconds <= 0 :
CreateDHCPpool() # 初始化可用 IP 池
TimeOutSeconds = LeaseTimeoutSeconds*5
print('IP地址租期超时,重建可用IP地址池')
continue
except Exception as e:
error_message = str(e).lower().replace(' ', '')
if 'ideinterrupt' in error_message:
print('IDE中断退出')
break
else:
print(f"出错,DHCP重建服务 001: {e}")
while True:
try:
utime.sleep(5) # 等5秒,重启WiFi
SetupNetWork()
CreateDHCPpool() # 初始化可用 IP 池
TimeOutSeconds = LeaseTimeoutSeconds*5
break
except:
print('重建失败')
continue
continue
Fdat= DHCP_frame(data) # 解析请求
if Fdat["操作码"] == 1: # 如果是 DHCP Discover 或 Request
client_ip = allocate_ip(Fdat["客户mac"].hex()) #分配IP
if client_ip:
BroadcastAddr = socket.getaddrinfo(IP_HEAD+'255', 68)[0][-1] #回应到广播地址
Fdat["分配IP"] = ip_to_int(client_ip)
response = DHCP_Respone_frame(Fdat)
server_socket.sendto(response,BroadcastAddr) # 广播应答到客户端
print(f'收到客户请求:Options_Value=0x{Fdat["Options_Value"]:02X}, ' +
f'传输编号={Fdat["传输编号"]:08X}, ' +
f'客户mac={(Fdat["客户mac"][:6]).hex()} ' +
f'分配IP={Fdat["分配IP"]:08X} : {client_ip}')
print('\t已经分配的地址:',allocated_ips)
server_socket.close()
ap.active(False)
del ap
if __name__ == "__main__":
try:
start_dhcp_server()
except KeyboardInterrupt:
print("DHCP 服务器已停止。")
六、运行效果
canMV K230 IDE的输出结果:
客户机得到的IP地址: