上一小節介紹了以太網幀的結構,以及幀中各個字段的作用。參與以太網通訊的實體,由以太網地址唯一標識。以太網地址也叫做 MAC 地址,我們對它仍知之甚少。
以太網地址在不同場景,稱謂也不一樣,常用叫法包括這些:
- 以太網地址
- MAC 地址
- 硬件地址
- 物理地址
- 網卡地址
網卡
在以太網中,每臺主機都需要安裝一個物理設備並通過網線連接到一起:
這個設備就是 網卡 ( NIC ),網絡接口卡 ( network interface card )的簡稱。有些文獻也將網卡稱爲 網絡接口控制器 ( network interface controller )。
從物理的層面看,網卡負責將比特流轉換成電信號發送出去; 反過來,也負責將檢測到的電信號轉換成比特流並接收。
從軟件的層面看,發送數據時,內核協議棧負責封裝以太網幀(填充 目的地址 , 源地址 , 類型 和 數據 並計算 校驗和),並調用網卡驅動發送; 接收數據時,負責驗證 目的地址 、 校驗和 並取出數據部分,交由上層協議棧處理。
每塊網卡出廠時,都預先分配了一個全球唯一的 MAC地址 ,並燒進硬件。 不管後來網卡身處何處,接入哪個網絡,MAC 地址均不變。 當然,某些操作系統也允許修改網卡的 MAC 地址。
MAC地址
MAC 地址由 6 個字節組成( 48 位),可以唯一標識 $2^{48}$ ,即 281474976710656 個網絡設備(比如網卡)。
MAC 地址 6 個字節可以劃分成兩部分,如下圖:
- 3 字節長的 廠商代碼 ( OUI ),由國際組織分配給不同的網絡設備商;
- 3 字節長的 序列號 ( SN ),由廠商分配給它生產的網絡設備;
廠商代碼和序列號都是唯一分配,因此 MAC 地址是 全球唯一 的。
冒分十六進制表示法
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 系統調用獲取網絡設備信息,大致步驟如下:
- 創建一個套接字,任意類型均可;
- 準備 ifreq 結構體,用於保存網卡設備信息;
- 將待查詢網卡名填充到 ifreq 結構體;
- 調用 ioctl 系統調用,向套接字發起
SIOCGIFHWADDR
請求,獲取物理地址; - 如無錯漏,內核將被查詢網卡的物理地址填充在 ifreq 結構體 ifr_hwaddr 字段中;
最後,附上一個完整的例子:
#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 地址轉換成冒分十六進制形式。
【小菜學網絡】系列文章首發於公衆號【小菜學編程】,敬請關注: