获取主机名、用户、组、网络信息相关函数
首先是获取主机名的方式,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信息
获取用户信息和组信息的方式是一样的,相关函数为getpwent
、setpwent
、endpwent
、getpwnam
、getpwuid
,唯一不一样的是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
gethostent
、gethostbyaddr
和gethostbyname
用来给给定的主机名或地址进行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.172
和183.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($_) }
- 使用unpack():
看下面的例子就知道了。
例如,下面使用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::pwent
和User::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编程。