名字和地址转换

系列文章导航:《Unix 网络编程》笔记

域名系统

简介

域名系统主要用于主机名字和 IP 地址之间的映射。主机名可以是:

  • 简单名字,如:centos01
  • 全限定域名(FQDN[1]),如:xxx.com

资源记录

记录 作用
A 指向IPv4
AAAA 指向IPv6
PTR 把IP地址映射为主机名
MX 邮件记录
CNAME 为二级域名指定域名或IP

解析器和名字服务器

DNS

DNS替代方法

如果使用 DNS 查找主机名,则使用 /etc/resolv.conf 指定的 DNS

有如下替代方法:

  • 静态主机文件,如 /etc/hosts
  • 网络信息系统(NIS)
  • 轻权目录访问协议(LDAP)

所有这些差异对应用开发人员是透明的,我们只需调用相关的解析器函数即可

IPv4 函数学习

域名和地址转换

gethostbyname

执行对 A 记录的查询,返回 IPv4 地址:

#include <netdb.h>

struct hostent *gethostbyname(const char * hostname);

// hostent:
struct hostent {
char *h_name; // 正式主机名
char **h_aliases; // 别名s
int h_addrtype; // AF_INET
int h_length; // 4 (32位IP地址)
char **h_addr_list; // IP地址s
}

错误情况

发生错误时,不设置 errno 变量,而是将全局整型变量 h_errno 设置为在头文件 netdb.h 中定义的如下常量之一:

  • HOST_NOT_FOUND
  • TRY_AGAIN
  • NO_RECOVERY
  • NO_DATA(等同于 NO_ADDRESS):表明主机在,但是没有 A 记录

多数解析器提供名为 hstrerror 函数,可以将某个 h_errno 代表的具体错误信息返回

案例

#include "unp.h"

