引言

在嵌入式Linux系统和网络设备开发中,动态主机配置协议(DHCP)是实现设备自动获取IP地址的关键技术。本文将详细介绍如何在Linux平台上使用Qt框架实现完整的DHCP自动获取网络配置功能,包括DHCP客户端调用、网络配置解析和Qt界面集成。

一、DHCP客户端选择:udhcpc

在嵌入式Linux环境中,我们通常选择轻量级的DHCP客户端。udhcpc是BusyBox工具集的一部分,具有体积小、功能完善的特点,非常适合资源受限的嵌入式设备。

udhcpc基本用法

udhcpc -b -s /opt/app/default.script -i eth0

参数说明:

  • -b: 后台运行模式

  • -s: 指定自定义配置脚本

  • -i: 指定网络接口

二、DHCP客户端脚本详解

脚本核心功能

我们的default.script脚本负责处理DHCP客户端的各个状态(注释版):

#!/bin/sh
 
# udhcpc script edited by Tim Riker <Tim@Rikers.org>
# 检查是否被 udhcpc 正确调用,如果没有参数则报错退出
[ -z "$1" ] && echo "Error: should be called from udhcpc" && exit 1

# 设置 DNS 配置文件路径
# 如果文件不存在则创建
# 根据 DHCP 提供的参数设置广播地址和子网掩码
# 处理 IPv6 地址
RESOLV_CONF="/etc/resolv.conf"
[ -e $RESOLV_CONF ] || touch $RESOLV_CONF
[ -n "$broadcast" ] && BROADCAST="broadcast $broadcast"
[ -n "$subnet" ] && NETMASK="netmask $subnet"
# Handle stateful DHCPv6 like DHCPv4
[ -n "$ipv6" ] && ip="$ipv6/128"
# 设置等待 IPv6 默认路由的超时时间,默认 10 秒
if [ -z "${IF_WAIT_DELAY}" ]; then
        IF_WAIT_DELAY=10
fi

# 作用: 等待 IPv6 默认路由出现
# 循环检查 IPv6 默认路由
# 每秒钟检查一次,最多等待 10 秒
# 显示进度点和超时提示
wait_for_ipv6_default_route() {
        printf "Waiting for IPv6 default route to appear"
        while [ $IF_WAIT_DELAY -gt 0 ]; do
                if [ -z "$(ip -6 route list | grep default)" ]; then
                        printf "\n"
                        return
                fi
                sleep 1
                printf "."
                : $((IF_WAIT_DELAY -= 1))
        done
        printf " timeout!\n"
}
# case 语句处理不同 DHCP 状态
# deconfig 状态 - 释放配置
# leasefail|nak 状态 - 租约失败
# renew|bound 状态 - 获取或更新租约
case "$1" in
        deconfig)
                /sbin/ifconfig $interface up
                /sbin/ifconfig $interface 0.0.0.0
# 启用网络接口但清除 IP 地址
# 从 resolv.conf 中删除该接口相关的 DNS 配置
# 使用临时文件安全地修改配置文件
                # drop info from this interface
                # resolv.conf may be a symlink to /tmp/, so take care
                TMPFILE=$(mktemp)
                grep -vE "# $interface\$" $RESOLV_CONF > $TMPFILE
                cat $TMPFILE > $RESOLV_CONF
                rm -f $TMPFILE
 # 如果存在 avahi-autoipd (零配置网络服务),则停止它
                if [ -x /usr/sbin/avahi-autoipd ]; then
                        /usr/sbin/avahi-autoipd -k $interface
                fi
                ;;
 
        leasefail|nak)
#DHCP 获取失败时,启动 avahi-autoipd 获取链路本地地址
                if [ -x /usr/sbin/avahi-autoipd ]; then
                        /usr/sbin/avahi-autoipd -wD $interface --no-chroot
                fi
                ;;
 
        renew|bound)
# 停止 avahi-autoipd(如果正在运行)
                if [ -x /usr/sbin/avahi-autoipd ]; then
                        /usr/sbin/avahi-autoipd -k $interface
                fi
# 配置接口的 IP 地址、广播地址和子网掩码
                /sbin/ifconfig $interface $ip $BROADCAST $NETMASK
# 如果有 IPv6 地址,等待默认路由出现
                if [ -n "$ipv6" ] ; then
                        wait_for_ipv6_default_route
                fi
# 删除接口上所有默认网关
# 添加 DHCP 提供的网关作为默认路由
                if [ -n "$router" ] ; then
                        echo "deleting routers"
                        while route del default gw 0.0.0.0 dev $interface 2> /dev/null; do
                                :
                        done
 
                        for i in $router ; do
                                route add default gw $i dev $interface
                        done
                fi
