Go 語言實現(xiàn)簡易版 netstat 命令
netstat 使用 go 語言實現(xiàn)是什么操作?本文從 netstat 原理出發(fā)詳細解讀了這一實踐。
netstat 工作原理
netstat 命令是 linux 系統(tǒng)中查看網(wǎng)絡情況的一個命令。比如我們可以通過netstat \-ntlp | grep 8080查看監(jiān)聽 8080 端口的進程。
netstat 工作原理如下:
- 通過讀取/proc/net/tcp 、/proc/net/tcp6 文件,獲取 socket 本地地址,本地端口,遠程地址,遠程端口,狀態(tài),inode 等信息
 - 接著掃描所有/proc/[pid]/fd 目錄下的的 socket 文件描述符,建立 inode 到進程 pid 映射
 - 根據(jù) pid 讀取/proc/[pid]/cmdline 文件,獲取進程命令和啟動參數(shù)
 - 根據(jù) 2,3 步驟,即可以獲得 1 中對應 socket 的相關進程信息
 
我們可以做個測試驗證整個流程。先使用 nc 命令監(jiān)聽 8090 端口:
- nc -l 8090
 
找到上面 nc 進程的 pid,查看該進程所有打開的文件描述符:
- vagrant@vagrant:/proc/25556/fd$ ls -alh
 - total 0
 - dr-x------ 2 vagrant vagrant 0 Nov 18 12:21 .
 - dr-xr-xr-x 9 vagrant vagrant 0 Nov 18 12:20 ..
 - lrwx------ 1 vagrant vagrant 64 Nov 18 12:21 0 -> /dev/pts/1
 - lrwx------ 1 vagrant vagrant 64 Nov 18 12:21 1 -> /dev/pts/1
 - lrwx------ 1 vagrant vagrant 64 Nov 18 12:21 2 -> /dev/pts/1
 - lrwx------ 1 vagrant vagrant 64 Nov 18 12:21 3 -> socket:[2226056]
 
上面列出的所有文件描述中,socket:[2226056]為 nc 命令監(jiān)聽 8090 端口所創(chuàng)建的 socket。其中2226056為該 socket 的 inode。
根據(jù)該 inode 號,我們查看/proc/net/tcp對應的記錄信息,其中1F9A為本地端口號,轉換成十進制恰好為 8090:
- vagrant@vagrant:/proc/25556/fd$ cat /proc/net/tcp | grep 2226056
 - 1: 00000000:1F9A 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 2226056 1 0000000000000000 100 0 0 10 0
 
根據(jù)進程 id,我們查看進程名稱和啟動參數(shù):
- vagrant@vagrant:/proc/25556/fd$ cat /proc/25556/cmdline
 - nc-l8090
 
下面我們看下/proc/net/tcp文件格式。
/proc/net/tcp 文件格式
/proc/net/tcp文件首先會列出所有監(jiān)聽狀態(tài)的 TCP 套接字,然后列出所有已建立的 TCP 套接字。我們通過head \-n 5 /proc/net/tcp命令查看該文件頭五行:
- sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
 - 0: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 22279 1 0000000000000000 100 0 0 10 0
 - 1: 00000000:1FBB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 21205 1 0000000000000000 100 0 0 10 0
 - 2: 00000000:26FB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 21203 1 0000000000000000 100 0 0 10 0
 - 3: 00000000:26FD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 21201 1 0000000000000000 100 0 0 10 0
 
每一行各個字段解釋說明如下,由于太長分為三部分說明:
第一部分:
- 46: 010310AC:9C4C 030310AC:1770 01
 - | | | | | |--> 連接狀態(tài),16 進制表示,具體值見下面說明
 - | | | | |------> 遠程 TCP 端口號,主機字節(jié)序,16 進制表示
 - | | | |-------------> 遠程 IPv4 地址,網(wǎng)絡字節(jié)序,16 進制表示
 - | | |--------------------> 本地 TCP 端口號,主機字節(jié)序,16 進制表示
 - | |---------------------------> 本地 IPv4 地址,網(wǎng)絡字節(jié)序,16 進制表示
 - |----------------------------------> 條目編號,從 0 開始
 
