上一小节介绍了以太网帧的结构,以及帧中各个字段的做用。参与以太网通信的实体,由以太网地址惟一标识。以太网地址也叫作 MAC 地址,咱们对它仍知之甚少。编程
以太网地址在不一样场景,称谓也不同,经常使用叫法包括这些:网络
- 以太网地址
- MAC 地址
- 硬件地址
- 物理地址
- 网卡地址
在以太网中,每台主机都须要安装一个物理设备并经过网线链接到一块儿:socket
这个设备就是 网卡 ( NIC ),网络接口卡 ( network interface card )的简称。有些文献也将网卡称为 网络接口控制器 ( network interface controller )。函数
从物理的层面看,网卡负责将比特流转换成电信号发送出去; 反过来,也负责将检测到的电信号转换成比特流并接收。工具
从软件的层面看,发送数据时,内核协议栈负责封装以太网帧(填充 目的地址 , 源地址 , 类型 和 数据 并计算 校验和),并调用网卡驱动发送; 接收数据时,负责验证 目的地址 、 校验和 并取出数据部分,交由上层协议栈处理。oop
每块网卡出厂时,都预先分配了一个全球惟一的 MAC地址 ,并烧进硬件。 无论后来网卡身处何处,接入哪一个网络,_MAC_ 地址均不变。 固然,某些操做系统也容许修改网卡的 MAC 地址。学习
MAC 地址由 6 个字节组成( _48_ 位),能够惟一标识 $2^{48}$ ,即 281474976710656 个网络设备(好比网卡)。spa
MAC 地址 _6_ 个字节能够划分红两部分,以下图:操作系统
厂商代码和序列号都是惟一分配,所以 MAC 地址是 全球惟一 的。code
MAC 地址 6 个字节如何展现呢? 是否可以做为 ASCII 来解读并显示?
恐怕不能。一个字节总共有 8 个位,而 ASCII 只定义了其中的 7 位。何况 ASCII 中定义了不少控制字符,能显示的也只有字母、数字以及一些经常使用符号。以上述地址为例,只有 0x5B
这个字节是能够显示的,对应着字符 [
。
好在,咱们能够用多个可读字符来表示一个原始字节。咱们将一个字节分红两部分,高 4
位以及低 4
位,每部分能够用一个十六进制字符来表示。以 0x00
这个字节为例,能够用两个字符 00
表示:
这样一来,整个地址能够用一个 12 字节长的字符串表示: 0010A4BA875B
。 为了进一步提升可读性,能够在中间插入冒号 :
: 00:10:A4:BA:87:5B
。
这就是 冒分十六进制表示法 ( colon hexadecimal notation )。
注意到,冒分十六进制总共须要 17
个字节。 若是算上字符串结尾处的 \0
,将达到 18 个字节,原始 MAC 地址的整整 3 倍!顺便提一下,十六进制字母字符用大小写均可以。
Linux 上有很多工具命令能够查看系统当前接入的网卡以及每张网卡的详细信息。
首先是 ifconfig 命令,他默认显示已启用的网卡,详情中能够看到每张网卡的物理地址:
fasion@u2004 [ ~ ] ➜ ifconfig enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 10.0.2.15 netmask 255.255.255.0 broadcast 10.0.2.255 inet6 fe80::a00:27ff:fe49:50dd prefixlen 64 scopeid 0x20<link> ether 08:00:27:49:50:dd txqueuelen 1000 (Ethernet) RX packets 3702 bytes 4881568 (4.8 MB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 538 bytes 42999 (42.9 KB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 enp0s8: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.56.2 netmask 255.255.255.0 broadcast 192.168.56.255 inet6 fe80::a00:27ff:fe56:831c prefixlen 64 scopeid 0x20<link> ether 08:00:27:56:83:1c txqueuelen 1000 (Ethernet) RX packets 4183 bytes 1809871 (1.8 MB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 2674 bytes 350013 (350.0 KB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 inet6 ::1 prefixlen 128 scopeid 0x10<host> loop txqueuelen 1000 (Local Loopback) RX packets 679 bytes 1510416 (1.5 MB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 679 bytes 1510416 (1.5 MB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
例子中,系统总共有 3 块已启用网卡,名字分别是 enp0s3 、 enp0s8 以及 lo 。其中 lo 是环回网卡,用于本机通信。ether 08:00:27:49:50:dd
代表,网卡 enp0s3 的物理地址是 08:00:27:49:50:dd
。
请注意,_ifconfig_ 是一个比较老旧的命令,正在慢慢淡出历史舞台。
ip 命令也能够查看系统网卡信息,默认显示全部网卡:
fasion@u2004 [ ~ ] ➜ ip link 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 08:00:27:49:50:dd brd ff:ff:ff:ff:ff:ff 3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 08:00:27:56:83:1c brd ff:ff:ff:ff:ff:ff
ip 命令输出信息比较紧凑, link/ether 08:00:27:49:50:dd
这行展现网卡的物理地址。
ip 命令是一个比较新的命令,功能很是强大。它除了能够用于管理网络设备,还能够用于管理路由表,策略路由以及各类隧道。所以,推荐重点学习掌握 ip 命令的用法。
若是程序中须要用到网卡地址,如何获取呢?
有个方法是执行 ip 命令输出网卡详情,而后从输出信息中截取网卡地址。例如:
fasion@u2004 [ ~ ] ➜ ip link show dev enp0s3 | grep 'link/ether' | awk '{print $2}' 08:00:27:49:50:dd
这种方法多用于 Shell 编程中。
更优雅的办法是经过套接字编程,直接向操做系统获取。_Linux_ 套接字支持经过 ioctl 系统调用获取网络设备信息,大体步骤以下:
SIOCGIFHWADDR
请求,获取物理地址;最后,附上一个完整的例子:
#include <net/if.h> #include <stdio.h> #include <string.h> #include <sys/ioctl.h> #include <sys/socket.h> /** * Convert binary MAC address to readable format. * * Arguments * n: binary format, must be 6 bytes. * * a: buffer for readable format, 18 bytes at least(`\0` included). **/ void mac_ntoa(unsigned char *n, char *a) { // traverse 6 bytes one by one sprintf(a, "%02x:%02x:%02x:%02x:%02x:%02x", n[0], n[1], n[2], n[3], n[4], n[5]); } int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "no iface given\n"); return 1; } // create a socket, any type is ok int s = socket(AF_INET, SOCK_STREAM, 0); if (-1 == s) { perror("Fail to create socket"); return 2; } // fill iface name to struct ifreq struct ifreq ifr; strncpy(ifr.ifr_name, argv[1], 15); // call ioctl to get hardware address int ret = ioctl(s, SIOCGIFHWADDR, &ifr); if (-1 == ret) { perror("Fail to get mac address"); return 3; } // convert to readable format char mac[18]; mac_ntoa((unsigned char *)ifr.ifr_hwaddr.sa_data, mac); // output result printf("IFace: %s\n", ifr.ifr_name); printf("MAC: %s\n", mac); return 0; }
其中,_mac_ntoa_ 函数调用字符串格式化函数 sprintf 将原始 MAC 地址转换成冒分十六进制形式。
【小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注: