netstat 使用 go 语言实现是什么操作?本文从 netstat 原理出发详细解读了这一实践。

netstat 工作原理

netstat 命令是 linux 系统中查看网络情况的一个命令。比如我们可以通过netstat \-ntlp | grep 8080查看监听 8080 端口的进程。
netstat 工作原理如下:

  1. 通过读取/proc/net/tcp 、/proc/net/tcp6 文件,获取 socket 本地地址,本地端口,远程地址,远程端口,状态,inode 等信息
  2. 接着扫描所有/proc/[pid]/fd 目录下的的 socket 文件描述符,建立 inode 到进程 pid 映射
  3. 根据 pid 读取/proc/[pid]/cmdline 文件,获取进程命令和启动参数
  4. 根据 2,3 步骤,即可以获得 1 中对应 socket 的相关进程信息
我们可以做个测试验证整个流程。先使用 nc 命令监听 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 命令监听 8090 端口所创建的 socket。其中2226056为该 socket 的 inode。
根据该 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

根据进程 id,我们查看进程名称和启动参数:
vagrant@vagrant:/proc/25556/fd$ cat /proc/25556/cmdline

nc-l8090

下面我们看下/proc/net/tcp文件格式。

/proc/net/tcp 文件格式

/proc/net/tcp文件首先会列出所有监听状态的 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 

   |      |      |      |      |   |--> 连接状态,16 进制表示,具体值见下面说明

   |      |      |      |      |------> 远程 TCP 端口号,主机字节序,16 进制表示

   |      |      |      |-------------> 远程 IPv4 地址,网络字节序,16 进制表示

   |      |      |--------------------> 本地 TCP 端口号,主机字节序,16 进制表示

   |      |---------------------------> 本地 IPv4 地址,网络字节序,16 进制表示

   |----------------------------------> 条目编号,从 0 开始

上面连接状态所有值如下,具体参见 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,当状态是 ESTABLISHED,表示接收队列中数据长度;状态是 LISTEN,表示已经完成连接队列的长度

      |-------------------------------> transmit-queue,发送队列中数据长度

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 实现简易版本 netstat 命令

netstat 工作原理和/proc/net/tcp文件结构,我们都已经了解了,现在可以使用据此使用 Go 实现一个简单版本的 netstat 命令。
核心代码如下,完整代码参加 
go-netstat[2]

// 状态码值
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
)


// 状态码
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 每一行解析后数据信息
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 行记录
funcparseRawSocketEntry(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
 {

returnnil
, 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
 {

returnnil
, 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
 {

returnnil
, err

 }

 se.dstPort = 
int
(port)


 state, _ := strconv.ParseInt(entryItems[
3
], 
16
32
// socket 状态
 se.state = states[
int
(state)]


 tcpQueue := strings.Split(entryItems[
4
], 
":"
)

 tQueue, err := strconv.ParseInt(tcpQueue[
0
], 
16
32
// 发送队列数据长度
if
 err != 
nil
 {

returnnil
, err

 }

 se.txQueue = 
int
(tQueue)

 sQueue, err := strconv.ParseInt(tcpQueue[
1
], 
16
32
// 接收队列数据长度
if
 err != 
nil
 {

returnnil
, err

 }

 se.rxQueue = 
int
(sQueue)


 se.uid, err = strconv.Atoi(entryItems[
7
]) 
// socket uid
if
 err != 
nil
 {

returnnil
, err

 }

 se.uname = systemUsers[entryItems[
7
]] 
// socket user name
 se.inode = entryItems[
9
]              
// socket inode
return
 se, 
nil
}


// hexIP 是网络字节序/大端法转换成的 16 进制的字符串
funcparseHexBigEndianIPStr(hexIP string)net.IP
 {

 b := []
byte
(hexIP)

for
 i, j := 
1
len
(b)
-2
; i < j; i, j = i+
2
, j
-2
 { 
// 反转字节,转换成小端法
  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))

}
文章转载:IT大咖说 

(版权归原作者所有,侵删)
点击下方“阅读原文”查看更多
继续阅读
阅读原文