上面連接狀態(tài)所有值如下,具體參見 linux 源碼 tcp\_states.h[1]:
- enum {
 - TCP_ESTABLISHED = 1,
 - TCP_SYN_SENT,
 - TCP_SYN_RECV,
 - TCP_FIN_WAIT1,
 - TCP_FIN_WAIT2,
 - TCP_TIME_WAIT,
 - TCP_CLOSE,
 - TCP_CLOSE_WAIT,
 - TCP_LAST_ACK,
 - TCP_LISTEN,
 - TCP_CLOSING, /* Now a valid state */
 - TCP_NEW_SYN_RECV,
 - TCP_MAX_STATES /* Leave at the end! */
 - };
 
第二部分:
- 00000150:00000000 01:00000019 00000000
 - | | | | |--> number of unrecovered RTO timeouts
 - | | | |----------> number of jiffies until timer expires
 - | | |----------------> timer_active,具體值見下面說明
 - | |----------------------> receive-queue,當狀態(tài)是 ESTABLISHED,表示接收隊列中數(shù)據(jù)長度;狀態(tài)是 LISTEN,表示已經(jīng)完成連接隊列的長度
 - |-------------------------------> transmit-queue,發(fā)送隊列中數(shù)據(jù)長度
 
timer_active 所有值與說明如下:
- 0 no timer is pending
 - 1 retransmit-timer is pending
 - 2 another timer (e.g. delayed ack or keepalive) is pending
 - 3 this is a socket in TIME_WAIT state. Not all fields will contain data (or even exist)
 - 4 zero window probe timer is pending
 
第三部分:
- 1000 0 54165785 4 cd1e6040 25 4 27 3 -1
 - | | | | | | | | | |--> slow start size threshold,
 - | | | | | | | | | or -1 if the threshold
 - | | | | | | | | | is >= 0xFFFF
 - | | | | | | | | |----> sending congestion window
 - | | | | | | | |-------> (ack.quick<<1)|ack.pingpong
 - | | | | | | |---------> Predicted tick of soft clock
 - | | | | | | (delayed ACK control data)
 - | | | | | |------------> retransmit timeout
 - | | | | |------------------> location of socket in memory
 - | | | |-----------------------> socket reference count
 - | | |-----------------------------> socket 的 inode 號
 - | |----------------------------------> unanswered 0-window probes
 - |---------------------------------------------> socket 所屬用戶的 uid
 
