udhcpd源码分析2--读取配置文件
1:重要的结构体
读取配置文件信息到全局的结构体struct server_config_t server_config中,这个结构在很多文件中都有引用到很重要。
/* dhcpd.h */
struct server_config_t { u_int32_t server; /* Our IP, in network order */ u_int32_t start; /* Start address of leases, network order */ u_int32_t end; /* End of leases, network order */ struct option_set *options;/* List of DHCP options loaded from the config file */ char *interface; /* The name of the interface to use */ int ifindex; /* Index number of the interface to use */ unsigned char arp[6]; /* Our arp address */ unsigned long lease; /* lease time in seconds (host order) */ unsigned long max_leases; /* maximum number of leases (including reserved address) */ char remaining; /* should the lease file be interpreted as lease time remaining, or as the time the lease expires */ unsigned long auto_time; /* how long should udhcpd wait before writing a config file. if this is zero, it will only write one on SIGUSR1 */ unsigned long decline_time;/* how long an address is reserved if a client returns a decline message */ unsigned long conflict_time;/* how long an arp conflict offender is leased for */ unsigned long offer_time; /* how long an offered address is reserved */ unsigned long min_lease; /* minimum lease a client can request*/ char *lease_file; char *pidfile; char *notify_file; /* What to run whenever leases are written */ u_int32_t siaddr; /* next server bootp option */ char *sname; /* bootp server name */ char *boot_file; /* bootp boot file option */ };
英文释意也很明白,比较重要的有struct option_set *options;成员,它是一个指向记录配置文件中对opt配置的链表的指针,并且data以CLV方式存储,结构如下:
/* dhcpd.h */
struct option_set { unsigned char *data; struct option_set *next; };
2:读入配置文件
/* dhcpd.c */
#ifdef COMBINED_BINARY int udhcpd_main(int argc, char *argv[]) #else int main(int argc, char *argv[]) #endif { fd_set rfds; struct timeval tv; int server_socket = -1; int bytes, retval; struct dhcpMessage packet; unsigned char *state; unsigned char *server_id, *requested; u_int32_t server_id_align, requested_align; unsigned long timeout_end; struct option_set *option; struct dhcpOfferedAddr *lease; int pid_fd; int max_sock; int sig; OPEN_LOG("udhcpd"); LOG(LOG_INFO, "udhcp server (v%s) started", VERSION); memset(&server_config, 0, sizeof(struct server_config_t)); /* 读取配置文件到server_config结构中供全局使用 */ if (argc < 2) read_config(DHCPD_CONF_FILE);/* use default config file */ else read_config(argv[1]);/* use designated config file */
在files.c里的read_config函数是读取的入口:
/* files.c */
/* 配置文件每一行的格式为'key(空格or\t)value'的格式(特殊:opt key(空格or\t)value),value值的类型有以下几种 分别对应以下的处理方法 value值类型 处理方法 ip read_ip string read_str number read_u32 yes/no read_yn opt read_opt */ int read_config(char *file) { FILE *in; char buffer[80], orig[80], *token, *line; int i; /* 先将默认配置解析到server_config<全局的配置信息结构体>结构中 */ for (i = 0; strlen(keywords[i].keyword); i++) if (strlen(keywords[i].def)) keywords[i].handler(keywords[i].def, keywords[i].var); if (!(in = fopen(file, "r"))) { LOG(LOG_ERR, "unable to open config file: %s", file); return 0; } /* 将外部配置文件一行一行解析 */ while (fgets(buffer, 80, in)) { /* fgets可能会将'\n'读入,如有就将其替换为'\0' */ if (strchr(buffer, '\n')) *(strchr(buffer, '\n')) = '\0'; strncpy(orig, buffer, 80); /* 以#开头的行配置跳过 */ if (strchr(buffer, '#')) *(strchr(buffer, '#')) = '\0'; token = buffer + strspn(buffer, " \t"); if (*token == '\0') continue; line = token + strcspn(token, " \t="); if (*line == '\0') continue; *line = '\0';/* 截取行得到token(keyword) */ line++; /* 获得首尾无空白符的配置信息 */ /* eat leading whitespace */ line = line + strspn(line, " \t="); /* eat trailing whitespace */ for (i = strlen(line); i > 0 && isspace(line[i - 1]); i--); line[i] = '\0'; /* token就是key值 line即此行的配置信息 */ for (i = 0; strlen(keywords[i].keyword); i++) /* 确认key值正确(忽略大小写) */ if (!strcasecmp(token, keywords[i].keyword)) /* 将此行的配置更新到server_config中 */ if (!keywords[i].handler(line, keywords[i].var)) { /* 如果更新失败就使用默认配置 */ LOG(LOG_ERR, "unable to parse '%s'", orig); /* reset back to the default value */ keywords[i].handler(keywords[i].def, keywords[i].var); } } fclose(in); return 1; }
这是我见过用C来截取字符串比较精巧的设计(用strchr,strspn,strcspn三个字符串处理函数实现),以后有读取配置文件的编码需求完全可以参考其做法,最后在得到的token和line即配置项名字和配置内容.这里有一个辅助结构体数组不得不提:
/* files.c */
//struct config_keyword 将key、处理方法、要保存的地址、默认配置四项组在一起 static struct config_keyword keywords[] = { /* keyword[14] handler variable address default[20] */ {"start", read_ip, &(server_config.start), "192.168.0.20"}, {"end", read_ip, &(server_config.end), "192.168.0.254"}, {"interface", read_str, &(server_config.interface), "eth0"}, {"option", read_opt, &(server_config.options), ""}, {"opt", read_opt, &(server_config.options), ""}, {"max_leases", read_u32, &(server_config.max_leases), "254"}, {"remaining", read_yn, &(server_config.remaining), "yes"}, {"auto_time", read_u32, &(server_config.auto_time), "7200"}, {"decline_time",read_u32, &(server_config.decline_time),"3600"}, {"conflict_time",read_u32,&(server_config.conflict_time),"3600"}, {"offer_time", read_u32, &(server_config.offer_time), "60"}, {"min_lease", read_u32, &(server_config.min_lease), "60"}, {"lease_file", read_str, &(server_config.lease_file), "/var/lib/misc/udhcpd.leases"}, {"pidfile", read_str, &(server_config.pidfile), "/var/run/udhcpd.pid"}, {"notify_file", read_str, &(server_config.notify_file), ""}, {"siaddr", read_ip, &(server_config.siaddr), "0.0.0.0"}, {"sname", read_str, &(server_config.sname), ""}, {"boot_file", read_str, &(server_config.boot_file), ""}, /*ADDME: static lease */ {"", NULL, NULL, ""} };
它的结构体原型为:
/* files.h */
struct config_keyword { char keyword[14]; int (*handler)(char *line, void *var); void *var; char def[30]; };
在通过read_config函数的解析后得到的token就去匹配keyword,匹配到了就调用其函数指针handler,传入line和对应的结构体成员地址进行赋值,一气呵成!(通过一个结构体数组将所有的配置关键字和对应的处理方式罗列出来,不仅看起来条理清晰而且在需要添加新的配置项时也很方便,设计思路真的值得参考).
上面讲到过,根据配置项的默认值对应不同的处理函数,下面讲述一下这些处理函数:
/* files.c */
static int read_ip(char *line, void *arg)
/* 将字符串格式的ip地址转换为u_int32_t保存在地址arg中 */ /* on these functions, make sure you datatype matches */ static int read_ip(char *line, void *arg) { struct in_addr *addr = arg; struct hostent *host; int retval = 1; if (!inet_aton(line, addr)) { /* 当line不是ip地址而是主机或域名时解析出IP地址并保存 */ if ((host = gethostbyname(line))) addr->s_addr = *((unsigned long *) host->h_addr_list[0]); else retval = 0; } return retval; }
static int read_str(char *line, void *arg)
/* 首先free掉arg指向的内存,然后根据字符串line大小分配合适内存,把字符串line拷贝 到分配的内存中(strdup函数的功能),让arg指向新分配的内存 */ static int read_str(char *line, void *arg) { char **dest = arg; if (*dest) free(*dest);/* 使用前先free以防止内存泄露 */ *dest = strdup(line); return 1; }
static int read_u32(char *line, void *arg)
/* 将line指向的内容转换为unsigned long 的类型保存于arg指向的内存中 */ static int read_u32(char *line, void *arg) { u_int32_t *dest = arg; char *endptr; *dest = strtoul(line, &endptr, 0); return endptr[0] == '\0'; }
static int read_yn(char *line, void *arg)
/* line为yes<忽略大小写> arg所指成员赋1,否则赋值0 */ static int read_yn(char *line, void *arg) { char *dest = arg; int retval = 1; if (!strcasecmp("yes", line)) *dest = 1; else if (!strcasecmp("no", line)) *dest = 0; else retval = 0; return retval; }
最后一个read_opt函数显得比较特殊,因为它的配置文件格式是opt/option name value,所以通过read_config函数之后,token是opt/option,line是name value,这样的line还得进行一次类似read_config函数的解析得到toke=name和line=value,最后将这个添加到struct option_set *options;指向的一个单链表中,类似的解析就在read_opt函数中进行.
3:配置文件中选项的读取
read_opt的思路和read_config函数的思路是相似的,先介绍一个重要的结构体数组options
/* options.c */
/* supported options are easily added here */ struct dhcp_option options[] = { /* name[10] flags code */ {"subnet", OPTION_IP | OPTION_REQ, 0x01}, {"timezone", OPTION_S32, 0x02}, {"router", OPTION_IP | OPTION_LIST | OPTION_REQ, 0x03}, {"timesvr", OPTION_IP | OPTION_LIST, 0x04}, {"namesvr", OPTION_IP | OPTION_LIST, 0x05}, {"dns", OPTION_IP | OPTION_LIST | OPTION_REQ, 0x06}, {"logsvr", OPTION_IP | OPTION_LIST, 0x07}, {"cookiesvr", OPTION_IP | OPTION_LIST, 0x08}, {"lprsvr", OPTION_IP | OPTION_LIST, 0x09}, {"hostname", OPTION_STRING | OPTION_REQ, 0x0c}, {"bootsize", OPTION_U16, 0x0d}, {"domain", OPTION_STRING | OPTION_REQ, 0x0f}, {"swapsvr", OPTION_IP, 0x10}, {"rootpath", OPTION_STRING, 0x11}, {"ipttl", OPTION_U8, 0x17}, {"mtu", OPTION_U16, 0x1a}, {"broadcast", OPTION_IP | OPTION_REQ, 0x1c}, {"ntpsrv", OPTION_IP | OPTION_LIST, 0x2a}, {"wins", OPTION_IP | OPTION_LIST, 0x2c}, {"requestip", OPTION_IP, 0x32}, {"lease", OPTION_U32, 0x33}, {"dhcptype", OPTION_U8, 0x35}, {"serverid", OPTION_IP, 0x36}, {"message", OPTION_STRING, 0x38}, {"tftp", OPTION_STRING, 0x42}, {"bootfile", OPTION_STRING, 0x43}, {"", 0x00, 0x00} };
这里建立了配置选项关键字及flag和code之间的联系.再看read_opt函数
/* files.c */
/* 函数将line中的name和value解析出来,通过name值定位到结构体数组 的options[X]、value通过函数转换得到的buffer、buffer的长度length、 及链表的头指针opt_list一起交由函数attach_option处理 */ /* read a dhcp option and add it to opt_list */ static int read_opt(char *line, void *arg) { struct option_set **opt_list = arg; char *opt, *val, *endptr; struct dhcp_option *option = NULL; int retval = 0, length = 0; char buffer[255]; u_int16_t result_u16; u_int32_t result_u32; int i; if (!(opt = strtok(line, " \t="))) return 0; /* 通过name找到结构体数组options中的对应项,和此name的code值联系起来 */ for (i = 0; options[i].code; i++) if (!strcmp(options[i].name, opt)) option = &(options[i]); if (!option) return 0; do { val = strtok(NULL, ", \t"); if (val) { length = option_lengths[option->flags & TYPE_MASK]; retval = 0; switch (option->flags & TYPE_MASK) { case OPTION_IP: retval = read_ip(val, buffer); break; case OPTION_IP_PAIR: retval = read_ip(val, buffer); if (!(val = strtok(NULL, ", \t/-"))) retval = 0; if (retval) retval = read_ip(val, buffer + 4); break; case OPTION_STRING: length = strlen(val); if (length > 0) { if (length > 254) length = 254; memcpy(buffer, val, length); retval = 1; } break; case OPTION_BOOLEAN: retval = read_yn(val, buffer); break; case OPTION_U8: buffer[0] = strtoul(val, &endptr, 0); retval = (endptr[0] == '\0'); break; case OPTION_U16: result_u16 = htons(strtoul(val, &endptr, 0)); memcpy(buffer, &result_u16, 2); retval = (endptr[0] == '\0'); break; case OPTION_S16: result_u16 = htons(strtol(val, &endptr, 0)); memcpy(buffer, &result_u16, 2); retval = (endptr[0] == '\0'); break; case OPTION_U32: result_u32 = htonl(strtoul(val, &endptr, 0)); memcpy(buffer, &result_u32, 4); retval = (endptr[0] == '\0'); break; case OPTION_S32: result_u32 = htonl(strtol(val, &endptr, 0)); memcpy(buffer, &result_u32, 4); retval = (endptr[0] == '\0'); break; default: break; } /* 把选项值放在以code升序的单链表中 */ if (retval) attach_option(opt_list, option, buffer, length); }; } while (val && retval && option->flags & OPTION_LIST); return retval; }
进入attach_option函数:
/* options.c */
/* add an option to the opt_list */ void attach_option(struct option_set **opt_list, struct dhcp_option *option, char *buffer, int length) { struct option_set *existing, *new, **curr; /* add it to an existing option */ /* 如果此opt已存在链表中则将buff添加到原值的后面 OPT_CODE=0,OPT_LEN=1*/ if ((existing = find_option(*opt_list, option->code))) { DEBUG(LOG_INFO, "Attaching option %s to existing member of list", option->name); if (option->flags & OPTION_LIST) { if (existing->data[OPT_LEN] + length <= 255) { existing->data = realloc(existing->data, existing->data[OPT_LEN] + length + 2); memcpy(existing->data + existing->data[OPT_LEN] + 2, buffer, length); /* byte byte length*byte length - - - - - - - - - - - - - - - - - - - - - - - -- | | | | | | code | length | buff_old | buff_new | | | | | | - - - - - - - - - - - - - - - - - - - - - - - -- */ existing->data[OPT_LEN] += length; } /* else, ignore the data, we could put this in a second option in the future */ } /* else, ignore the new data */ } else { /* 按照code值顺序添加新节点到链表中 */ DEBUG(LOG_INFO, "Attaching option %s to list", option->name); /* make a new option */ new = malloc(sizeof(struct option_set)); new->data = malloc(length + 2); new->data[OPT_CODE] = option->code; new->data[OPT_LEN] = length; memcpy(new->data + 2, buffer, length); /* curr始终作为指向新节点指针的指针 */ curr = opt_list; while (*curr && (*curr)->data[OPT_CODE] < option->code) curr = &(*curr)->next; new->next = *curr; *curr = new; } }
这就是上面讲述的使用CLV的方式存储选项配置,这个配置选项链表是升序的单链表,传入的是指向链表头部的指针的指针(这点很重要,以前单链表的操作会考虑插入作为第一个节点这种特殊情况,这里就不用).
4:总结
配置文件的读取模块大概就是以上的内容,通过分析实现源码觉得有很多实现方式很值得参考.
可作为轮子用的有:
1:通过strchr,strspn,strcspn三个字符串处理函数截取配置信息
2:attach_option函数中对于升序链表的操作
值得参考的设计思路:
1:将配置信息的名字,处理方法,保存位置,默认值组织一个结构体数组,这样的管理方式结构清晰且易于扩展