骏马金龙

网名骏马金龙,钟情于IT世界里的各种原理和实现机制,强迫症重症患者。爱研究、爱翻译、爱分享。特借此一亩三分田记录自己成长点滴!!!
我本问道人,道心不坚,必将与道无缘!

Perl获取主机名、用户、组、网络信息

获取主机名、用户、组、网络信息相关函数

首先是获取主机名的方式,Perl提供了Sys::Hostname模块,可以查询当前的主机名:

use Sys::Hostname;
print hostname, "\n";

Perl中提供了下面一大堆的内置函数用来获取用户、组、网络相关的信息。这些perl函数在C中也都有对应的函数。

# 获取和设置用户和组
endgrent - be done using group file
endpwent - be done using passwd file
getgrent - get next group record
getgrgid - get group record given group user ID
getgrnam - get group record given group name
getlogin - return who logged in at this tty
getpwent - get next passwd record
getpwnam - get passwd record given user login name
getpwuid - get passwd record given user ID
setgrent - prepare group file for use
setpwent - prepare passwd file for use

# 获取和设置网络信息
endhostent - be done using hosts file
endnetent - be done using networks file
endprotoent - be done using protocols file
endservent - be done using services file
gethostbyaddr - get host record given its address
gethostbyname - get host record given name
gethostent - get next hosts record
getnetbyaddr - get network record given its address
getnetbyname - get networks record given name
getnetent - get next networks record
getprotobyname - get protocol record given name
getprotobynumber - get protocol record numeric protocol
getprotoent - get next protocols record
getservbyname - get services record given its name
getservbyport - get services record given numeric port
getservent - get next services record
sethostent - prepare hosts file for use
setnetent - prepare networks file for use
setprotoent - prepare protocols file for use
setservent - prepare services file for use

从动作上分为3类:

  • getXXX:获取信息操作
  • setXXX:设置操作
  • endXXX:关闭操作

getXXXXent表示从对应文件中读取每一行并迭代,这个过程中间会隐式打开对应文件(所以隐式地维护了一个文件句柄)并使用指针指向每次读取完的位置,可以使用setXXXent来重置指针使其指向文件开头(也就是说从头开始迭代),使用endXXXent来关闭隐式打开的文件句柄(有些函数会通过网络解析得到结果,如DNS解析,endXXXent也会关闭这个网络连接从而终止解析过程)。

这里共有18个函数。包括以下几个文件(以Linux操作系统为例):

  • /etc/group:{end | set | get}grent
  • /etc/passwd、/etc/shadow:{end | set | get}pwent
  • /etc/hosts:{end | set | get}hostent
  • /etc/networks:{end | set | get}netent
  • /etc/services:{end | set | get}servent
  • /etc/protocols:{end | set | get}protoent

通过getXXXent迭代这些文件里的每一行,都得到一个XXXent结构体对象。例如/etc/group中的每一行对应一个grent对象,这一行中的各个字段是一个grent结构体的各个字段。

这里6种结构体对象包含的字段如下:

# 0        1          2           3         4
( $name,   $passwd,   $gid,       $members  ) = getgr*
( $name,   $aliases,  $addrtype,  $net      ) = getnet*
( $name,   $aliases,  $port,      $proto    ) = getserv*
( $name,   $aliases,  $proto                ) = getproto*
( $name,   $aliases,  $addrtype,  $length,  @addrs ) = gethost*
( $name,   $passwd,   $uid,       $gid,     $quota,
$comment,  $gcos,     $dir,       $shell,   $expire ) = getpw*
# 5        6          7           8         9

这种结构体对象并非只能从这些文件中获取,自己也可以构建或通过其它函数返回,相关内容在后面介绍中会提到。