int main(int argc, char **argv)
{
char *ptr, **pptr;
char str[INET_ADDRSTRLEN];
struct hostent *hptr; while (--argc > 0)
{
// 遍历每一个域名
ptr = *++argv;
if ((hptr = gethostbyname(ptr)) == NULL)
{
// 错误信息
err_msg("gethostbyname error for host: %s: %s",
ptr, hstrerror(h_errno));
continue;
} // 各种打印
printf("official hostname: %s\n", hptr->h_name); for (pptr = hptr->h_aliases; *pptr != NULL; pptr++)
printf("\talias: %s\n", *pptr); switch (hptr->h_addrtype)
{
case AF_INET:
pptr = hptr->h_addr_list;
for (; *pptr != NULL; pptr++)
printf("\taddress: %s\n",
Inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
break; default:
err_ret("unknown address type");
break;
}
}
exit(0);
}
[root@centos-5610 names]# ./hostent ethy.cn www.ethy.cn smtp.ethy.cn mail.ethy.cn
official hostname: ym.163.com
alias: ethy.cn
address: 117.147.199.37
gethostbyname error for host: www.ethy.cn: Unknown host
official hostname: cli.ym.ntes53.netease.com
alias: smtp.ethy.cn
alias: smtp.ym.163.com
address: 101.71.155.42

gethostbyaddr

与上一个的功能正好相反,查询 PTR 记录

#inlcude <netdh.h>

struct hostent *gethostbyaddr(const char *addr,
socklen_t len, // 对于 IPv4 为4
int family); // AF_INET

服务和端口转换

/etc/services 文件中保存了许多知名服务的端口和服务名称的映射,如下:

 time            37/tcp          timserver
time 37/udp timserver
rlp 39/tcp resource # resource location
rlp 39/udp resource # resource location
nameserver 42/tcp name # IEN 116
nameserver 42/udp name # IEN 116
nicname 43/tcp whois
nicname 43/udp whois
tacacs 49/tcp # Login Host Protocol (TACACS)
tacacs 49/udp # Login Host Protocol (TACACS)
re-mail-ck 50/tcp # Remote Mail Checking Protocol
re-mail-ck 50/udp # Remote Mail Checking Protocol
domain 53/tcp # name-domain server
domain 53/udp
whois++ 63/tcp whoispp
whois++ 63/udp whoispp

getservbyname

#include <netdb.h>

struct servent *getservbyname(const char* servname, const char *protoname);

// servent
struct servent {
char *s_name;
char **s_aliases;
int s_port;
char *s_proto;
}

几个案例:

getservbyname("domain", "udp");
getservbyname("ftp", "tcp");
getservbyname("ftp", NULL);
getesrvbyname("ftp", "udp");

如果没有指定协议,则会自动匹配(一般来说同一服务的 TCP 和 UDP 端口是相同的),但是如果指定的协议没有,则会报错

getservbyport

#include <netdb.h>

struct servent *getservbyport(int port, const char *protoname);

其中 port 参数必须为网络字节序,例如:

getservbyport(htons(53), "udp");

相同的端口上,不同的协议可能有不同的服务!

时间服务客户端改进

可以通过上面所学对时间服务的客户端进行改进:

int main(int argc, char** argv) {
int sockfd, n;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
struct in_addr** pptr;
struct in_addr* inetaddrp[2];
struct in_addr inetaddr;
struct hostent* hp;
struct servent* sp; if (argc != 3)
err_quit("usage: daytimetcpcli1 <hostname> <service>"); printf("%s:%s\n", argv[1], argv[2]);
// 获取域名对应的地址
if ((hp = gethostbyname(argv[1])) == NULL) {
// 获取失败, 猜测可能是用户输入了IP地址,所以进行转换
// 将 IP 地址从点分十进制转换为32位二进制
if (inet_aton(argv[1], &inetaddr) == 0) {
// 失败了
err_quit("hostname error for %s: %s", argv[1], hstrerror(h_errno));
} else {
// 保存
inetaddrp[0] = &inetaddr;
inetaddrp[1] = NULL;
pptr = inetaddrp;
}
} else {
// 直接转换出来的就是32位二进制
pptr = (struct in_addr**)hp->h_addr_list;
} // 服务转换
if ((sp = getservbyname(argv[2], "tcp")) == NULL)
err_quit("getservbyname error for %s", argv[2]); // 循环,对查询得到的所有IP进行访问
for (; *pptr != NULL; pptr++) {
sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = sp->s_port;
memcpy(&servaddr.sin_addr, *pptr, sizeof(struct in_addr));
printf("trying %s\n", Sock_ntop((SA*)&servaddr, sizeof(servaddr))); // 只要有一个成功
if (connect(sockfd, (SA*)&servaddr, sizeof(servaddr)) == 0)
break; /* success */
err_ret("connect error");
close(sockfd);
}
if (*pptr == NULL)
err_quit("unable to connect"); // 输出
while ((n = Read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
exit(0);
}

getaddrinfo 函数

getaddrinfo 介绍

优势:

  • 这个函数可以处理域名和地址的转换,以及服务和端口的转换
  • 返回一个 sockaddr 列表,而不是上面的结构,这些 sockaddr 可以直接使用
  • 把协议相关的内容隐藏了起来
#include <netdb.h>

int getaddrinfo(const char *hostname, // 主机名或地址串
const char *service, // 服务名或十进制端口号数串
const struct addrinfo *hints, // 填写对期望结果的暗示
struct addrinfo **result); // 返回的信息保存在这里

参数解释

  • hints

    • hints 可以是一个空指针,也可以是一个指向某个 addrinfo 结构的指针
    • 调用者在其中可以填写关于期望返回信息类型的暗示,前四个属性都可以设置
  • result

    • 如果函数返回0,则会更新 result 对应的 addrinfo 链表
    • 链表可能有多个项,这取决于 hostname 关联了几个地址,以及是否有不同的协议类型
    • 链表是无序的,也就是说 TCP 未必会放在前面
    • 返回的 addrinfo 中的信息可以用于 socket 的相关操作,如 connect、sendto、bind
  • 如果 ai_flags 设置了 AI_CANONNAME ,那么返回的第一个 addrinfo 结构的 ai_canonname 指向所查找主机的规范名字

addrinfo 结构

// addrinfo
struct addrinfo {
int ai_flags; // 一些标志位,用来进行特殊的设置
int ai_family; // AF_XXX 如 AF_INET、AF_INET6
int ai_socktype; // SOCK_XXX 如 SOCK_STREAM SOCK_DGRAM
int ai_protocol; // 协议名称,如 IPPROTO_TCP,如果前面两项可以唯一确认,则此项可为0
socklen_t ai_addrlen;
char *ai_canonname;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;
}

下面对各个结构进行详细的解释:

  • ai_flags 用来设置一些标志位

    可用的标志值和含义如下:请忽略点符号,这里只是为了方便阅读

    标志值 作用
    AI_PASSIVE 套接字将用于被动打开(如服务器端)
    AI_CAN.ON.NAME 返回主机的规范名称,保存在返回的链表的第一项的 ai_canonname
    AI_NUMERIC.HOST 防止任何类型的名字到地址映射,hostname 必须是一个地址串
    AI_NUMERIC.SERV 放置任何类型的服务到端口映射,service 必须是十进制端口号数串
    AI_V4.MAPPED 如果同时指定 ai_family 为 AF_INET6,又没有 AAAA 记录,就返回 IPv4 对应的 IPv6 地址
    AI_ALL 如果同时指定上一项,则返回 IPv6 和 IPv4 对应的 IPv6 地址
    AI_ADDR.CONFIG 按照所在主机的配置选择返回地址类型
  • family、socktype、protocol 已经在注释上说的比较清楚了,他们可以直接被使用来操作 socket

  • ai_addrlen ai_addr 套接字结构的大小

  • ai_canonname 在上表格中提到了,在需要的时候是主机的规范名称

  • ai_addr 指向套接字地址的指针,已经被函数填充好了,且类型自适应

  • ai_next 指向下一位

案例

一个调用的案例:

struct addrinfo hints, *res;

bzero(&hints, sizeof(hints));
hints.ai_flags = AI_CANONNAME;
hints.ai_family = AF_INET; getaddrinfo("freebsd4", "domain", &hints, &res);

一个可能的结果:

最佳实践

在调用 getaddrinfo 时,共有 6 个可以选择的参数组合:

  • hostname、service
  • ai_flags、ai_family、ai_socktype、ai_protocol

常见的组合方式如下所述

客户端

  • TCP

    1. 指定 hostname 和 service
    2. 返回后,针对返回的所有 IP 地址,逐一调用 socket 和 connect,直到有一个连接成功,或全部失败
  • UDP

    1. 返回后,调用 sendto 或 connect

    2. 如果客户能够判定第一个地址看起来不工作,则尝试其余的地址

      两种判断方式:

      1. 已连接,获取到错误信息
      2. 未连接,等待消息超时
  • 如果客户端清楚套接字的类型,则应该设置 hints 为合适的值

服务器

  • TCP

    1. 只指定 service,不指定hostname
    2. 在 hints 结构中指定 AI_PASSIVE 标志
    3. TCP 服务器随后调用 socket、bind、listen
  • UDP

    1. 调用 socket、bind、recvfrom
  • 如果服务器清楚套接字的类型,则应该设置 hints 为合适的值

返回的 addrinfo 结构的数目

返回的 addrinfo 的数目和暗示信息中 ai_socktype 的对应关系:

gai_strerror

#include <netdb.h>

const char *gai_strerror(int error);

作用:对于 getaddrinfo 的非 0 错误码,将该数值作为参数,输出其对应的错误信息

freeaddrinfo

作用

getaddrinfo 返回的所有存储空间都是动态获取的(比如 malloc),包括:

  • addrinfo 结构
  • ai_addr 结构
  • ai_canonname 字符串

这些存储空间通过 freeaddrinfo 返还给系统

#include <netdb.h>

void freeaddrinfo(struct addrinfo *ai);

注意

  • 在头节点上调用,会释放整个 addrinfo 链表
  • 注意如果你采用浅拷贝使用了某些属性,则再访问时可能会因为该地址已经被释放而出错

自己封装函数

host_serv

作用:简化 getaddrinfo 的步骤

struct addrinfo* host_serv(const char* host,
const char* serv,
int family,
int socktype) {
int n;
struct addrinfo hints, *res; bzero(&hints, sizeof(struct addrinfo));
hints.ai_flags = AI_CANONNAME; /* always return canonical name */
hints.ai_family = family; /* AF_UNSPEC, AF_INET, AF_INET6, etc. */
hints.ai_socktype = socktype; /* 0, SOCK_STREAM, SOCK_DGRAM, etc. */ if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
return (NULL); return (res); /* return pointer to first on linked list */
}

tcp_connect

作用:创建一个 TCP 套接字并连接到一个服务器

其步骤和上文最佳实践部分基本一致

int tcp_connect(const char* host, const char* serv) {
int sockfd, n;
struct addrinfo hints, *res, *ressave; bzero(&hints, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM; if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
err_quit("tcp_connect error for %s, %s: %s", host, serv,
gai_strerror(n));
ressave = res; do {
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sockfd < 0)
continue; /* ignore this one */ if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
break; /* success */ Close(sockfd); /* ignore this one */
} while ((res = res->ai_next) != NULL); if (res == NULL) /* errno set from final connect() */
err_sys("tcp_connect error for %s, %s", host, serv); freeaddrinfo(ressave); return (sockfd);
}

时间程序客户端改进

这个类似于前面部分的,只不过把部分步骤封装在 tcp_connect 中了!

int main(int argc, char** argv) {
int sockfd, n;
char recvline[MAXLINE + 1];
socklen_t len;
struct sockaddr_storage ss; if (argc != 3)
err_quit("usage: daytimetcpcli <hostname/IPaddress> <service/port#>"); sockfd = Tcp_connect(argv[1], argv[2]); len = sizeof(ss);
Getpeername(sockfd, (SA*)&ss, &len);
printf("connected to %s\n", Sock_ntop_host((SA*)&ss, len)); while ((n = Read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
exit(0);
}

tcp_listen

int tcp_listen(const char* host, const char* serv, socklen_t* addrlenp) {
int listenfd, n;
const int on = 1;
struct addrinfo hints, *res, *ressave; bzero(&hints, sizeof(struct addrinfo));
hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM; if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
err_quit("tcp_listen error for %s, %s: %s", host, serv,
gai_strerror(n));
ressave = res; do {
listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (listenfd < 0)
continue; /* error, try next one */ Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)
break; /* success */ Close(listenfd); /* bind error, close and try next one */
} while ((res = res->ai_next) != NULL); if (res == NULL) /* errno from final socket() or bind() */
err_sys("tcp_listen error for %s, %s", host, serv); Listen(listenfd, LISTENQ); if (addrlenp)
*addrlenp = res->ai_addrlen; /* return size of protocol address */ freeaddrinfo(ressave); return (listenfd);
}

时间程序服务器端改进

用 tcp_listen 代替部分步骤

int main(int argc, char** argv) {
int listenfd, connfd;
socklen_t len;
char buff[MAXLINE];
time_t ticks;
struct sockaddr_storage cliaddr; if (argc != 2)
err_quit("usage: daytimetcpsrv1 <service or port#>"); listenfd = Tcp_listen(NULL, argv[1], NULL); for (;;) {
len = sizeof(cliaddr);
connfd = Accept(listenfd, (SA*)&cliaddr, &len);
printf("connection from %s\n", Sock_ntop((SA*)&cliaddr, len)); ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
Write(connfd, buff, strlen(buff)); Close(connfd);
}
}

再次改进

上述代码有一个问题:

  • tcp_listen 的第一个参数是 NULL
  • 而且 tcp_listen 内部指定的地址族为 AF_UNSPEC
  • 两者结合可能导致 getaddrinfo 返回非期望地址族的套接字地址结构

用一个小技巧,可以指定,使用 IPv6 还是 IPv4:

int main(int argc, char** argv) {
int listenfd, connfd;
socklen_t len, addrlen;
char buff[MAXLINE];
time_t ticks;
struct sockaddr_storage cliaddr; if (argc == 2)
listenfd = Tcp_listen(NULL, argv[1], &addrlen);
else if (argc == 3)
listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
else
err_quit("usage: daytimetcpsrv2 [ <host> ] <service or port>"); for (;;) {
len = sizeof(cliaddr);
connfd = Accept(listenfd, (SA*)&cliaddr, &len);
printf("connection from %s\n", Sock_ntop((SA*)&cliaddr, len)); ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
Write(connfd, buff, strlen(buff)); Close(connfd);
}
}

测试案例:

[root@centos-5610 names]# ./daytimetcpsrv2 0::0 daytime
connection from [fe80::5054:ff:fe4d:77d3]:37428 [root@centos-5610 names]# ./daytimetcpsrv2 0.0.0.0 daytime
connection from 10.0.2.15:52178

udp_client

这个套接字地址结构的大小在 lenp 中返回,不允许是一个空指针(而TCP允许),因为 sendto 和 recvfrom 调用都需要直到套接字地址结构的长度

int udp_client(const char* host,
const char* serv,
SA** saptr,
socklen_t* lenp) {
int sockfd, n;
struct addrinfo hints, *res, *ressave; bzero(&hints, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM; if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
err_quit("udp_client error for %s, %s: %s", host, serv,
gai_strerror(n));
ressave = res; do {
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sockfd >= 0)
break; /* success */
} while ((res = res->ai_next) != NULL); if (res == NULL) /* errno set from final socket() */
err_sys("udp_client error for %s, %s", host, serv); *saptr = Malloc(res->ai_addrlen);
memcpy(*saptr, res->ai_addr, res->ai_addrlen);
*lenp = res->ai_addrlen; freeaddrinfo(ressave); return (sockfd);
}

协议无关时间获取客户程序(UDP)

这里协议无关指的是 IPv4 or IPv6

int main(int argc, char** argv) {
int sockfd, n;
char recvline[MAXLINE + 1];
socklen_t salen;
struct sockaddr* sa; if (argc != 3)
err_quit("usage: daytimeudpcli1 <hostname/IPaddress> <service/port#>"); sockfd = Udp_client(argv[1], argv[2], (void**)&sa, &salen); printf("sending to %s\n", Sock_ntop_host(sa, salen)); Sendto(sockfd, "", 1, 0, sa, salen); /* send 1-byte datagram */ n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = '\0'; /* null terminate */
Fputs(recvline, stdout); exit(0);
}

udp_connect

  • 由于 connect 会保存相关的端口和端口信息,所以我们只需要知道返回的描述符就可以了
  • 和 tcp_connect 不同,UDP 的错误在发送一个数据报才能知晓(没有三次握手的过程)
int main(int argc, char** argv) {
int sockfd, n;
char recvline[MAXLINE + 1];
socklen_t salen;
struct sockaddr* sa; if (argc != 3)
err_quit("usage: daytimeudpcli1 <hostname/IPaddress> <service/port#>"); sockfd = Udp_client(argv[1], argv[2], (void**)&sa, &salen); printf("sending to %s\n", Sock_ntop_host(sa, salen)); Sendto(sockfd, "", 1, 0, sa, salen); /* send 1-byte datagram */ n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = '\0'; /* null terminate */
Fputs(recvline, stdout); exit(0);
}

udp_server

int main(int argc, char** argv) {
int sockfd, n;
char recvline[MAXLINE + 1];
socklen_t salen;
struct sockaddr* sa; if (argc != 3)
err_quit("usage: daytimeudpcli1 <hostname/IPaddress> <service/port#>"); sockfd = Udp_client(argv[1], argv[2], (void**)&sa, &salen); printf("sending to %s\n", Sock_ntop_host(sa, salen)); Sendto(sockfd, "", 1, 0, sa, salen); /* send 1-byte datagram */ n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = '\0'; /* null terminate */
Fputs(recvline, stdout); exit(0);
}

和上面 tcp_server 一样,可以通过那个小技巧来决定使用 IPv4 还是 IPv6

getnameinfo

  • getaddrinfo 的互补函数
  • 以一个套接字地址为参数,返回描述其中的主机的一个字符串和描述其中的服务的另一个字符串
  • 协议无关,函数内部自行处理
#include <netdb.h>

int getnameinfo(const struct sockaddr *sockaddr, socklen_t addrlen,
char *host, sockelne_t hostlen, // 调用者预先分配
char *serv, socklent_t servlen, // 调用者预先分配
int flags);

flags:

常值 说明 备注
NI_DGRAM 数据报服务 如果知道是UDP,则应设置,以免部分服务端口的冲突
NI_NAME.REQD 若不能从地址解析出名字则返回错误
NI_NO.FQDN 只返回FQDN的主机名部分 如a.foo.com,将截断为a
NI_NUMERIC.HOST 以数串格式返回主机字符串 不要调用 DNS,
NI_NUMERIC.SCOPE 以数串格式返回范围标识字符串
NI_NUMERIC.SERV 以数串格式返回服务字符串 服务器通常应该设置这个标识

可重入函数

定义

可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入 OS 调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如 全局变量区, 中断向量表 等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

在使用时需要注意:

  • gethostbynamegethostbyaddrgetservbynamegetservbyport 都是不可重入的,因为它们都返回指向同一个静态结构的指针

  • inet_ptoninet_ntop 总是可重入的

  • 因为历史原因,inet_ntoa 是不可重入的,不过部分实现提供了使用线程特定数据的可重入版本

  • getaddrinfo 可重入的前提是由它调用的函数都可重入,这就是说,它应该调用可重入版本的 gethostbyname 和 getservbyname

  • getnameinfo 可重入的前提是由它调用的函数都可重入,这就是说,它应该调用可重入版本的 gethostbyaddr 和 getservbyport

  • errno 在每一个进程各有一个副本,但是多线程下也会发生被其他线程修改的情况

解决方案

  • 不使用函数的不可重入版本

  • 就 errno 例子而言,可以使用类似如下的代码进行避免:

    void sig_alrm(int signo) {
    int errno_save;
    errno_save = errno;
    if (write( ... ) != nbytes) {
    fprintf(stderr, "Errno = %d\n", errno);
    }
    errno = errno_save;
    }

可重入版本

gethostbyname_r

gethostbyaddr_r

具体描述暂略

本文没有提到的

  • 作废的IPv6地址解析函数
  • 其他网络相关信息

由于此两章暂时用不到,故略


  1. Full Qualified Domain Name

最新文章

  1. 设置line-height:1.5和line-height:150%或者line-height:150px的区别
  2. ArcGIS Engine开发之空间查询
  3. Dom 概览
  4. Shiro权限验证代码记录,正确找到shiro框架在什么地方做了权限识别
  5. iOS开发 传感器(加速计、摇一摇、计步器)
  6. android application plugins framework
  7. Oracle 用户权限分配说明
  8. 3223: Tyvj 1729 文艺平衡树 - BZOJ
  9. Requirements
  10. Find out C++ Memory Usage in Code
  11. 回调函数 callback 的简单理解
  12. linux命令:env
  13. 刚收到一个吃瓜群众看了肯定不信的offer!
  14. Redux进阶(一)
  15. 并发系列(5)之 Future 框架详解
  16. 你可能不知道的printf
  17. 安装Logtail(Linux系统)
  18. [No000011C]使人醒悟的生活中的定律
  19. VMware Ubuntu如何连接互联网
  20. pgm5

热门文章

  1. 深入理解ES6之《用模块封装代码》
  2. ubantu之Git使用
  3. java中this这个概念初学者非常难理解,请举例说明
  4. 帝国cms插件 解决后台修改信息时内容关键字不替换的问题
  5. 学习打卡day16&amp;&amp;echarts入门
  6. iNeuOS工业互联网操作系统,三维(3D)模型在线编辑应用和实时数据统计(和值、均值、众数、方差、中位数等)
  7. js常用框架原理
  8. 2021.08.06 P4392 Sound静音问题(ST表)
  9. for in 语法遍历对象
  10. 2022最新IntellJ IDEA的zheng开发部署文档