# 清理该接口之前的 DNS 配置
                # drop info from this interface
                # resolv.conf may be a symlink to /tmp/, so take care
                TMPFILE=$(mktemp)
                grep -vE "# $interface\$" $RESOLV_CONF > $TMPFILE
                cat $TMPFILE > $RESOLV_CONF
                rm -f $TMPFILE
# 设置搜索域(优先使用 RFC3397 选项 119)
                # prefer rfc3397 domain search list (option 119) if available
                if [ -n "$search" ]; then
                        search_list=$search
                elif [ -n "$domain" ]; then
                        search_list=$domain
                fi
 
                [ -n "$search_list" ] &&
                        echo "search $search_list # $interface" >> $RESOLV_CONF
#添加 DNS 服务器到 resolv.conf
                for i in $dns ; do
                        echo adding dns $i
                        echo "nameserver $i # $interface" >> $RESOLV_CONF
                done
                ;;
esac
# 钩子脚本执行
# 作用: 执行扩展钩子脚本
# 查找与当前脚本同名的 .d 目录
# 执行该目录下所有可执行文件,传递相同的参数
HOOK_DIR="$0.d"
for hook in "${HOOK_DIR}/"*; do
    [ -f "${hook}" -a -x "${hook}" ] || continue
    "${hook}" "${@}"
done
# 脚本成功退出
exit 0

default.script(无注释版):

#!/bin/sh
 
# udhcpc script edited by Tim Riker <Tim@Rikers.org>
 
[ -z "$1" ] && echo "Error: should be called from udhcpc" && exit 1
 
RESOLV_CONF="/etc/resolv.conf"
[ -e $RESOLV_CONF ] || touch $RESOLV_CONF
[ -n "$broadcast" ] && BROADCAST="broadcast $broadcast"
[ -n "$subnet" ] && NETMASK="netmask $subnet"
# Handle stateful DHCPv6 like DHCPv4
[ -n "$ipv6" ] && ip="$ipv6/128"
 
if [ -z "${IF_WAIT_DELAY}" ]; then
        IF_WAIT_DELAY=10
fi
 
wait_for_ipv6_default_route() {
        printf "Waiting for IPv6 default route to appear"
        while [ $IF_WAIT_DELAY -gt 0 ]; do
                if [ -z "$(ip -6 route list | grep default)" ]; then
                        printf "\n"
                        return
                fi
                sleep 1
                printf "."
                : $((IF_WAIT_DELAY -= 1))
        done
        printf " timeout!\n"
}
 
case "$1" in
        deconfig)
                /sbin/ifconfig $interface up
                /sbin/ifconfig $interface 0.0.0.0
 
                # drop info from this interface
                # resolv.conf may be a symlink to /tmp/, so take care
                TMPFILE=$(mktemp)
                grep -vE "# $interface\$" $RESOLV_CONF > $TMPFILE
                cat $TMPFILE > $RESOLV_CONF
                rm -f $TMPFILE
 
                if [ -x /usr/sbin/avahi-autoipd ]; then
                        /usr/sbin/avahi-autoipd -k $interface
                fi
                ;;
 
        leasefail|nak)
                if [ -x /usr/sbin/avahi-autoipd ]; then
                        /usr/sbin/avahi-autoipd -wD $interface --no-chroot
                fi
                ;;
 
        renew|bound)
                if [ -x /usr/sbin/avahi-autoipd ]; then
                        /usr/sbin/avahi-autoipd -k $interface
                fi
                /sbin/ifconfig $interface $ip $BROADCAST $NETMASK
                if [ -n "$ipv6" ] ; then
                        wait_for_ipv6_default_route
                fi
 
                if [ -n "$router" ] ; then
                        echo "deleting routers"
                        while route del default gw 0.0.0.0 dev $interface 2> /dev/null; do
                                :
                        done
 
                        for i in $router ; do
                                route add default gw $i dev $interface
                        done
                fi
 
                # drop info from this interface
                # resolv.conf may be a symlink to /tmp/, so take care
                TMPFILE=$(mktemp)
                grep -vE "# $interface\$" $RESOLV_CONF > $TMPFILE
                cat $TMPFILE > $RESOLV_CONF
                rm -f $TMPFILE
 
                # prefer rfc3397 domain search list (option 119) if available
                if [ -n "$search" ]; then
                        search_list=$search
                elif [ -n "$domain" ]; then
                        search_list=$domain
                fi
 
                [ -n "$search_list" ] &&
                        echo "search $search_list # $interface" >> $RESOLV_CONF
 
                for i in $dns ; do
                        echo adding dns $i
                        echo "nameserver $i # $interface" >> $RESOLV_CONF
                done
                ;;
esac
 
HOOK_DIR="$0.d"
for hook in "${HOOK_DIR}/"*; do
    [ -f "${hook}" -a -x "${hook}" ] || continue
    "${hook}" "${@}"