除了上面的分类,用户、组还能继续分为:

  • getgrgid:根据给定的gid,搜索/etc/group中对应的组,并返回该组信息
  • getgrnam:根据给定的组name,搜索/etc/group中对应的组,并返回该组信息
  • getpwnam:根据给定的uid,搜索/etc/passwd、/etc/shadow中对应的用户,并返回该用户信息
  • getpwuid:根据给定的用户name,搜索/etc/passwd、/etc/shadow中对应的用户,并返回该用户信息
  • getlogin:返回当前登录的用户名,返回的依据是从/run/utmp或/etc/utmp搜索

获取网络信息的操作还有:

  • gethostbyaddr:通过ipv4地址获取host信息(这里的host和dns解析有关,即对应于/etc/hosts)
  • gethostbyname:通过主机名获取host信息(这里的host和dns解析有关,即对应于/etc/hosts)
  • getnetbyaddr:获取网段信息(对应于/etc/networks文件)
  • getnetbyname:获取网段信息(对应于/etc/networks文件)
  • getservbyname:获取服务信息(对应于/etc/services文件)
  • getservbyport:获取服务信息(对应于/etc/services文件)
  • getprotobyname:获取协议信息(对应于/etc/protocols文件)
  • getprotobynumber:获取协议信息(对应于/etc/protocols文件)

这些函数在标量上下文中返回单个name或id类数据,在列表上下文中返回各自的ent结构体对象。

虽然看着一大堆,分类后其实很容易记忆。但是,这些函数都不是很方便,因为有不少函数里的参数或返回值是需要或经过了二进制打包的。比如gethostbyaddr函数的参数是一个ip地址,但不能直接传递"192.168.100.12"这样的地址,而是先使用pack()或使用Socket模块提供的inet_aton()将其转换成二进制格式,再传递给gethostbyaddr

正是因为这些函数使用起来并不方便,所以每种类型(passwd、group、host、network、service、protocol)都有对应的面向对象的模块,使用这些模块中的方法,可以免去转换的过程。它们对应的模块为:

  • User::pwent
  • User::grent
  • Net::hostent
  • Net::netent
  • Net::servent
  • Net::protoent

由于很多函数在用法上是非常类似的,所以在后文介绍重复内容时仅将简单说明重复函数的用法。

获取用户和组信息

相关的函数和模块有:

  • {get | set | end}pwent
  • {get | set | end}grent
  • getgrgid
  • getgrname
  • getpwuid
  • getpwnam
  • getlogin
  • User::pwent
  • User::grent

group信息

首先解释getgrent以及grent结构对象,因为grent结构将group相关的东西全都串起来了。在后文也都按照这种方式介绍每一种分类。

getgrent函数会遍历/etc/group文件,并从中读取每一行,每次遍历到一行都放进一个称为grent的结构对象中。前文列出过各种结构对象包含的字段,其中grent结构包括如下字段:

# 0       1         2     3  
( $name,  $passwd,  $gid, $members ) = getgr*

例如,/etc/group中的前三行为:

$ head -n 3 /etc/group
root:x:0:test1,test2,test3
daemon:x:1:
bin:x:2:

使用getgrent()取前3行:

#!/usr/bin/perl

use strict;
use warnings;
use 5.010;

my $i;
while(my ($name, $passwd, $gid, $members) = getgrent) {
        $i++;
        last if $i == 4;
        say "group name: $name";
        say "group passwd: $passwd";
        say "group id: $gid";
        say "group member: $members";
        say "-" x 10;
}

执行结果:

group name: root
group passwd: x
group id: 0
group member: test1 test2 test3
----------
group name: daemon
group passwd: x
group id: 1
group member:
----------
group name: bin
group passwd: x
group id: 2
group member:
----------

前面说过,getXXXent()会隐式地打开对应的文件并维护一个对应的文件句柄,同时会使用指针标记好已读取到哪一行。可以使用setgrent()重置指针使其回到文件头部(也就是从头开始迭代),使用endgrent()关闭隐式维护的文件句柄,当然,再次调用getXXXent()会再次打开文件句柄。

