网络编程系列:C++使用Linux的API快速获取网卡信息详解(AddressInfo)
前言
这个算是笔者新开的一个短期小坑?因为笔者决定制作一个小的网络库给我后面开发一些小玩具使用,因此,这里是一个文档存档,记录笔者思路用的小玩意。
如何在Linux下获取我们的网卡基本地址
一般而言,我们会使用一个好东西叫做getifaddrs,他的函数签名和说明笔者放到下面:
/* Create a linked list of `struct ifaddrs' structures, one for each
network interface on the host machine. If successful, store the
list in *IFAP and return 0. On errors, return -1 and set `errno'.
The storage returned in *IFAP is allocated dynamically and can
only be properly freed by passing it to `freeifaddrs'. */
extern int getifaddrs (struct ifaddrs **__ifap) __THROW;
/* Reclaim the storage allocated by a previous `getifaddrs' call. */
extern void freeifaddrs (struct ifaddrs *__ifa) __THROW;
这是两个C的API,前者获取我们的ifaddrs,后者将我们的申请给释放掉。这里,我们会得到一个ifaddrs链表,遍历这个链表后,我们就能如期的拿到外面想要的信息:
/* The `getifaddrs' function generates a linked list of these structures.
Each element of the list describes one network interface. */
struct ifaddrs
{
struct ifaddrs *ifa_next; /* Pointer to the next structure. */
char *ifa_name; /* Name of this network interface. */
unsigned int ifa_flags; /* Flags as from SIOCGIFFLAGS ioctl. */
struct sockaddr *ifa_addr; /* Network address of this interface. */
struct sockaddr *ifa_netmask; /* Netmask of this interface. */
union
{
/* At most one of the following two is valid. If the IFF_BROADCAST
bit is set in `ifa_flags', then `ifa_broadaddr' is valid. If the
IFF_POINTOPOINT bit is set, then `ifa_dstaddr' is valid.
It is never the case that both these bits are set at once. */
struct sockaddr *ifu_broadaddr; /* Broadcast address of this interface. */
struct sockaddr *ifu_dstaddr; /* Point-to-point destination address. */
} ifa_ifu;
/* These very same macros are defined by <net/if.h> for `struct ifaddr'.
So if they are defined already, the existing definitions will be fine. */
# ifndef ifa_broadaddr
# define ifa_broadaddr ifa_ifu.ifu_broadaddr
# endif
# ifndef ifa_dstaddr
# define ifa_dstaddr ifa_ifu.ifu_dstaddr
# endif
void *ifa_data; /* Address-specific data (may be unused). */
};
这是我们需要遍历的结构体成员。下面笔者一个一个说这些的
struct ifaddrs { struct ifaddrs *ifa_next; // 下一个节点指针,形成链表 char *ifa_name; // 接口名称,如 "eth0"、"lo" 等 unsigned int ifa_flags; // 接口标志(SIOCGIFFLAGS),如 IFF_UP、IFF_LOOPBACK 等 struct sockaddr *ifa_addr; // 接口地址,如 IPv4 或 IPv6 地址 struct sockaddr *ifa_netmask; // 子网掩码 union { struct sockaddr *ifu_broadaddr; // 广播地址(iff_flags 设置了 IFF_BROADCAST 时有效) struct sockaddr *ifu_dstaddr; // 点对点地址(iff_flags 设置了 IFF_POINTOPOINT 时有效) } ifa_ifu; void *ifa_data; // 接口附加信息(通常是系统内部使用,可以为 NULL) };🔁 ifa_next
- 类型:
struct ifaddrs*- 说明:指向下一个
ifaddrs节点,用于遍历链表。
🔠 ifa_name
- 类型:
char*- 说明:接口的名称,常见如
"eth0"、"lo"、"wlan0"等。
🚩 ifa_flags
- 类型:
unsigned int- 说明:接口标志,来源于
SIOCGIFFLAGSioctl。- 常见值(定义于
<net/if.h>):
IFF_UP:接口已启用。IFF_LOOPBACK:环回接口(如 lo)。IFF_BROADCAST:支持广播。IFF_POINTOPOINT:点对点链路。IFF_MULTICAST:支持多播。可用
ifa_flags & IFF_XXX判断特性。
🌐 ifa_addr
- 类型:
struct sockaddr*- 说明:接口的主地址(IP 地址),支持 IPv4(
AF_INET)、IPv6(AF_INET6)等。
🎭 ifa_netmask
- 类型:
struct sockaddr*- 说明:接口的子网掩码。类型通常与
ifa_addr相同。
📢 ifa_ifu 联合体(广播 / 点对点地址)
union { struct sockaddr *ifu_broadaddr; // 广播地址 struct sockaddr *ifu_dstaddr; // 点对点目的地址 } ifa_ifu;只会有一个有效:
- 如果
ifa_flags & IFF_BROADCAST,则ifu_broadaddr有效。- 如果
ifa_flags & IFF_POINTOPOINT,则ifu_dstaddr有效。为方便使用,下面两个宏被定义(来自
<net/if.h>):#define ifa_broadaddr ifa_ifu.ifu_broadaddr #define ifa_dstaddr ifa_ifu.ifu_dstaddr
📎 ifa_data
- 类型:
void*- 说明:通常为 NULL,或指向某些协议相关的数据,系统依赖实现。
- 很少使用,可忽略。
总而言之就是下面一张表:
字段 含义 ifa_next链表下一项 ifa_name接口名称 ifa_flags接口属性(是否up、多播等) ifa_addr接口地址(IP) ifa_netmask子网掩码 ifa_broadaddr/ifa_dstaddr广播地址 / 点对点目的地址 ifa_data附加数据(可忽略)
这里是我看的一本书,叫做《Hands-On Network Programming with C》(Lewis Van Winkle著)的一个改编的小例子。
#include <ifaddrs.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
int main() {
struct ifaddrs *ifaddr, *ifa;
char addr_buf[INET6_ADDRSTRLEN];
if (getifaddrs(&ifaddr) == -1) {
perror("getifaddrs");
return 1;
}
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
if (!ifa->ifa_addr) continue;
int family = ifa->ifa_addr->sa_family;
printf("Interface: %s\n", ifa->ifa_name);
if (family == AF_INET || family == AF_INET6) {
void *addr;
if (family == AF_INET)
addr = &((struct sockaddr_in *)ifa->ifa_addr)->sin_addr;
else
addr = &((struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr;
inet_ntop(family, addr, addr_buf, sizeof(addr_buf));
printf(" Address: %s\n", addr_buf);
}
printf(" Flags: 0x%x\n", ifa->ifa_flags);
}
freeifaddrs(ifaddr);
return 0;
}
补充:如何在Linux下获取我们的网卡MAC地址
笔者的一位朋友跟我聊过Linux获取网卡的MAC地址的事情,这里只给出在Linux下快速获取我们的MAC地址,我们知道,上面的接口一定对MAC地址是无力的,很简单,因为它属于数据链路层/物理层的部分了,属于硬件的部分,要向硬件驱动要。
询问硬件驱动,我们很容易想到Linux下的通用接口ioctl,我们传入对应的property后,返回我们关心的数据。
const char* ifname = "wlan0"; // ifname 是接口名称,我们可以在上面的基础上进一步咨询MAC地址
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
return "";
struct ifreq ifr;
std::strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
ifr.ifr_name[IFNAMSIZ - 1] = '\0';
if (ioctl(sock, SIOCGIFHWADDR, &ifr) != 0) {
close(sock);
return "";
}
close(sock); // 释放掉我们的socket
// 我们到这里,就拿到了我们期待的MAC地址了
unsigned char* mac = reinterpret_cast<unsigned char*>(ifr.ifr_hwaddr.sa_data);
构建基本的抽象
#pragma once
#include "common/protocals_enum.h"
#include <string>
#include <vector>
/**
* @brief AddressInfo collects the net card's status
*
*/
struct AddressInfo {
ProtocolEnum::IPVersionType ip_version_type; ///< ip version type
ProtocolEnum::TransferType transfer_type; ///< transfer type
ProtocolEnum::NetState net_state; ///< network status, on or off
ProtocolEnum::NetType net_type; ///< net type
std::string address_string; ///< address
std::string interface_string; ///< interface name
std::string mac_addr; ///< mac if existed
};
/**
* @brief AddressInfoQuery query the address info
*
*/
struct AddressInfoQuery {
static std::vector<AddressInfo> query_local(); ///< query local
};
我们需要做的就是封装上面的小例子,转化成一个得到的AddressInfo动态数组返回本地的信息。
#include "address_iterator.h"
#include "common/protocals_enum.h"
#include "unistd.h"
#include <arpa/inet.h>
#include <cstring>
#include <ifaddrs.h>
#include <iomanip>
#include <net/if.h>
#include <netinet/in.h>
#include <netpacket/packet.h>
#include <sstream>
#include <stdexcept>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <vector>
namespace {
std::string get_mac_address_string(const char* ifname) {
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
return "";
struct ifreq ifr;
std::strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
ifr.ifr_name[IFNAMSIZ - 1] = '\0';
if (ioctl(sock, SIOCGIFHWADDR, &ifr) != 0) {
close(sock);
return "";
}
close(sock);
unsigned char* mac = reinterpret_cast<unsigned char*>(ifr.ifr_hwaddr.sa_data);
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (int i = 0; i < 6; ++i) {
oss << std::setw(2) << static_cast<int>(mac[i]);
if (i != 5)
oss << ":";
}
return oss.str();
}
}
std::vector<AddressInfo> AddressInfoQuery::query_local() {
std::vector<AddressInfo> results;
struct ifaddrs* ifaddr;
if (::getifaddrs(&ifaddr) == -1) {
throw std::invalid_argument("Can not candidate the getifaddrs");
}
for (ifaddrs* current = ifaddr; current != nullptr; current = current->ifa_next) {
// now iterate the current sessions
if (!current->ifa_addr)
continue; // not in the scope
AddressInfo info;
info.interface_string = current->ifa_name;
info.mac_addr = get_mac_address_string(current->ifa_name);
int raw_family_type = current->ifa_addr->sa_family;
if (raw_family_type == AF_INET || raw_family_type == AF_INET6) {
char tmp_buffer[INET6_ADDRSTRLEN];
void* src;
if (raw_family_type == AF_INET) {
src = &((struct sockaddr_in*)current->ifa_addr)->sin_addr;
info.ip_version_type = ProtocolEnum::IPVersionType::IPv4;
} else {
src = &((struct sockaddr_in6*)current->ifa_addr)->sin6_addr;
info.ip_version_type = ProtocolEnum::IPVersionType::IPv6;
}
inet_ntop(raw_family_type, src, tmp_buffer, sizeof(tmp_buffer));
info.address_string = tmp_buffer;
} else {
info.address_string = current->ifa_addr->sa_data;
}
if (current->ifa_flags & IFF_UP) {
info.net_state = ProtocolEnum::NetState::ON;
} else {
info.net_state = ProtocolEnum::NetState::OFF;
}
// types
ProtocolEnum::NetType result = ProtocolEnum::NetType::Normal;
if (current->ifa_flags & IFF_LOOPBACK)
result = static_cast<ProtocolEnum::NetType>(result | ProtocolEnum::NetType::LoopBack);
if (current->ifa_flags & IFF_BROADCAST)
result = static_cast<ProtocolEnum::NetType>(result | ProtocolEnum::NetType::BroadCastable);
if (current->ifa_flags & IFF_MULTICAST)
result = static_cast<ProtocolEnum::NetType>(result | ProtocolEnum::NetType::MultiCastable);
info.net_type = result;
results.emplace_back(info);
}
::freeifaddrs(ifaddr);
return results;
}
query_local() 方法是本模块的核心,它通过系统调用 getifaddrs() 获取本地所有网络接口的详细信息,并将其封装为 std::vector<AddressInfo> 返回。每个 AddressInfo 结构体代表一个网络接口。
get_mac_address_string的基本工作原理
作用
此辅助函数用于根据网络接口的名称(例如 "eth0")获取其对应的 MAC 地址 并以字符串形式返回。
工作原理
- 创建 Socket: 它首先创建一个
AF_INET类型的DGRAM(数据报) socket。这个 socket 仅用于进行系统控制操作,不用于实际网络通信。 - 准备
ifreq结构体:struct ifreq是一个通用的网络设备控制结构体。函数将传入的接口名称拷贝到ifr.ifr_name字段。 - 调用
ioctl: 通过ioctl系统调用,并使用SIOCGIFHWADDR命令,向内核查询指定接口的硬件地址(MAC 地址)。查询结果会填充到ifr.ifr_hwaddr字段中。 - MAC 地址格式化: 获取到原始的 MAC 地址(通常是
unsigned char数组)后,函数会将其格式化为标准的十六进制冒号分隔字符串(例如 "00:11:22:AA:BB:CC"),并返回。 - 错误处理: 如果 socket 创建失败或
ioctl调用失败,函数将返回空字符串。
queryLocal方法的核心原理
工作原理
- 获取接口地址列表:
- 函数首先调用
::getifaddrs(&ifaddr)系统函数。这是一个 POSIX 标准函数,用于获取系统中所有网络接口的配置信息,并以链表的形式存储在ifaddr指向的struct ifaddrs结构体中。 - 如果
getifaddrs()调用失败,将抛出std::invalid_argument异常,指示无法获取网络接口信息。
- 函数首先调用
- 遍历接口链表:
- 函数通过循环遍历
ifaddr链表,current指针逐个指向链表中的每个ifaddrs结构体,直到链表末尾 (nullptr)。 - 跳过无效地址: 在每次循环开始时,会检查
current->ifa_addr是否为nullptr。如果为nullptr,则表示当前接口没有关联的地址信息(例如,某些虚拟接口或链路层接口可能没有IP地址),因此会跳过当前项。
- 函数通过循环遍历
- 填充
AddressInfo结构: 对于每个有效的网络接口,函数会创建一个AddressInfo对象并填充其成员:info.interface_string: 直接从current->ifa_name获取接口名称。info.mac_addr: 调用前述的get_mac_address_string()辅助函数,传入接口名称以获取其 MAC 地址。- IP 地址和版本 (
info.address_string,info.ip_version_type):- 通过检查
current->ifa_addr->sa_family来判断地址族是 IPv4 (AF_INET) 还是 IPv6 (AF_INET6)。 - 根据地址族,将
current->ifa_addr强制转换为struct sockaddr_in或struct sockaddr_in6,并提取出sin_addr或sin6_addr(即实际的 IP 地址)。 - 使用
inet_ntop()函数将二进制的 IP 地址转换为可读的字符串形式,存储在info.address_string中。 - 设置
info.ip_version_type为IPv4或IPv6。 - 其他地址族: 如果
sa_family既不是AF_INET也不是AF_INET6(例如,AF_PACKET用于链路层),则直接将current->ifa_addr->sa_data的内容(原始数据)赋给info.address_string。
- 通过检查
- 网络状态 (
info.net_state):- 通过检查
current->ifa_flags中是否设置了IFF_UP标志来判断接口是否处于“启用”状态。 - 相应地设置
info.net_state为ProtocolEnum::NetState::ON或ProtocolEnum::NetState::OFF。
- 通过检查
- 网络类型 (
info.net_type):- 通过按位或 (
|) 操作组合ProtocolEnum::NetType枚举值来表示接口支持的多种类型。 - 检查
ifa_flags中的IFF_LOOPBACK(回环)、IFF_BROADCAST(广播能力)和IFF_MULTICAST(多播能力)标志,并将相应的类型添加到info.net_type中。
- 通过按位或 (
- 添加结果并释放资源:
- 填充完
AddressInfo对象后,将其添加到results向量中。 - 循环结束后,调用
::freeifaddrs(ifaddr)释放由getifaddrs()分配的链表内存,防止内存泄漏。 - 最后,返回包含所有查询结果的
results向量。
- 填充完