hostapd的分析
来源:互联网 发布:淘宝店铺被彻底释放 编辑:程序博客网 时间:2024/05/30 12:30
Hostapd的功能就是作为AP的认证服务器,负责控制管理stations(通常可以认为带无线网卡的PC)的接入和认证。
通过Hostapd可以将无线网卡切换为AP/Master模式,通过修改配置文件,可以建立一个开放式的(不加密)的,WEP,WPA或WPA2的无线网络。并且通过修改配置文件可以设置无线网卡的各种参数,包括频率,信号,beacon包时间间隔,是否发送beacon包,如果响应探针请求等等。还可以设置mac地址过滤条件等。
hostapd的main函数
int main(int argc, char *argv[]){ struct hapd_interfaces interfaces; int ret = 1; size_t i, j; int c, debug = 0, daemonize = 0; char *pid_file = NULL; const char *log_file = NULL; const char *entropy_file = NULL; char **bss_config = NULL, **tmp_bss; size_t num_bss_configs = 0;#ifdef CONFIG_DEBUG_LINUX_TRACING int enable_trace_dbg = 0;#endif /* CONFIG_DEBUG_LINUX_TRACING */ int start_ifaces_in_sync = 0; char **if_names = NULL; size_t if_names_size = 0; if (os_program_init()) return -1; os_memset(&interfaces, 0, sizeof(interfaces)); interfaces.reload_config = hostapd_reload_config; interfaces.config_read_cb = hostapd_config_read; interfaces.for_each_interface = hostapd_for_each_interface; interfaces.ctrl_iface_init = hostapd_ctrl_iface_init; interfaces.ctrl_iface_deinit = hostapd_ctrl_iface_deinit; interfaces.driver_init = hostapd_driver_init; interfaces.global_iface_path = NULL; interfaces.global_iface_name = NULL; interfaces.global_ctrl_sock = -1; dl_list_init(&interfaces.global_ctrl_dst); for (;;) { c = getopt(argc, argv, "b:Bde:f:hi:KP:STtu:vg:G:"); if (c < 0) break; switch (c) { case 'h': usage(); break; case 'd': debug++; if (wpa_debug_level > 0) wpa_debug_level--; break; case 'B': daemonize++; break; case 'e': entropy_file = optarg; break; case 'f': log_file = optarg; break; case 'K': wpa_debug_show_keys++; break; case 'P': os_free(pid_file); pid_file = os_rel2abs_path(optarg); break; case 't': wpa_debug_timestamp++; break;#ifdef CONFIG_DEBUG_LINUX_TRACING case 'T': enable_trace_dbg = 1; break;#endif /* CONFIG_DEBUG_LINUX_TRACING */ case 'v': show_version(); exit(1); break; case 'g': if (hostapd_get_global_ctrl_iface(&interfaces, optarg)) return -1; break; case 'G': if (hostapd_get_ctrl_iface_group(&interfaces, optarg)) return -1; break; case 'b': tmp_bss = os_realloc_array(bss_config, num_bss_configs + 1, sizeof(char *)); if (tmp_bss == NULL) goto out; bss_config = tmp_bss; bss_config[num_bss_configs++] = optarg; break; case 'S': start_ifaces_in_sync = 1; break;#ifdef CONFIG_WPS case 'u': return gen_uuid(optarg);#endif /* CONFIG_WPS */ case 'i': if (hostapd_get_interface_names(&if_names, &if_names_size, optarg)) goto out; break; default: usage(); break; } } if (optind == argc && interfaces.global_iface_path == NULL && num_bss_configs == 0) usage(); wpa_msg_register_ifname_cb(hostapd_msg_ifname_cb); if (log_file) wpa_debug_open_file(log_file); else wpa_debug_setup_stdout();#ifdef CONFIG_DEBUG_LINUX_TRACING if (enable_trace_dbg) { int tret = wpa_debug_open_linux_tracing(); if (tret) { wpa_printf(MSG_ERROR, "Failed to enable trace logging"); return -1; } }#endif /* CONFIG_DEBUG_LINUX_TRACING */ interfaces.count = argc - optind; if (interfaces.count || num_bss_configs) { interfaces.iface = os_calloc(interfaces.count + num_bss_configs, sizeof(struct hostapd_iface *)); if (interfaces.iface == NULL) { wpa_printf(MSG_ERROR, "malloc failed"); return -1; } } if (hostapd_global_init(&interfaces, entropy_file)) { wpa_printf(MSG_ERROR, "Failed to initialize global context"); return -1; } eloop_register_timeout(HOSTAPD_CLEANUP_INTERVAL, 0, hostapd_periodic, &interfaces, NULL); if (fst_global_init()) { wpa_printf(MSG_ERROR, "Failed to initialize global FST context"); goto out; }#if defined(CONFIG_FST) && defined(CONFIG_CTRL_IFACE) if (!fst_global_add_ctrl(fst_ctrl_cli)) wpa_printf(MSG_WARNING, "Failed to add CLI FST ctrl");#endif /* CONFIG_FST && CONFIG_CTRL_IFACE */ /* Allocate and parse configuration for full interface files */ for (i = 0; i < interfaces.count; i++) { char *if_name = NULL; if (i < if_names_size) if_name = if_names[i]; interfaces.iface[i] = hostapd_interface_init(&interfaces, if_name, argv[optind + i], debug); if (!interfaces.iface[i]) { wpa_printf(MSG_ERROR, "Failed to initialize interface"); goto out; } if (start_ifaces_in_sync) interfaces.iface[i]->need_to_start_in_sync = 1; } /* Allocate and parse configuration for per-BSS files */ for (i = 0; i < num_bss_configs; i++) { struct hostapd_iface *iface; char *fname; wpa_printf(MSG_INFO, "BSS config: %s", bss_config[i]); fname = os_strchr(bss_config[i], ':'); if (fname == NULL) { wpa_printf(MSG_ERROR, "Invalid BSS config identifier '%s'", bss_config[i]); goto out; } *fname++ = '\0'; iface = hostapd_interface_init_bss(&interfaces, bss_config[i], fname, debug); if (iface == NULL) goto out; for (j = 0; j < interfaces.count; j++) { if (interfaces.iface[j] == iface) break; } if (j == interfaces.count) { struct hostapd_iface **tmp; tmp = os_realloc_array(interfaces.iface, interfaces.count + 1, sizeof(struct hostapd_iface *)); if (tmp == NULL) { hostapd_interface_deinit_free(iface); goto out; } interfaces.iface = tmp; interfaces.iface[interfaces.count++] = iface; } } /* * Enable configured interfaces. Depending on channel configuration, * this may complete full initialization before returning or use a * callback mechanism to complete setup in case of operations like HT * co-ex scans, ACS, or DFS are needed to determine channel parameters. * In such case, the interface will be enabled from eloop context within * hostapd_global_run(). */ interfaces.terminate_on_error = interfaces.count; for (i = 0; i < interfaces.count; i++) { //根据配置文件设置iface信息 if (hostapd_driver_init(interfaces.iface[i]) || hostapd_setup_interface(interfaces.iface[i])) //将配置文件通过写入驱动 goto out; } hostapd_global_ctrl_iface_init(&interfaces); if (hostapd_global_run(&interfaces, daemonize, pid_file)) { wpa_printf(MSG_ERROR, "Failed to start eloop"); goto out; } ret = 0; out: hostapd_global_ctrl_iface_deinit(&interfaces); /* Deinitialize all interfaces */ for (i = 0; i < interfaces.count; i++) { if (!interfaces.iface[i]) continue; interfaces.iface[i]->driver_ap_teardown = !!(interfaces.iface[i]->drv_flags & WPA_DRIVER_FLAGS_AP_TEARDOWN_SUPPORT); hostapd_interface_deinit_free(interfaces.iface[i]); } os_free(interfaces.iface); if (interfaces.eloop_initialized) eloop_cancel_timeout(hostapd_periodic, &interfaces, NULL); hostapd_global_deinit(pid_file, interfaces.eloop_initialized); os_free(pid_file); if (log_file) wpa_debug_close_file(); wpa_debug_close_linux_tracing(); os_free(bss_config); for (i = 0; i < if_names_size; i++) os_free(if_names[i]); os_free(if_names); fst_global_deinit(); os_program_deinit(); return ret;}
该函数主要分为三部分:
第一部是读取命令行参数作相应的处理。
第二部分主要是根据配置文件设置hapd_interface
的参数然后通过一系列的函数调用进入内核态设置相应的内核参数
第三部分是在函数hostapd_global_run
中死循环来检测socket或者timeout或者event的相关量是否发生变化进而调用相应的提前注册到该事件上的函数。
在函数hostapd_global_init
中初始化eloop这个全局变量并进入eloop死循环中:
static int hostapd_global_init(struct hapd_interfaces *interfaces, const char *entropy_file){ int i; os_memset(&global, 0, sizeof(global)); //重置global变量 hostapd_logger_register_cb(hostapd_logger_cb); if (eap_server_register_methods()) { //注册eap server的加密方法 wpa_printf(MSG_ERROR, "Failed to register EAP methods"); return -1; } if (eloop_init()) { wpa_printf(MSG_ERROR, "Failed to initialize event loop"); return -1; } interfaces->eloop_initialized = 1; random_init(entropy_file);#ifndef CONFIG_NATIVE_WINDOWS eloop_register_signal(SIGHUP, handle_reload, interfaces); eloop_register_signal(SIGUSR1, handle_dump_state, interfaces);#endif /* CONFIG_NATIVE_WINDOWS */ eloop_register_signal_terminate(handle_term, interfaces);#ifndef CONFIG_NATIVE_WINDOWS openlog("hostapd", 0, LOG_DAEMON);#endif /* CONFIG_NATIVE_WINDOWS */ for (i = 0; wpa_drivers[i]; i++) global.drv_count++; if (global.drv_count == 0) { wpa_printf(MSG_ERROR, "No drivers enabled"); return -1; } global.drv_priv = os_calloc(global.drv_count, sizeof(void *)); if (global.drv_priv == NULL) return -1; return 0;}
使用eap_server_register_methods
函数注册eap server支持的安全模式,并存放在一个链表里面。
调用 eloop_init
函数初始化全局变量eloop结构体。
调用 random_init
对各个事件注册,
void random_init(const char *entropy_file){ os_free(random_entropy_file); if (entropy_file) random_entropy_file = os_strdup(entropy_file); else random_entropy_file = NULL; random_read_entropy();#ifdef __linux__ if (random_fd >= 0) return; random_fd = open("/dev/random", O_RDONLY | O_NONBLOCK); if (random_fd < 0) { wpa_printf(MSG_ERROR, "random: Cannot open /dev/random: %s", strerror(errno)); return; } wpa_printf(MSG_DEBUG, "random: Trying to read entropy from " "/dev/random"); eloop_register_read_sock(random_fd, random_read_fd, NULL, NULL);#endif /* __linux__ */ random_write_entropy();}
eloop_register_read_sock
函数继续调用,eloop_register_read_sock
,再使用eloop_register_sock
函数来实现注册socket。
int eloop_register_sock(int sock, eloop_event_type type, eloop_sock_handler handler, void *eloop_data, void *user_data){ struct eloop_sock_table *table; assert(sock >= 0); table = eloop_get_sock_table(type); return eloop_sock_table_add_sock(table, sock, handler, eloop_data, user_data);}
从代码中可以看到该函数将相应的handler和data放进sock_table表中,实现注册。
回到main函数中,hostapd_interface_init
读取hostapd配置文件并进行分配和解析:
/** * hostapd_interface_init - Read configuration file and init BSS data * * This function is used to parse configuration file for a full interface (one * or more BSSes sharing the same radio) and allocate memory for the BSS * interfaces. No actiual driver operations are started. */static struct hostapd_iface *hostapd_interface_init(struct hapd_interfaces *interfaces, const char *if_name, const char *config_fname, int debug){ struct hostapd_iface *iface; int k; wpa_printf(MSG_ERROR, "Configuration file: %s", config_fname); iface = hostapd_init(interfaces, config_fname); if (!iface) return NULL; if (if_name) { os_strlcpy(iface->conf->bss[0]->iface, if_name, sizeof(iface->conf->bss[0]->iface)); } iface->interfaces = interfaces; for (k = 0; k < debug; k++) { if (iface->bss[0]->conf->logger_stdout_level > 0) iface->bss[0]->conf->logger_stdout_level--; } if (iface->conf->bss[0]->iface[0] == '\0' && !hostapd_drv_none(iface->bss[0])) { wpa_printf(MSG_ERROR, "Interface name not specified in %s, nor by '-i' parameter", config_fname); hostapd_interface_deinit_free(iface); return NULL; } return iface;}
其中调用hostapd_init()
初始化配置:
/** * hostapd_init - Allocate and initialize per-interface data * @config_file: Path to the configuration file * Returns: Pointer to the allocated interface data or %NULL on failure * * This function is used to allocate main data structures for per-interface * data. The allocated data buffer will be freed by calling * hostapd_cleanup_iface(). */struct hostapd_iface * hostapd_init(struct hapd_interfaces *interfaces, const char *config_file){ struct hostapd_iface *hapd_iface = NULL; struct hostapd_config *conf = NULL; struct hostapd_data *hapd; size_t i; hapd_iface = hostapd_alloc_iface(); if (hapd_iface == NULL) goto fail; hapd_iface->config_fname = os_strdup(config_file); if (hapd_iface->config_fname == NULL) goto fail; conf = interfaces->config_read_cb(hapd_iface->config_fname);//读取配置文件 if (conf == NULL) goto fail; hapd_iface->conf = conf; hapd_iface->num_bss = conf->num_bss; hapd_iface->bss = os_calloc(conf->num_bss, sizeof(struct hostapd_data *)); if (hapd_iface->bss == NULL) goto fail;//根据配置文件中bss的配置个数conf->num_bss的值通过调用hostapd_alloc_bss_data分配空间及相关设置 for (i = 0; i < conf->num_bss; i++) { hapd = hapd_iface->bss[i] = hostapd_alloc_bss_data(hapd_iface, conf, conf->bss[i]); if (hapd == NULL) goto fail; hapd->msg_ctx = hapd; } return hapd_iface;fail: wpa_printf(MSG_ERROR, "Failed to set up interface with %s", config_file); if (conf) hostapd_config_free(conf); if (hapd_iface) { os_free(hapd_iface->config_fname); os_free(hapd_iface->bss); wpa_printf(MSG_DEBUG, "%s: free iface %p", __func__, hapd_iface); os_free(hapd_iface); } return NULL;}
通过调用函数hostapd_driver_init
获取配置信息保存在iface[i]
中,然后通过调用函数hostapd_setup_interface
函数将其配置信息写入内核。写入通过依次调用一下函数来实现:
hostapd_setup_interface------>setup_interface ------>hostapd_set_country ------>setup_interface2 ------>hostapd_setup_interface_complete ------>hostapd_set_freq ------>hostapd_set_rts ------>hostapd_set_state ------>hostapd_tx_queue_para,
通过这几个调用进入netlink层。
通过 hostapd_global_run
处理socket事件的过程如下
static int hostapd_global_run(struct hapd_interfaces *ifaces, int daemonize, const char *pid_file){#ifdef EAP_SERVER_TNC int tnc = 0; size_t i, k; for (i = 0; !tnc && i < ifaces->count; i++) { for (k = 0; k < ifaces->iface[i]->num_bss; k++) { if (ifaces->iface[i]->bss[0]->conf->tnc) { tnc++; break; } } } if (tnc && tncs_global_init() < 0) {//调用tncs_global_init完成tnc相关的初始化 wpa_printf(MSG_ERROR, "Failed to initialize TNCS"); return -1; }#endif /* EAP_SERVER_TNC */ if (daemonize) { if (os_daemonize(pid_file)) {//调用os_daemonize函数实现将该程序以后台进程运行 wpa_printf(MSG_ERROR, "daemon: %s", strerror(errno)); return -1; } if (eloop_sock_requeue()) { wpa_printf(MSG_ERROR, "eloop_sock_requeue: %s", strerror(errno)); return -1; } } eloop_run(); return 0;}
其中核心函数是eloop_run
:
void eloop_run(void){#ifdef CONFIG_ELOOP_POLL int num_poll_fds; int timeout_ms = 0;#endif /* CONFIG_ELOOP_POLL */#ifdef CONFIG_ELOOP_SELECT fd_set *rfds, *wfds, *efds; struct timeval _tv;#endif /* CONFIG_ELOOP_SELECT */#ifdef CONFIG_ELOOP_EPOLL int timeout_ms = -1;#endif /* CONFIG_ELOOP_EPOLL */#ifdef CONFIG_ELOOP_KQUEUE struct timespec ts;#endif /* CONFIG_ELOOP_KQUEUE */ int res; struct os_reltime tv, now;#ifdef CONFIG_ELOOP_SELECT //为三个文件描述符集申请空间 rfds = os_malloc(sizeof(*rfds)); wfds = os_malloc(sizeof(*wfds)); efds = os_malloc(sizeof(*efds)); if (rfds == NULL || wfds == NULL || efds == NULL) goto out;#endif /* CONFIG_ELOOP_SELECT */ while (!eloop.terminate && (!dl_list_empty(&eloop.timeout) || eloop.readers.count > 0 || eloop.writers.count > 0 || eloop.exceptions.count > 0)) { struct eloop_timeout *timeout; if (eloop.pending_terminate) { /* * This may happen in some corner cases where a signal * is received during a blocking operation. We need to * process the pending signals and exit if requested to * avoid hitting the SIGALRM limit if the blocking * operation took more than two seconds. */ // eloop_process_pending_signals函数对发生的信号进行处理 eloop_process_pending_signals(); if (eloop.terminate) break; } timeout = dl_list_first(&eloop.timeout, struct eloop_timeout, list); if (timeout) { //对超时时间进行设置timeout,主要是为下面调用的select函数会用到超时时间做准备 os_get_reltime(&now); if (os_reltime_before(&now, &timeout->time)) os_reltime_sub(&timeout->time, &now, &tv); else tv.sec = tv.usec = 0;#if defined(CONFIG_ELOOP_POLL) || defined(CONFIG_ELOOP_EPOLL) timeout_ms = tv.sec * 1000 + tv.usec / 1000;#endif /* defined(CONFIG_ELOOP_POLL) || defined(CONFIG_ELOOP_EPOLL) */#ifdef CONFIG_ELOOP_SELECT _tv.tv_sec = tv.sec; _tv.tv_usec = tv.usec;#endif /* CONFIG_ELOOP_SELECT */#ifdef CONFIG_ELOOP_KQUEUE ts.tv_sec = tv.sec; ts.tv_nsec = tv.usec * 1000L;#endif /* CONFIG_ELOOP_KQUEUE */ }#ifdef CONFIG_ELOOP_POLL num_poll_fds = eloop_sock_table_set_fds( &eloop.readers, &eloop.writers, &eloop.exceptions, eloop.pollfds, eloop.pollfds_map, eloop.max_pollfd_map); res = poll(eloop.pollfds, num_poll_fds, timeout ? timeout_ms : -1);#endif /* CONFIG_ELOOP_POLL */#ifdef CONFIG_ELOOP_SELECT //将申请的文件描述符集与eloop对象相结合,并调用select函数对这些文件发生异常进行监听 eloop_sock_table_set_fds(&eloop.readers, rfds); eloop_sock_table_set_fds(&eloop.writers, wfds); eloop_sock_table_set_fds(&eloop.exceptions, efds); res = select(eloop.max_sock + 1, rfds, wfds, efds, timeout ? &_tv : NULL);#endif /* CONFIG_ELOOP_SELECT */#ifdef CONFIG_ELOOP_EPOLL if (eloop.count == 0) { res = 0; } else { res = epoll_wait(eloop.epollfd, eloop.epoll_events, eloop.count, timeout_ms); }#endif /* CONFIG_ELOOP_EPOLL */#ifdef CONFIG_ELOOP_KQUEUE if (eloop.count == 0) { res = 0; } else { res = kevent(eloop.kqueuefd, NULL, 0, eloop.kqueue_events, eloop.kqueue_nevents, timeout ? &ts : NULL); }#endif /* CONFIG_ELOOP_KQUEUE */ if (res < 0 && errno != EINTR && errno != 0) { wpa_printf(MSG_ERROR, "eloop: %s: %s",#ifdef CONFIG_ELOOP_POLL "poll"#endif /* CONFIG_ELOOP_POLL */#ifdef CONFIG_ELOOP_SELECT "select"#endif /* CONFIG_ELOOP_SELECT */#ifdef CONFIG_ELOOP_EPOLL "epoll"#endif /* CONFIG_ELOOP_EPOLL */#ifdef CONFIG_ELOOP_KQUEUE "kqueue"#endif /* CONFIG_ELOOP_EKQUEUE */ , strerror(errno)); goto out; } eloop.readers.changed = 0; eloop.writers.changed = 0; eloop.exceptions.changed = 0; eloop_process_pending_signals(); /* check if some registered timeouts have occurred */ timeout = dl_list_first(&eloop.timeout, struct eloop_timeout, list); if (timeout) { os_get_reltime(&now); if (!os_reltime_before(&now, &timeout->time)) { void *eloop_data = timeout->eloop_data; void *user_data = timeout->user_data; eloop_timeout_handler handler = timeout->handler; eloop_remove_timeout(timeout); handler(eloop_data, user_data); } } if (res <= 0) continue; if (eloop.readers.changed || eloop.writers.changed || eloop.exceptions.changed) { /* * Sockets may have been closed and reopened with the * same FD in the signal or timeout handlers, so we * must skip the previous results and check again * whether any of the currently registered sockets have * events. */ continue; }#ifdef CONFIG_ELOOP_POLL eloop_sock_table_dispatch(&eloop.readers, &eloop.writers, &eloop.exceptions, eloop.pollfds_map, eloop.max_pollfd_map);#endif /* CONFIG_ELOOP_POLL */#ifdef CONFIG_ELOOP_SELECT eloop_sock_table_dispatch(&eloop.readers, rfds); eloop_sock_table_dispatch(&eloop.writers, wfds); eloop_sock_table_dispatch(&eloop.exceptions, efds);#endif /* CONFIG_ELOOP_SELECT */#ifdef CONFIG_ELOOP_EPOLL eloop_sock_table_dispatch(eloop.epoll_events, res);#endif /* CONFIG_ELOOP_EPOLL */#ifdef CONFIG_ELOOP_KQUEUE eloop_sock_table_dispatch(eloop.kqueue_events, res);#endif /* CONFIG_ELOOP_KQUEUE */ } eloop.terminate = 0;out:#ifdef CONFIG_ELOOP_SELECT os_free(rfds); os_free(wfds); os_free(efds);#endif /* CONFIG_ELOOP_SELECT */ return;}
eloop_process_pending_signals
函数对发生的信号进行处理
static void eloop_process_pending_signals(void){ int i; if (eloop.signaled == 0)//有没有信号产生,如果有,那么这个标志位将为1,说明有信号需要处理,如果为0,那么没有信号要处理,函数返回 return; eloop.signaled = 0;//将信号标示为置0,以便下次有信号产生时,置1 if (eloop.pending_terminate) { //如果不用处理后面将会产生的信号,则立即向进程发送一个SIGALARM信号,然后将这个标志置0 #ifndef CONFIG_NATIVE_WINDOWS alarm(0);#endif /* CONFIG_NATIVE_WINDOWS */ eloop.pending_terminate = 0; } for (i = 0; i < eloop.signal_count; i++) { //对信号标示进行处理 if (eloop.signals[i].signaled) { eloop.signals[i].signaled = 0; //调用处理函数对相应的信号进行处理 eloop.signals[i].handler(eloop.signals[i].sig, eloop.signals[i].user_data); } }}
eloop_run
函数的核心将要处理的socket事件添加到相应的表中,如下:
eloop_sock_table_set_fds(&eloop.readers, rfds); eloop_sock_table_set_fds(&eloop.writers, wfds); eloop_sock_table_set_fds(&eloop.exceptions, efds); res = select(eloop.max_sock + 1, rfds, wfds, efds, timeout ? &_tv : NULL);
最后执行相应的提前注册的函数:
eloop_sock_table_dispatch(&eloop.readers, rfds); eloop_sock_table_dispatch(&eloop.writers, wfds); eloop_sock_table_dispatch(&eloop.exceptions, efds);
- hostapd的分析
- hostapd的radius/eap server代码分析(2)-hostapd配置
- hostapd的radius/eap server代码分析(2)-hostapd配置
- hostapd源代码分析(二):hostapd的工作机制
- hostapd源代码分析(二):hostapd的工作机制
- hostapd wpa_supplicant madwifi详细分析(一)——hostapd是干嘛的
- hostapd wpa_supplicant madwifi详细分析(一)——hostapd是干嘛的
- hostapd wpa_supplicant madwifi详细分析(一)——hostapd是干嘛的
- hostapd--内部结构分析
- hostapd源码分析
- 无线网络--hostapd分析
- hostapd的radius/eap server代码分析(1)-main
- hostapd的radius/eap server代码分析(1)-main
- hostapd的radius/eap server代码分析(4)-在windows下启动hostapd的radius/eap server
- hostapd(AP)的配置文件
- 可用的hostapd.conf
- hostapd
- hostapd
- springMVC + Mybatis + Druid + dubbo(maven)入门姿势
- ASP.NET Web Pages – 全局页面
- MySQL数据类型
- 群控系统 微信群控系统 手机群控系统 手机群控软件
- nor flash和nand flash的区别
- hostapd的分析
- matlab 音量标准化
- 第四章 视皮层细胞感受野的特性与分类(读书笔记)
- Python 列表
- Service的使用
- 前端优化:九个技巧,提高Web性能
- PHP PDO函数库详解
- KNN算法理解
- 加速maven镜像仓库