例如,读取两个组后,绕回到开头再读两个组,在这过程中打开的文件并没有关闭。然后使用endgrent关闭已打开的文件句柄。最后再次读取两个组,这会重新打开文件,直到程序退出后文件被关闭。

#!/usr/bin/perl
#
use strict;
use warnings;
use 5.010;

say join ', ', getgrent;
say join ', ', getgrent;
setgrent;
say join ', ', getgrent;
say join ', ', getgrent;

say '-' x 30;
system 'lsof -n | grep "group"';
endgrent;
say '-' x 30;
system 'lsof -n | grep "group"';
say '-' x 30;

say join ', ', getgrent;
say join ', ', getgrent;

执行结果:

root, x, 0, test1 test2 test3
bin, x, 1, 
root, x, 0, test1 test2 test3
bin, x, 1, 
------------------------------
systemd   1      root    6r   DIR  0,21  0     1148 /sys/fs/cgroup/systemd
perl      70014  root    3r   REG  8,2   721   35750228 /etc/group
------------------------------
systemd   1      root    6r   DIR  0,21  0     1148 /sys/fs/cgroup/systemd
------------------------------
root, x, 0, test1 test2 test3
bin, x, 1,

使用getgrnam()、getgrgid()也可以获取相关组信息。在标量上下文下,前者返回gid,后者返回group_name。在列表上下文,两者都返回grent结构:

#!/usr/bin/perl
use strict;
use warnings;
use 5.010;

my $gr_gid = getgrnam 'root';
say "gr_gid: $gr_gid";

say '=' x 10, ' scalar context ', '=' x 10;
my $gr_name = getgrgid 0;
say "gr_name: $gr_name";

say '=' x 10, ' list context ', '=' x 10;
my ($name, $passwd, $gid, $members) = getgrnam 'root';

say "name: $name";
say "passwd: $passwd";
say "gid: $gid";
say "members: $members";

执行结果:

gr_gid: 0
========== scalar context ==========
gr_name: root
========== list context ==========
name: root
passwd: x
gid: 0
members: test1 test2 test3

user信息

获取用户信息和组信息的方式是一样的,相关函数为getpwentsetpwentendpwentgetpwnamgetpwuid,唯一不一样的是pwent结构对象的字段和grent结构对象的字段不一样。

# 0        1          2           3         4
$name,     $passwd,   $uid,       $gid,     $quota,
$comment,  $gcos,     $dir,       $shell,   $expire
# 5        6          7           8         9

需要注意的是,不同操作系统中支持的字段不一样。

#!/usr/bin/perl
use strict;
use warnings;
use 5.010;

my $user_uid = getpwnam 'root';
say "user_uid: $user_uid";

say '=' x 10, ' scalar context ', '=' x 10;
my $user_name = getpwuid 0;
say "user_name: $user_name";

say '=' x 10, ' list context ', '=' x 10;
say join ', ', getpwnam 'root';

结果:

user_uid: 0
========== scalar context ==========
user_name: root
========== list context ==========
root, 这里是root的密码, 0, 0, , , root, /root, /bin/bash

当前登录用户

getlogin()函数可以获取当前用户名。

#!/usr/bin/perl
use strict;
use warnings;
use 5.010;

say getlogin;

root用户执行结果:

$ perl login.pl
root

User::grent和User::pwent模块

这两个模块提供了面向对象方式的操作。导入这两个模块,里面的函数默认会覆盖同名内置函数get{pw | gr}{ent | nam | gid},即Core模块中的这些函数,并且模块中的这些函数会返回对应的对象。

# grent OO
my $gr = getgrnam 'root' or die "no root group";

# pwent OO
my $pw = getpwuid 0 or die "no uid=0 user";

通过这些对象,可以直接获取到给定字段的值,且在导入了:FIELDS标签后,可以直接使用变量来访问对应字段。例如:

# grent OO
方法             等价变量
------------------------
$gr->name       $gr_name
$gr->gid        $gr_gid
$gr->passwd     $gr_passwd
$gr->members    $gr_members