Go 實現(xiàn)簡易版本 netstat 命令
netstat 工作原理和/proc/net/tcp文件結構,我們都已經(jīng)了解了,現(xiàn)在可以使用據(jù)此使用 Go 實現(xiàn)一個簡單版本的 netstat 命令。
核心代碼如下,完整代碼參加 go-netstat[2]:
- // 狀態(tài)碼值
 - const (
 - TCP_ESTABLISHED = iota + 1
 - TCP_SYN_SENT
 - TCP_SYN_RECV
 - TCP_FIN_WAIT1
 - TCP_FIN_WAIT2
 - TCP_TIME_WAIT
 - TCP_CLOSE
 - TCP_CLOSE_WAIT
 - TCP_LAST_ACK
 - TCP_LISTEN
 - TCP_CLOSING
 - //TCP_NEW_SYN_RECV
 - //TCP_MAX_STATES
 - )
 - // 狀態(tài)碼
 - var states = map[int]string{
 - TCP_ESTABLISHED: "ESTABLISHED",
 - TCP_SYN_SENT: "SYN_SENT",
 - TCP_SYN_RECV: "SYN_RECV",
 - TCP_FIN_WAIT1: "FIN_WAIT1",
 - TCP_FIN_WAIT2: "FIN_WAIT2",
 - TCP_TIME_WAIT: "TIME_WAIT",
 - TCP_CLOSE: "CLOSE",
 - TCP_CLOSE_WAIT: "CLOSE_WAIT",
 - TCP_LAST_ACK: "LAST_ACK",
 - TCP_LISTEN: "LISTEN",
 - TCP_CLOSING: "CLOSING",
 - //TCP_NEW_SYN_RECV: "NEW_SYN_RECV",
 - //TCP_MAX_STATES: "MAX_STATES",
 - }
 - // socketEntry 結構體,用來存儲/proc/net/tcp 每一行解析后數(shù)據(jù)信息
 - type socketEntry struct {
 - id int
 - srcIP net.IP
 - srcPort int
 - dstIP net.IP
 - dstPort int
 - state string
 - txQueue int
 - rxQueue int
 - timer int8
 - timerDuration time.Duration
 - rto time.Duration // retransmission timeout
 - uid int
 - uname string
 - timeout time.Duration
 - inode string
 - }
 - // 解析/proc/net/tcp 行記錄
 - func parseRawSocketEntry(entry string) (*socketEntry, error) {
 - se := &socketEntry{}
 - entrys := strings.Split(strings.TrimSpace(entry), " ")
 - entryItems := make([]string, 0, 17)
 - for _, ent := range entrys {
 - if ent == "" {
 - continue
 - }
 - entryItems = append(entryItems, ent)
 - }
 - id, err := strconv.Atoi(string(entryItems[0][:len(entryItems[0])-1]))
 - if err != nil {
 - return nil, err
 - }
 - se.id = id // sockect entry id
 - localAddr := strings.Split(entryItems[1], ":") // 本地 ip
 - se.srcIP = parseHexBigEndianIPStr(localAddr[0])
 - port, err := strconv.ParseInt(localAddr[1], 16, 32) // 本地 port
 - if err != nil {
 - return nil, err
 - }
 - se.srcPort = int(port)
 - remoteAddr := strings.Split(entryItems[2], ":") // 遠程 ip
 - se.dstIP = parseHexBigEndianIPStr(remoteAddr[0])
 - port, err = strconv.ParseInt(remoteAddr[1], 16, 32) // 遠程 port
 - if err != nil {
 - return nil, err
 - }
 - se.dstPort = int(port)
 - state, _ := strconv.ParseInt(entryItems[3], 16, 32) // socket 狀態(tài)
 - se.state = states[int(state)]
 - tcpQueue := strings.Split(entryItems[4], ":")
 - tQueue, err := strconv.ParseInt(tcpQueue[0], 16, 32) // 發(fā)送隊列數(shù)據(jù)長度
 - if err != nil {
 - return nil, err
 - }
 - se.txQueue = int(tQueue)
 - sQueue, err := strconv.ParseInt(tcpQueue[1], 16, 32) // 接收隊列數(shù)據(jù)長度
 - if err != nil {
 - return nil, err
 - }
 - se.rxQueue = int(sQueue)
 - se.uid, err = strconv.Atoi(entryItems[7]) // socket uid
 - if err != nil {
 - return nil, err
 - }
 - se.uname = systemUsers[entryItems[7]] // socket user name
 - se.inode = entryItems[9] // socket inode
 - return se, nil
 - }
 - // hexIP 是網(wǎng)絡字節(jié)序/大端法轉換成的 16 進制的字符串
 - func parseHexBigEndianIPStr(hexIP string) net.IP {
 - b := []byte(hexIP)
 - for i, j := 1, len(b)-2; i < j; i, j = i+2, j-2 { // 反轉字節(jié),轉換成小端法
 - b[i], b[i-1], b[j], b[j+1] = b[j+1], b[j], b[i-1], b[i]
 - }
 - l, _ := strconv.ParseInt(string(b), 16, 64)
 - return net.IPv4(byte(l>>24), byte(l>>16), byte(l>>8), byte(l))
 - }
 


















 
 
 











 
 
 
 