done
 
exit 0

关键状态处理

  1. bound/renew状态:获取新IP或更新现有IP

  2. deconfig状态:释放当前网络配置

  3. leasefail/nak状态:处理DHCP失败情况
     

三、Qt应用程序集成

3.1 启动DHCP客户端

在Qt中,我们通过QTimer来管理DHCP客户端:

void NetworkModule::UseDHCP()
{
    m_pTimer = new QTimer(this);
    connect(m_pTimer, SIGNAL(timeout()), this, SLOT(Slot_Timeout()));
    
    // 启动DHCP客户端
    system("udhcpc -b -s /opt/app/default.script -i eth0");
    
    m_pTimer->start(5000); // 5秒后检查结果
}

3.2 网络配置解析

通过系统调用和文件解析获取网络配置信息:

// 将IP地址字符串转换为unsigned long
unsigned long ipToLong(const char* ipStr)
{
	struct in_addr addr;
	if (inet_pton(AF_INET, ipStr, &addr) <= 0) {
		return 0;
	}
	return ntohl(addr.s_addr);
}

3.3 完整的配置获取函数

// 获取网络接口配置
int getNetworkConfiguration(const char* interface,
	unsigned long* ip,
	unsigned long* mask,
	unsigned long* gateway,
	unsigned long* dns)
{
	// 初始化输出参数
	if (ip) *ip = 0;
	if (mask) *mask = 0;
	// 获取IP和掩码
	int fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (fd < 0) {
		return 0; // 失败
	}

	struct ifreq ifr;
	strncpy(ifr.ifr_name, interface, IFNAMSIZ - 1);

	// 获取IP地址
	if (ioctl(fd, SIOCGIFADDR, &ifr) == 0) {
		struct sockaddr_in* ipaddr = (struct sockaddr_in*)&ifr.ifr_addr;
		if (ip) *ip = ntohl(ipaddr->sin_addr.s_addr);
	}

	// 获取子网掩码
	if (ioctl(fd, SIOCGIFNETMASK, &ifr) == 0) {
		struct sockaddr_in* netmask = (struct sockaddr_in*)&ifr.ifr_netmask;
		if (mask) *mask = ntohl(netmask->sin_addr.s_addr);
	}

	close(fd);

	// 获取网关 - 读取/proc/net/route
	FILE* routeFile = fopen("/proc/net/route", "r");
	if (routeFile) {
		char line[256];
		fgets(line, sizeof(line), routeFile); // 跳过标题行

		while (fgets(line, sizeof(line), routeFile)) {
			char iface[16];
			unsigned long dest, gw, flags;
			int refCnt, use, metric, mask_val;

			if (sscanf(line, "%15s %lx %lx %x %d %d %d %lx",
				iface, &dest, &gw, &flags, &refCnt, &use, &metric, &mask_val) >= 3) {
				if (strcmp(iface, interface) == 0 && dest == 0) {
					// 转换小端格式到大端格式
					if (gateway) {
						*gateway = ((gw & 0xFF) << 24) |
							((gw & 0xFF00) << 8) |
							((gw & 0xFF0000) >> 8) |
							((gw & 0xFF000000) >> 24);
					}
					break;
				}
			}
		}
		fclose(routeFile);
	}

	// 获取DNS - 读取/etc/resolv.conf
	FILE* resolvFile = fopen("/etc/resolv.conf", "r");
	if (resolvFile) {
		char line[256];
		while (fgets(line, sizeof(line), resolvFile)) {
			if (strncmp(line, "nameserver", 10) == 0) {
				char dnsStr[16];
				if (sscanf(line + 10, "%15s", dnsStr) == 1) {
					if (dns) *dns = ipToLong(dnsStr);
					break; // 只获取第一个DNS服务器
				}
			}
		}
		fclose(resolvFile);
	}

	// 如果网关未从/proc/net/route获取,尝试使用route命令
	if (gateway && *gateway == 0) {
		FILE* routeCmd = popen("route -n", "r");
		if (routeCmd) {
			char line[256];
			while (fgets(line, sizeof(line), routeCmd)) {
				if (strstr(line, "0.0.0.0") || strstr(line, "default")) {
					char dest[16], gw[16], mask_str[16], flags[16], iface[16];
					if (sscanf(line, "%15s %15s %15s %15s %*s %*s %*s %15s",
						dest, gw, mask_str, flags, iface) >= 2) {
						if (strcmp(iface, interface) == 0) {
							*gateway = ipToLong(gw);
							break;
						}
					}
				}
			}
			pclose(routeCmd);
		}
	}

	return 1;
}

3.4 定时器槽函数处理