# pwent OO
方法             等价变量
------------------------
$pw->name        $pw_name
$pw->passwd      $pw_passwd
$pw->uid         $pw_uid
$pw->gid         $pw_gid
$pw->quota       $pw_quota
$pw->comment     $pw_comment
$pw->gecos       $pw_gecos
$pw->dir         $pw_dir
$pw->shell       $pw_shell

同时,还各自提供了getpw()和getgr()方法,这两个方法是getpwuid()、getpwnam()、getgrgid()、getgrnam()的多态方法,它根据参数类型判断调用哪个函数。例如给getpw()传递数值时,表示调用getpwuid(),传递字符串时表示调用getpwnam()。

例如:

#!/usr/bin/perl
use strict;
use warnings;
use 5.010;

use User::pwent qw(:FIELDS);
use User::grent qw(:FIELDS);

my $gr = getgrnam 'root' or die 'no root group';
my $pw = getpwuid 0 or die 'no uid=0 user';

# grent OO
say '=' x 10 , ' grent OO ', '=' x 10;
say $gr->name;
say $gr_name;
say $gr->gid;
say $gr_gid;

say '=' x 10 , ' pwent OO ', '=' x 10;
# pwent OO
say $pw->name;
say $pw->shell;
say $pw_dir;

执行结果:

========== grent OO ==========
root
root
0
0
========== pwent OO ==========
root
/bin/bash
/root

地址解析:hosts信息

经过前面grent和pwent的解释之后,再理解hosts、net、serv、proto就简单多了。

hosts相关函数和模块:

  • gethostent
  • sethostent
  • endhostent
  • gethostbyaddr
  • gethostbyname
  • Net::hostent

gethostentgethostbyaddrgethostbyname用来给给定的主机名或地址进行DNS解析。它会读取/etc/hosts文件,也会从网络上做DNS解析。

必要的基础知识

在开始解释这些函数之前,先了解些必要基础知识。

在/etc/hosts文件中包含了本地的DNS解析记录,例如:

# IP_address canonical_hostname [aliases...]
127.0.0.1   localhost localhost.localdomain
::1         localhost localhost.localdomain
192.168.100.12 www.longshuai.com www1.longshuai.com

其中第一列是IP地址,第二列是该IP地址对应的规范主机名(canonical_hostname),从第三列开始全都是主机别名(aliases),即DNS里的CNAME记录。

如果将上面的www.longshuai.com换成DNS资源记录,区域数据文件中对应的配置如下:

www.longshuai.com.    IN  A   192.168.100.12
www1.longshuai.com.   IN  CNAME  www.longshuai.com.

也就是说,当查询www.longshuai.com时,会得到其对应的IP地址192.168.100.12,当查询别名www1.longshuai.com时,先解析回规范主机名www.longshuai.com,再得到其对应的A记录,即192.168.100.12

例如,使用host命令(或nslookup、dig命令)解析一下www.baidu.com相关的记录:

$ host www.baidu.com
www.baidu.com is an alias for www.a.shifen.com.
www.a.shifen.com has address 183.232.231.172
www.a.shifen.com has address 183.232.231.174

结果说明了,www.baidu.com只是www.a.shifen.com的一个别名,这个主机名有两个A记录,IP地址分别是183.232.231.172183.232.231.174

hostent结构

和前面介绍的grent、pwent一样,hostent也是一个结构体对象,该结构体对象包含如下字段:

# 0       1          2           3         4
( $name,  $aliases,  $addrtype,  $length,  @addrs )

其中:

  • $aliases中可能会包含多个别名,它是一个数组,但必须使用$aliases而非@aliases,否则后面几个返回值都被收集到@aliases中
  • $addrtype是地址类型,现在支持AF_INET和AF_INET6两种类型
  • $length是长度,ipv4地址长度都是4字节,所以值为4,ipv6是16字节
  • @addrs是以二进制方式打包的地址列表,无法直接输出,需要将每个地址元素都解包。解包的方式有两种:
    • 使用unpack():for (@addrs) { my ($a, $b, $c, $d) = unpack 'C4', $_ }
    • 使用Socket模块的inet_ntoa():for (@addrs){ my $addr = inet_ntoa($_) }

