Go 语言实现简易版 netstat 命令

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

  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 端口: 

  1. nc -l 8090 

找到上面 nc 进程的 pid,查看该进程所有打开的文件描述符: 

  1. vagrant@vagrant:/proc/25556/fd$ ls -alh  
  2. total 0  
  3. dr-x------ 2 vagrant vagrant  0 Nov 18 12:21 .  
  4. dr-xr-xr-x 9 vagrant vagrant  0 Nov 18 12:20 ..  
  5. lrwx------ 1 vagrant vagrant 64 Nov 18 12:21 0 -> /dev/pts/1  
  6. lrwx------ 1 vagrant vagrant 64 Nov 18 12:21 1 -> /dev/pts/1  
  7. lrwx------ 1 vagrant vagrant 64 Nov 18 12:21 2 -> /dev/pts/1  
  8. 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: 

  1. vagrant@vagrant:/proc/25556/fd$ cat /proc/net/tcp | grep 2226056  
  2.    1: 00000000:1F9A 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 2226056 1 0000000000000000 100 0 0 10 0 

根据进程 id,我们查看进程名称和启动参数: 

  1. vagrant@vagrant:/proc/25556/fd$ cat /proc/25556/cmdline  
  2. nc-l8090 

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

/proc/net/tcp 文件格式

/proc/net/tcp文件首先会列出所有监听状态的 TCP 套接字,然后列出所有已建立的 TCP 套接字。我们通过head \-n 5 /proc/net/tcp命令查看该文件头五行: 

  1. sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode  
  2.    0: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 22279 1 0000000000000000 100 0 0 10 0  
  3.    1: 00000000:1FBB 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 21205 1 0000000000000000 100 0 0 10 0  
  4.    2: 00000000:26FB 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 21203 1 0000000000000000 100 0 0 10 0  
  5.    3: 00000000:26FD 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 21201 1 0000000000000000 100 0 0 10 0 

每一行各个字段解释说明如下,由于太长分为三部分说明:

第一部分: 

  1. 46: 010310AC:9C4C 030310AC:1770 01   
  2. |      |      |      |      |   |--> 连接状态,16 进制表示,具体值见下面说明  
  3. |      |      |      |      |------> 远程 TCP 端口号,主机字节序,16 进制表示  
  4. |      |      |      |-------------> 远程 IPv4 地址,网络字节序,16 进制表示  
  5. |      |      |--------------------> 本地 TCP 端口号,主机字节序,16 进制表示  
  6. |      |---------------------------> 本地 IPv4 地址,网络字节序,16 进制表示  
  7. |----------------------------------> 条目编号,从 0 开始 

上面连接状态所有值如下,具体参见 linux 源码 tcp\_states.h[1]: 

  1. enum {  
  2.  TCP_ESTABLISHED = 1 
  3.  TCP_SYN_SENT,  
  4.  TCP_SYN_RECV,  
  5.  TCP_FIN_WAIT1,  
  6.  TCP_FIN_WAIT2,  
  7.  TCP_TIME_WAIT,  
  8.  TCP_CLOSE,  
  9.  TCP_CLOSE_WAIT,  
  10.  TCP_LAST_ACK,  
  11.  TCP_LISTEN,  
  12.  TCP_CLOSING, /* Now a valid state */  
  13.  TCP_NEW_SYN_RECV,  
  14.  TCP_MAX_STATES /* Leave at the end! */  
  15. }; 

第二部分: 

  1. 00000150:00000000 01:00000019 00000000    
  2.       |        |     |     |       |--> number of unrecovered RTO timeouts  
  3.       |        |     |     |----------> number of jiffies until timer expires  
  4.       |        |     |----------------> timer_active,具体值见下面说明  
  5.       |        |----------------------> receive-queue,当状态是 ESTABLISHED,表示接收队列中数据长度;状态是 LISTEN,表示已经完成连接队列的长度  
  6.       |-------------------------------> 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