void NetworkModule::Slot_Timeout()
{
    m_pTimer->stop();
    
    unsigned long ip = 0, mask = 0, gateway = 0, dns = 0;
    if (getNetworkConfiguration("eth0", &ip, &mask, &gateway, &dns))
    {
        // 字节序转换
        config_data->NetConfig.Ip = ((ip & 0xff000000) >> 24) | 
                                   ((ip & 0x00ff0000) >> 8) |
                                   ((ip & 0x0000ff00) << 8) | 
                                   ((ip & 0x000000ff) << 24);
        
        // 更新界面显示
        UpdateAddress();
    }
}

四、关键技术点详解

4.1 字节序处理

在网络编程中,需要正确处理主机字节序和网络字节序的转换:

4.2 多数据源配置获取

我们的实现从多个系统源获取网络配置,确保数据的准确性:

  1. ioctl系统调用:获取接口IP和掩码

  2. /proc/net/route:解析路由表获取网关

  3. /etc/resolv.conf:读取DNS服务器配置

  4. route命令备选:当文件解析失败时的备用方案

4.3 优化——可增加错误处理和超时机制

// 在DHCP请求中添加超时保护,并在模块中添加retryCount成员变量判断是否重试
// 在UseDHCP函数中,这个时间可依据实际情况修改
m_pTimer->start(5000); // 5秒超时
retryCount=0;


// 在Slot_Timeout函数中,可以对网络配置获取的容错处理
if (getNetworkConfiguration("eth0", &ip, &mask, &gateway, &dns)) {
    // 成功获取配置
} else {
    // 失败处理,可能重试或使用备用配置
    retryCount++;
    if(retryCount<=3)
    {
        m_pTimer->start(5000);
    }
    else
    {
        QMessageBox::warning(this, "错误", "无法获取网络配置");
    }
}

五、最终流程

第一步:创建DHCP配置脚本

1.1 创建脚本文件

打开终端,执行以下命令创建脚本文件:

sudo mkdir -p /opt/app
sudo nano /opt/app/default.script
1.2 复制粘贴default.script脚本内容

将本文default.script内容复制到default.script文件中(删除所有注释,只保留纯代码):

1.3 设置脚本权限
sudo chmod +x /opt/app/default.script

第二步:创建Qt网络模块

2.1 在头文件中声明变量和函数

在Qt项目中的头文件(如networksettings.h)中添加:

#include <QTimer>
#include <QObject>

class NetworkModule : public QObject
{
    Q_OBJECT

public:
    explicit NetworkModule(QObject *parent = nullptr);
    void UseDHCP();//!!!!!!!!!!!!添加

private slots:
    void Slot_Timeout();//!!!!!!!!!!!!添加

private:
    QTimer *m_pTimer;//!!!!!!!!!!!!添加
    // 更新界面显示
    void UpdateAddress(int type);//!!!!!!!!!!!!添加
};
2.2 在cpp文件中实现UseDHCP()函数
2.3 实现Slot_Timeout()函数

第三步:创建dhcp.cpp完整实现

创建dhcp.h和dhcp.cpp文件,实现所有网络配置相关的函数:
dhcp.h:

#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// IP地址字符串转unsigned long
unsigned long ipToLong(const char* ipStr)
// 获取网络接口配置
int getNetworkConfiguration(const char* interface,
                                         unsigned long* ip,
                                         unsigned long* mask,
                                         unsigned long* gateway,
                                         unsigned long* dns)

dhcp.cpp文件中对函数进行实现。

测试步骤

  1. 编译项目:在Qt Creator中编译你的项目

  2. 运行程序:启动应用程序

  3. 点击DHCP按钮:在界面中点击获取DHCP配置的按钮

  4. 检查结果:等待5秒后查看网络配置是否更新

常见问题解决

问题1:脚本权限不足
sudo chmod +x /opt/app/default.script
问题2:udhcpc命令不存在

bash

# Ubuntu/Debian
sudo apt-get install udhcpc

# CentOS/RHEL  
sudo yum install udhcpc
问题3:网络接口名称不对

如果你的网络接口不是eth0,请修改代码中的接口名称:

  • 查看可用接口:ip addr show

  • 修改代码中的"eth0"为你的接口名称

六、总结

本文详细介绍了在Linux平台上使用Qt实现DHCP自动获取网络配置的完整方案。通过结合udhcpc客户端、自定义配置脚本和Qt应用程序,我们实现了一个稳定可靠的网络配置管理系统。

方案优势:

  1. 轻量高效:使用BusyBox的udhcpc,资源占用小

  2. 功能完整:支持IP、网关、DNS等完整网络配置

  3. 界面友好:Qt提供良好的用户交互体验

  4. 稳定可靠:完善的错误处理和超时机制

这种方案特别适合嵌入式设备、网络设备和物联网设备等需要自动网络配置的应用场景。通过本文的介绍,开发者可以快速在自己的项目中实现类似的DHCP网络配置功能。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