看下面的例子就知道了。

例如,下面使用gethostent()获取/etc/hosts中的每一个结构对象:

#!/usr/bin/perl
#
use strict;
use warnings;
use 5.010;

while(my ($name, $aliases, $addrtype, $length, @addrs) = gethostent){
    say "name: $name";
    say "aliases: $aliases";
    say "addrtype: $addrtype";
    say "length: $length";
    say "addrs: @addrs";
    say '-' x  10;
}

结果:

name: localhost
aliases: localhost.localdomain localhost4 localhost4.localdomain4
addrtype: 2
length: 4
addrs: 
----------
name: localhost
aliases: localhost.localdomain localhost6 localhost6.localdomain6
addrtype: 2
length: 4
addrs: 
----------
name: www.longshuai.com
aliases: www1.longshuai.com
addrtype: 2
length: 4
addrs: 

        ----------

可见,第四个字段是无法直接输出的,需要将其解包。可以使用unpack()或Socket模块中的inet_ntoa():

# 使用inet_ntoa()将二进制打包的地址转换成点分十进制的IP
use Socket qw(/inet/);
for (@addrs){
    say "addrs: ", inet_ntoa $_;
}

# 使用unpack()解包
for (@addrs){
    say "addrs: ", join '.', unpack 'C4', $_;
}

再看看gethostbyname(),只要给一个主机名,就可以解析出别名以及相关A记录。在列表上下文它返回hostent结构对象,在标量上下文,它返回打包好的IP地址。

#!/usr/bin/perl
use strict;
use warnings;

use Socket qw(/inet/);
use 5.010;

# scalar context
my $addr = gethostbyname $ARGV[0];
say "addr: ", inet_ntoa $addr;

# list context
my ($name, $aliases, $addrtype, $length, @addrs) = gethostbyname $ARGV[0];

say "name: $name";
say "aliases: $aliases";
say "addrtype: $addrtype";
say "length: $length";
for (@addrs){
    say "addrs: ", inet_ntoa $_;
}

执行:

$ perl host.pl www.baidu.com
addr: 183.232.231.174
name: www.baidu.com
aliases: 
addrtype: 2
length: 4
addrs: 183.232.231.174
addrs: 183.232.231.172

$ perl host.pl www.perl.org
addr: 151.101.42.217
name: dualstack.osff.map.fastly.net
aliases: www.perl.org cdn-fastly.perl.org
addrtype: 2
length: 4
addrs: 151.101.42.217

如果使用gethostbyaddr(),那么其IP地址参数需要使用inet_aton()或unpack()进行打包转换。但注意,一般gethostbyaddr()只用于解析/etc/hosts文件中有的地址。

#!/usr/bin/perl
use strict;
use warnings;

use Socket qw(/inet/ AF_INET);
use 5.010;

# scalar context
my $host = gethostbyaddr(inet_aton('192.168.100.21'), AF_INET);
say "host: ", $host;

# list context
my ($name, $aliases, $addrtype, $length, @addrs) = gethostbyaddr(inet_aton('192.168.100.21'), AF_INET);
say "name: $name";
say "aliases: $aliases";
say "addrtype: $addrtype";
say "length: $length";
for (@addrs){
    say "addrs: ", inet_ntoa $_;
}

一个简单的DNS A记录解析程序

在使用gethostbyname和gethostbyaddr的时候,解析之后一定要判断返回结果是否存在,即$name是否为undef。如下是一个简单的DNS A记录解析系统:

#!/usr/bin/perl
use strict;
use warnings;

use Socket qw(/inet/);
use 5.010;

die "Give me a hostname" unless @ARGV;