第三部分: 

  1. 1000        0 54165785 4 cd1e6040 25 4 27 3 -1  
  2.    |          |    |     |    |     |  | |  | |--> slow start size threshold,   
  3.    |          |    |     |    |     |  | |  |      or -1 if the threshold  
  4.    |          |    |     |    |     |  | |  |      is >= 0xFFFF  
  5.    |          |    |     |    |     |  | |  |----> sending congestion window  
  6.    |          |    |     |    |     |  | |-------> (ack.quick<<1)|ack.pingpong  
  7.    |          |    |     |    |     |  |---------> Predicted tick of soft clock  
  8.    |          |    |     |    |     |              (delayed ACK control data)  
  9.    |          |    |     |    |     |------------> retransmit timeout  
  10.    |          |    |     |    |------------------> location of socket in memory  
  11.    |          |    |     |-----------------------> socket reference count  
  12.    |          |    |-----------------------------> socket 的 inode 号  
  13.    |          |----------------------------------> unanswered 0-window probes 
  14.    |---------------------------------------------> socket 所属用户的 uid 

Go 实现简易版本 netstat 命令

netstat 工作原理和/proc/net/tcp文件结构,我们都已经了解了,现在可以使用据此使用 Go 实现一个简单版本的 netstat 命令。

核心代码如下,完整代码参加 go-netstat[2]: 

  1. // 状态码值  
  2. const (  
  3.  TCP_ESTABLISHED = iota + 1  
  4.  TCP_SYN_SENT  
  5.  TCP_SYN_RECV  
  6.  TCP_FIN_WAIT1  
  7.  TCP_FIN_WAIT2 
  8.  TCP_TIME_WAIT  
  9.  TCP_CLOSE  
  10.  TCP_CLOSE_WAIT  
  11.  TCP_LAST_ACK  
  12.  TCP_LISTEN  
  13.  TCP_CLOSING  
  14.  //TCP_NEW_SYN_RECV  
  15.  //TCP_MAX_STATES  
  16.  
  17. // 状态码  
  18. var states = map[int]string{  
  19.  TCP_ESTABLISHED: "ESTABLISHED",  
  20.  TCP_SYN_SENT:    "SYN_SENT",  
  21.  TCP_SYN_RECV:    "SYN_RECV",  
  22.  TCP_FIN_WAIT1:   "FIN_WAIT1",  
  23.  TCP_FIN_WAIT2:   "FIN_WAIT2",  
  24.  TCP_TIME_WAIT:   "TIME_WAIT",  
  25.  TCP_CLOSE:       "CLOSE",  
  26.  TCP_CLOSE_WAIT:  "CLOSE_WAIT",  
  27.  TCP_LAST_ACK:    "LAST_ACK",  
  28.  TCP_LISTEN:      "LISTEN",  
  29.  TCP_CLOSING:     "CLOSING",  
  30.  //TCP_NEW_SYN_RECV: "NEW_SYN_RECV",  
  31.  //TCP_MAX_STATES:   "MAX_STATES",  
  32.  
  33. // socketEntry 结构体,用来存储/proc/net/tcp 每一行解析后数据信息  
  34. type socketEntry struct {  
  35.  id      int  
  36.  srcIP   net.IP  
  37.  srcPort int  
  38.  dstIP   net.IP  
  39.  dstPort int  
  40.  state   string  
  41.  txQueue       int  
  42.  rxQueue       int  
  43.  timer         int8  
  44.  timerDuration time.Duration  
  45.  rto           time.Duration // retransmission timeout  
  46.  uid           int  
  47.  uname         string  
  48.  timeout       time.Duration  
  49.  inode         string  
  50.  
  51. // 解析/proc/net/tcp 行记录  
  52. func parseRawSocketEntry(entry string) (*socketEntry, error) {  
  53.  se := &socketEntry{} 
  54.  entrys :strings.Split(strings.TrimSpace(entry), " ")  
  55.  entryItems :make([]string, 0, 17)  
  56.  for _, ent :range entrys {  
  57.   if ent == "" {  
  58.    continue  
  59.   }  
  60.   entryItems = append(entryItems, ent)  
  61.  }  
  62.  id, err :strconv.Atoi(string(entryItems[0][:len(entryItems[0])-1]))  
  63.  if err != nil {  
  64.   return nil, err  
  65.  }  
  66.  se.id = id                                     // sockect entry id  
  67.  localAddr :strings.Split(entryItems[1], ":") // 本地 ip  
  68.  se.srcIP = parseHexBigEndianIPStr(localAddr[0])  
  69.  port, err :strconv.ParseInt(localAddr[1], 16, 32) // 本地 port  
  70.  if err != nil {  
  71.   return nil, err  
  72.  }  
  73.  se.srcPort = int(port)  
  74.  remoteAddr :strings.Split(entryItems[2], ":") // 远程 ip  
  75.  se.dstIP = parseHexBigEndianIPStr(remoteAddr[0])  
  76.  port, err = strconv.ParseInt(remoteAddr[1], 16, 32) // 远程 port  
  77.  if err != nil { 
  78.   return nil, err  
  79.  }  
  80.  se.dstPort = int(port)  
  81.  state, _ :strconv.ParseInt(entryItems[3], 16, 32) // socket 状态  
  82.  se.state = states[int(state)] 
  83.  tcpQueue :strings.Split(entryItems[4], ":")  
  84.  tQueue, err :strconv.ParseInt(tcpQueue[0], 16, 32) // 发送队列数据长度  
  85.  if err != nil {  
  86.   return nil, err  
  87.  }  
  88.  se.txQueue = int(tQueue)  
  89.  sQueue, err :strconv.ParseInt(tcpQueue[1], 16, 32) // 接收队列数据长度  
  90.  if err != nil {  
  91.   return nil, err  
  92.  }  
  93.  se.rxQueue = int(sQueue)   
  94.  se.uid, err = strconv.Atoi(entryItems[7]) // socket uid  
  95.  if err != nil {  
  96.   return nil, err  
  97.  }  
  98.  se.uname = systemUsers[entryItems[7]] // socket user name 
  99.  se.inode = entryItems[9]              // socket inode  
  100.  return se, nil  
  101. // hexIP 是网络字节序/大端法转换成的 16 进制的字符串  
  102. func parseHexBigEndianIPStr(hexIP string) net.IP {  
  103.  b := []byte(hexIP)  
  104.  for i, j :1, len(b)-2; i < j; i, j = i+2, j-2 { // 反转字节,转换成小端法  
  105.   b[i], b[i-1], b[j], b[j+1] = b[j+1], b[j], b[i-1], b[i]  
  106.  }  
  107.  l, _ :strconv.ParseInt(string(b), 16, 64)  
  108.  return net.IPv4(byte(l>>24), byte(l>>16), byte(l>>8), byte(l))  
  109.  

 

责任编辑:庞桂玉 来源: 马哥Linux运维
相关推荐

2023-12-29 08:31:49

Spring框架模块

2020-08-12 08:56:30

代码凯撒密码函数

2022-11-01 18:29:25

Go语言排序算法

2023-05-08 07:55:05

快速排序Go 语言

2012-03-13 10:40:58

Google Go

2022-05-19 14:14:26

go语言限流算法

2022-02-11 13:44:56

fiber架构React

2012-08-06 08:50:05

Go语言

2023-03-27 00:20:48

2022-04-18 10:01:07

Go 语言汉诺塔游戏

2017-01-13 08:37:57

PythonAlphaGoMuGo

2023-07-31 08:01:13

二叉搜索测试

2021-07-26 09:47:38

Go语言C++

2021-03-01 21:59:25

编程语言GoCX

2021-03-01 18:35:18

Go语言虚拟机

2011-12-05 10:37:53

Linux服务器Shell脚本

2022-10-20 11:49:49

JS动画帧,CSS

2022-07-20 09:52:44

Go语言短信验证码

2014-12-26 09:52:08

Go

2024-02-06 10:04:49

Express框架repo
点赞
收藏

51CTO技术栈公众号