https://bitmingw.com/2018/05/06/get-ip-address-of-network-interface-in-python/
在 StackOverflow 上流傳着這樣一份用 Python 獲取網卡 IPv4 地址的神秘代碼。
import socket
import fcntl
import struct
def get_ip_address(ifname):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
return socket.inet_ntoa(fcntl.ioctl(
s.fileno(),
0x8915,
struct.pack('256s', ifname[:15]))[20:24])
get_ip_address('eth0')
網卡名字可以從/sys/class/net或者/proc/net/dev中找到。
但是,很少有人知道這段代碼是如何工作的。本文將為你揭開這段代碼的神秘面紗。
Python socket
Python 的 socket 模塊提供了有關網絡接口的底層控制方法。socket.socket 函數會創建一個新的 socket 對象。它的用法如下:
1 |
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None) |
第一個參數 family 指定了網絡地址的類型。最常用的值有兩個:默認值 AF_INET 對應 IPv4,AF_INET6 是 IPv6。
第二個參數 type 代表了傳輸層協議的類型。默認值 SOCK_STREAM 是我們熟知的 TCP 協議,而 SOCK_DGRAM 則對應 UDP 協議。
因此,s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 的含義是,創建一個使用 IPv4 網絡和 UDP 協議的 socket 對象 s。
為了獲取網卡的 IP 地址,創建 TCP socket 或是 UDP socket 是沒有差別的。由於 socket.AF_INET 和 socket.SOCK_STREAM 都是 socket.socket 函數的默認參數,所以這一行實際上可以簡寫成
1 |
s = socket.socket() |
socket 對象 s 創建后,可以通過 s.fileno() 獲取這個 socket 的文件描述符(file descriptor)。
另外,代碼中的 socket.inet_ntoa 函數把一個 4 字節 IP 地址(即 struct in_addr)轉化成點分十進制的可讀形式。
現在,我們可以推斷出,從 fcntl.ioctl 到 [20:24] 這一大段內容,是用來獲取網卡對應的 4 字節 IP 地址的。
fcntl 與 ioctl
fcntl 與 ioctl 是 UNIX/Linux 系統中用於文件控制和 I/O 控制的兩個系統調用。Python 在此基礎上進行了封裝。函數 ioctl 的用法如下:
fcntl.ioctl(fd, request, arg=0, mutate_flag=True)
參數 fd 是我們想控制的文件的文件描述符。在 UNIX/Linux 系統中,I/O 設備也用文件來表示,因此這里需要傳入 socket 的文件操作符。
第二個參數 request 是我們想進行的操作。這個操作由一個預定義的 32 位整數表示。代碼中使用的 0x8915 在 /usr/include/linux/sockios.h 文件中定義,它對應的符號是 SIOCGIFADDR,我們正是通過這一操作來取得 IPv4 地址。想要查詢 ioctl 支持的所有操作,可以在命令行中輸入 man ioctl_list。
第三個參數 arg 是操作所需的參數,這通常是一個 32 位整數或一段二進制內容。根據文檔(man netdevice),使用 SIOCGIFADDR 時需要傳入的參數是結構體 struct ifreq。struct ifreq 的定義位於 /usr/include/net/if.h
1 |
struct ifreq { |
在我的計算機上,IFNAMSIZ 的值是 16,即網卡名稱最長為 15 字節(第16個字節必須是 \0 用來表示字符串的結尾),而 struct ifreq 的大小是 40 字節。對於 SIOCGIFADDR 來說,只有前 16 個字節 ifr_name 是有意義的,后面的值都可以設定為 0x00。
操作 SIOCGIFADDR 返回的結果也是 struct ifreq 結構體。其中,網卡的 IPv4 地址信息包含在 struct sockaddr ifr_addr 結構體內。這個 4 字節的 IP 地址位於 struct ifreq 結構體 20-23 字節處。所以我們會看到,fcntl.ioctl 返回的結果后面有 [20:24] ——只需要把這 4 個字節拿去轉換就可以了。
到現在為止,如果我們可以正確生成 40 字節的 struct ifreq 結構體,就可以通過 ioctl 拿到 IPv4 的地址。生成結構體需要用到 Python 的 struct 模塊。
Python struct 與 unicode string
Python 的 struct 模塊用於生成和解析二進制內容。struct.pack 的用法如下:
1 |
struct.pack(fmt, v1, v2, ...) |
這個函數比較像 printf,第一個參數用於設定格式,后續的參數用於填充內容。
struct.pack('256s', ifname[:15]) 用 ifname 的前 15 個字節填充了一個 256 字節的二進制空間,未指定內容的空間會用字節 0x00 填充。事實上,由於 struct ifreq 的大小只有 40 字節,將 256s 換成 40s 也能得到期望的 struct ifreq 結構體。
最后我們來講一講字符串的問題。Python 2 是不區分 str 和 bytes 的,所以 ifname這個字符串可以直接拿來當一組字節用。代碼中的 ifname[:15] 是一種防御性的措施,即只保留前 15 個字節。如果確信用戶的輸入合法,直接使用 ifname 也可以。但是在 Python 3 中,由於字符串不能隱式地當作一組字節用,所以需要額外的轉換,具體來說就是把
1 |
# Python 2 |
變成
1 |
# Python 3 |
其中 utf-8 是字符串 ifname 的編碼方法。
自學之道
看到這里,你可能回想,如果今后遇到類似的代碼,應該如何分析它背后的原理呢?技法不外乎兩條,一是多看文檔,二是多讀源碼。
本文例子中使用的 Python 函數,在官方文檔中有詳細的表述。細心閱讀之后,不難搞清使用每個函數的意圖,並進一步推斷該函數的參數需滿足的條件,以及返回值的形態。
當涉及到 ioctl 等 UNIX/Linux 系統調用的時候,僅僅依靠閱讀文檔(而且這種文檔可能不太容易找到)是不能完全掌握代碼意圖的。涉及到具體邏輯,參數與返回值,結構體內容與大小等細節問題時,就需要研讀源碼,探尋蛛絲馬跡。研讀源碼時,要有的放矢,先用搜索縮小范圍,再逐行精讀。有時,甚至需要通過動手實驗(例如用 sizeof查看結構體大小)來嘗試發現新的線索。
看文檔與讀源碼,都是需要很多耐心的工作。剖析代碼可能動輒需要幾個小時的時間,但當真相大白之時,相信你能夠感受到那豁然開朗的快樂。
struct sockaddr和struct sockaddr_in這兩個結構體用來處理網絡通信的地址。
在各種系統調用或者函數中,只要和網絡地址打交道,就得用到這兩個結構體。
網絡中的地址包含3個方面的屬性:
1 地址類型: ipv4還是ipv6
2 ip地址
3 端口
相應的,頭文件有如下定義:
- include <netinet/in.h>
- struct sockaddr {
- unsigned short sa_family; // 2 bytes address family, AF_xxx
- char sa_data[14]; // 14 bytes of protocol address
- };
- // IPv4 AF_INET sockets:
- struct sockaddr_in {
- short sin_family; // 2 bytes e.g. AF_INET, AF_INET6
- unsigned short sin_port; // 2 bytes e.g. htons(3490)
- struct in_addr sin_addr; // 4 bytes see struct in_addr, below
- char sin_zero[8]; // 8 bytes zero this if you want to
- };
- struct in_addr {
- unsigned long s_addr; // 4 bytes load with inet_pton()
- };
注釋中標明了屬性的含義及其字節大小,這兩個結構體一樣大,都是16個字節,而且都有family屬性,不同的是:
sockaddr用其余14個字節來表示sa_data,而sockaddr_in把14個字節拆分成sin_port, sin_addr和sin_zero
分別表示端口、ip地址。sin_zero用來填充字節使sockaddr_in和sockaddr保持一樣大小。
sockaddr和sockaddr_in包含的數據都是一樣的,但他們在使用上有區別:
程序員不應操作sockaddr,sockaddr是給操作系統用的
程序員應使用sockaddr_in來表示地址,sockaddr_in區分了地址和端口,使用更方便。
一般的用法為:
程序員把類型、ip地址、端口填充sockaddr_in結構體,然后強制轉換成sockaddr,作為參數傳遞給系統調用函數
網絡編程中一段典型的代碼為:
- int sockfd;
- struct sockaddr_in servaddr;
- sockfd = Socket(AF_INET, SOCK_STREAM, 0);
- /* 填充struct sockaddr_in */
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_port = htons(SERV_PORT);
- inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
- /* 強制轉換成struct sockaddr */
- connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));