while(my $lookup = shift @ARGV){
    my ($name, $aliases, $addrtype, $length, @addrs) = gethostbyname $lookup;
    # must check $name defined or not
    if($name){
        foreach(@addrs){
            $_ = inet_ntoa($_);
        }
        if ($name eq$lookup){
            print "$lookup IP address: @addrs\n";
        } else {
            print "$lookup (real name $name) IP address: @addrs\n";
        }
    } else {
        print "host $lookup not found\n";
    }
}

执行:

$ perl host.pl www.sina.com
www.sina.com (real name wwwus.sina.com) IP address: 66.102.251.33

$ perl host.pl www.cnblogs.com www.baidu.com
www.cnblogs.com IP address: 42.121.252.58
www.baidu.com IP address: 183.232.231.174 183.232.231.172

Net::hostent

User::pwentUser::grent模块一样,Net::hostent提供了与内置函数gethostent、gethostbyname、gethostbyaddr同名的面向对象的方法,使得不需要中间繁琐的打包、解包过程。默认导入该模块时会覆盖这几个内置函数,如果想要引用这几个函数,可以加上CORE::前缀,如CORE::gethostbyname

my $host = gethostbyname 'www.baidu.com';
my $host = gethostbyaddr '192.168.100.21';

同样的,这个模块提供了hostent各字段对应的方法和变量:

$host->name       ==>   $h_name
$host->addr       ==>   $h_addr
$host->aliases    ==>   @h_aliases
$host->addrtype   ==>   $h_addrtype
$host->length     ==>   $h_length
$host->addr_list  ==>   @h_addr_list

此外,还提供了gethost()方法来替代gethostbyname()和gethostbyaddr(),它根据传递的参数类型来决定调用这两个方法中的哪一个。

网段解析:networks信息

相关函数和模块:

  • getnetent
  • setnetent
  • endnetent
  • getnetbyaddr
  • getnetbyname
  • Net::netent

必要基础

这个用的不多,因为它解析/etc/networks文件,而这个文件是定义网段(注意是网段而不是具体的IP地址)和网段字符串名一一映射关系的。而且,它指定指定A、B、C类网段(注意是网段而不是具体的IP地址),所以这个文件中网段总是以.0结尾的。

例如,基本上所有操作系统上都有这么两行:

default 0.0.0.0
loopback 127.0.0.0

这表示default代表的是0.0.0.0这个默认网段,loopback表示的是127.0.0.0网段。当有需要解析/etc/networks文件的程序(如route命令解析该文件,ip命令不解析该文件)需要用到网段时,就可以指定或显示该网段对应的名称,当然在查询的时候可以使用-n(一般是这个选项),来禁用名称到ID的解析映射。

用法

跳过,基本用不上。

服务和协议:services和protocols

/etc/services中记录了服务和端口/协议相关的信息。例如:

# service-name port/protocol [aliases ...]  [# comment]
tcpmux          1/tcp                       # TCP port service multiplexer
tcpmux          1/udp                       # TCP port service multiplexer
rje             5/tcp                       # Remote Job Entry
rje             5/udp                       # Remote Job Entry
echo            7/tcp
echo            7/udp
discard         9/tcp        sink null
discard         9/udp        sink null

$ grep ' 22/' /etc/services
ssh             22/tcp                      # The Secure Shell (SSH) Protocol
ssh             22/udp                      # The Secure Shell (SSH) Protocol
ssh             22/sctp                     # SSH

$ grep ' 3306/' /etc/services
mysql           3306/tcp                    # MySQL
mysql           3306/udp                    # MySQL

/etc/protocols文件中保存了tcp/ip协议栈各种协议的信息,包括协议名、代号、别名等等。例如ip协议的代号是0,别名是IP,tcp协议的代号是6,别名是TCP。这个文件是不能改的,一改就导致发送的TCP/IP相关的数据包出错。

这两个服务基本上不用管,除非要TCP/IP编程。

posted @ 2019-03-06 21:26 骏马金龙 阅读(...) 评论(...) 编辑 收藏