Go 语言实现简易版 netstat 命令

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

netstat 工作原理

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

netstat 工作原理如下:

 通过读取/proc/net/tcp 、实现/proc/net/tcp6 文件,简易获取 socket 本地地址,命令本地端口,实现远程地址,简易远程端口,命令状态,实现inode 等信息  接着扫描所有/proc/[pid]/fd 目录下的简易的 socket 文件描述符,建立 inode 到进程 pid 映射  根据 pid 读取/proc/[pid]/cmdline 文件,命令获取进程命令和启动参数  根据 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] 

上面列出的云南idc服务商所有文件描述中,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 行记录  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 状态   se.state = states[int(state)]  tcpQueue := strings.Split(entryItems[4], ":")   tQueue, err := strconv.ParseInt(tcpQueue[0], 16, 32) // 发送队列数据长度   if err != nil {     return nil, err   }   se.txQueue = int(tQueue)   sQueue, err := strconv.ParseInt(tcpQueue[1], 16, 32) // 接收队列数据长度   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 是网络字节序/大端法转换成的 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 {  // 反转字节,源码库转换成小端法    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科技类资讯
上一篇:英特尔推出12量子比特QPU进入量子计算领域,并将斥资46亿美元在波兰建厂
下一篇:2023年,数据中心需要“分布式”