android联网最最开始就是要拿ip地址,dns地址,掩码,网关等,虽然可以静态设置,但90%的场景都是要用到dhcp动态获取地址。android中有dhcpcd这个服务端负责从网络中获得地址,而系统会通过一个库(libnetutils)去请求和控制服务端。并写入属性共享给开发者。

android gpuimage hdr算法 android hdcp_ethernet

一、了解一下dhcp协议

        我们都知道设备为了上网,必须要有IP地址、掩码、网关地址等信息,上Internet还要有DNS服务器地址。配置静态地址又不是很友好,所以由DHCP协议自动获取配置。DHCP的前身是bootp,所以如果用wireshark抓dhcp包的话,最好用bootp过滤。DHCP分为两部分,一个服务端,一个客户端。所有IP网络配置都由DHCP服务端集中管理,并负责处理客户端的DHCP请求;而客户端则采用服务端分配的网络配置。这里要说一点,DHCP服务器提供两种分配地址的方式,一种是自动分配,客户端第一次从服务端申请到IP后就永久使用;另一种是动态分配,即服务器分配给客户端地址会携带一个续约周期,到期客户端要释放此IP地址,重新去服务器获取新地址或续约地址,动态的好处是一些不用了的设备就会回收地址,节约了IP地址。

以下是dhcp的报文结构并附上一个discoverry请求包对应着理解。

android gpuimage hdr算法 android hdcp_android dhcp_02

android gpuimage hdr算法 android hdcp_dhcp_03

DHCP协议流程

以下是dhcp客户端请求流程及对应的抓包截图

android gpuimage hdr算法 android hdcp_ethernet_04

android gpuimage hdr算法 android hdcp_netcfg_05

二、dhcpcd的启动

        在android系统中,dhcp分为两个部分,一个是dhcpcd作为与网络上DHCP服务器的客户端用来与其通讯,获得网络地址资源;另一个是android系统与dhcpcd交互的(我们暂时称为dhcp client)用来控制和同步数据的。

android gpuimage hdr算法 android hdcp_netcfg_06

        DHCP服务器与dhcpcd的交互协议之前已经简单说过了,这里主要介绍android系统中dhcp的使用,dhcpcd作为DHCP服务器的客户端又作为android系统的服务端存在于android系统,其源码位于external/dhcpcd下,根据android.mk可以看出,编译生成的一个dhcpcd执行文件,放在/system/bin下。下面我们来看看它又是如何与属性服务关联的。

    

     android属性服务是由init创建的,大家对windows的注册表这个东东有了解的话,就容易理解android的属性服务,一般,系统或应用程序会把自己的一些属性存储在注册表中,即使系统重启或应用重启,它能根据之前在注册表中设置的属性,进行相应的初始化。android提供这个功能就叫做属性服务(property service)。应用程序可通过这个属性机制查询或设置属性,也可以启动或停止一些服务。属性服务会在另一篇讲android启动的时候详细说明,这里只简单的介绍一下。属性服务由init创建,其本身是创建共享内存用于IPC共享,属性服务中的属性及服务来自default.prop,/system/build.prop,/system/default.prop,/data/local.prop和***.rc,属性服务对外是通过socket通信,客户端设置或操作属性服务中的属性或服务,也是通过soket来完成的。

         现在回归到dhcpcd的启动,首先dhcpcd是要注册到属性服务中,android系统通过属性服务控制dhcpcd的启动。注册属性服务就要看***.rc文件 例如

service dhcpcd_eth0 /system/bin/dhcpcd -ABKLG 
 
    class main 
 
    disabled 
 
    oneshot

系统(libnetutils)通过

property_set(ctrl_prop, DAEMON_NAME);//向名字为dhcpcd的属性service,发送"ctrl.start"启动命令字 来启动dhcpcd,用"ctl.stop"停止命令停止dhcpcd。dhcpcd获得的资源会通过脚本设置到属性中。

三、dhcpcd的配置

        dhcpcd的配置,一般是启动的时候跟上参数或配置文件完成的,譬如 /system/bin/dhcpcd -ABKL -f dhcpcd.conf或 /system/bin/dhcpcd -n等等,参数的定义晚上都能查到,这里就简单的过一下

dhcpcd [-bdeknpqABDEGKLTV] [-c, --script script] [-f, --config file] 
            [-h, --hostname hostname] [-i, --vendorclassid vendorclassid] 
            [-l, --leasetime seconds] [-m, --metric metric] 
            [-o, --option option] [-r, --request address] 
            [-s, --inform address[/cidr]] [-t, --timeout seconds] 
            [-u, --userclass class] [-v, --vendor code, value] 
            [-y, --reboot seconds] [-z, --allowinterfaces pattern] 
            [-C, --nohook hook] [-F, --fqdn FQDN] [-I, --clientid clientid] 
            [-O, --nooption option] [-Q, --require option] 
            [-S, --static value] [-X, --blacklist address[/cidr]] 
            [-Z, --denyinterfaces pattern] [interface] [...] 
     dhcpcd -k, --release [interface] 
     dhcpcd -x, --exit [interface]

四、dhcpcd在android中数据传递流程

1、封装到netcfg中,可以使用netcfg+参数(netcfg dhcp eth0,netcfg up eth0等)使用。

        从

system\core\netcfg下的android.mk可以看出编译生成netcfg命令,而netcfg的功能都在netcfg.c中有一个结构体数组来定义如

struct  
{ 
    const char *name; 
    int nargs; 
    void *func; 
} CMDS[] = { 
dhcp",   1, do_dhcp }, 
up",     1, ifc_up }, 
down",   1, ifc_down }, 
deldefault", 1, ifc_remove_default_route }, 
hwaddr", 2, set_hwaddr }, 
    { 0, 0, 0 }, 
};

这里调用netcfg dhcp就会相应的调用到 do_dhcp(1为参数是一个譬如eth0)->dhcp_init_ifc->open_raw_socket,拿到socket的fd调用poll等待激活或超时后填充msg(init_dhcp_discover_msg或init_dhcp_request_msg)->send_message->send_packet->sendmsg(s, &msghdr, 0)发送给DHCP服务器,而在这个循环里也接收receive_packet和解析decode_dhcp_msg,处理dhcp的回复包信息(ack,nak,offer等)。这里要指出流程dhcp_init_ifc这里就已经在libnetutils这个库中处理了,所以dhcp的所有处理包括android调用的都是在此库中完成的。

int dhcp_init_ifc(const char *ifname) 
{ 
    省略部分代码 
    s =  
open_raw_socket 
(ifname, hwaddr, if_index); 
    for (;;) { 
        r =  
poll(&pfd, 1, timeout); 
        if (r == 0) { 
            if (timeout >= TIMEOUT_MAX) { 
                printerr("timed out\n"); 
                if ( info.type == DHCPOFFER ) { 
                    printerr("no acknowledgement from DHCP server\nconfiguring %s with offered parameters\n", ifname); 
                    return dhcp_configure(ifname, &info); 
                } 
                errno = ETIME; 
                close(s); 
                return -1; 
            } 
            timeout = timeout * 2; 
        transmit: 
            size = 0; 
            msg = NULL; 
            switch(state) { 
            case STATE_SELECTING: 
                msg = &discover_msg; 
                size =  
init_dhcp_discover_msg(msg, hwaddr, xid); 
                break; 
            case STATE_REQUESTING: 
                msg = &request_msg; 
                size = 
 init_dhcp_request_msg(msg, hwaddr, xid, info.ipaddr, info.serveraddr); 
                break; 
            default: 
                r = 0; 
            } 
            if (size != 0) { 
                r =  
send_message(s, if_index, msg, size); 
                if (r < 0) { 
                    printerr("error sending dhcp msg: %s\n", strerror(errno)); 
                } 
            } 
            continue; 
        } 
        if (r < 0) { 
            if ((errno == EAGAIN) || (errno == EINTR)) { 
                continue; 
            } 
            return fatal("poll failed"); 
        } 
        errno = 0; 
        r =  
receive_packet(s, &reply); 
        if (r < 0) { 
            if (errno != 0) { 
                ALOGD("receive_packet failed (%d): %s", r, strerror(errno)); 
                if (errno == ENETDOWN || errno == ENXIO) { 
                    return -1; 
                } 
            } 
            continue; 
        } 
        
 decode_dhcp_msg(&reply, r, &info); 
        if (state == STATE_SELECTING) { 
            valid_reply = is_valid_reply(&discover_msg, &reply, r); 
        } else { 
            valid_reply = is_valid_reply(&request_msg, &reply, r); 
        } 
        switch(state) { 
        case STATE_SELECTING: 
info.type == DHCPOFFER) { 
                state = STATE_REQUESTING; 
                timeout = TIMEOUT_INITIAL; 
                xid++; 
                goto transmit; 
            } 
            break; 
        case STATE_REQUESTING: 
info.type == DHCPACK) { 
                printerr("configuring %s\n", ifname); 
                close(s); 
                return dhcp_configure(ifname, &info); 
info.type == DHCPNAK) { 
                printerr("configuration request denied\n"); 
                close(s); 
                return -1; 
            } else { 
                printerr("ignoring %s message in state %d\n", 
                         dhcp_type_to_name(info.type), state); 
            } 
            break; 
        } 
    } 
    close(s); 
    return 0; 
}

2、封装为libnetutils库中,提供给jni(android_net_netutils)调用。

        android中提供给java层的网络调用接口主要就是NetworkUtils类,其中封装了 android_net_netutils的native方法,而jni的方法又是调用libnetutils提供的方法,而libnetutils才是android主要维护dhcp服务的交互。举一个启动dhcpcd的例子来说明一下

    NetworkUtils.runDhcp(mIface, dhcpResults) 直接调用JNI的native函数 android_net_utils_runDhcp -> android_net_utils_runDhcpCommon-> ::dhcp_do_request(nameStr, ipaddr, gateway, &prefixLength, dns, server, &lease, vendorInfo, domains, mtu)这里获取到ip信息利用 jmethodID回调java层函数设置这些信息到java层的类中。

int dhcp_do_request(const char *interface, 
char *ipaddr, 
char *gateway, 
uint32_t *prefixLength, 
char *dns[], 
char *server, 
uint32_t *lease, 
char *vendorInfo, 
char *domain, 
char *mtu) 
 
{

   忽略一些代码

 

const char *ctrl_prop = "ctl.start"; 
 
    const char *desired_status = "running"; 
 
     
snprintf(result_prop_name, sizeof(result_prop_name), "%s.%s.result", 
 
            DHCP_PROP_NAME_PREFIX, 
 
            p2p_interface);  
 
            DHCP_PROP_NAME_PREFIX, 
 
            p2p_interface);   
拼接字符串result_prop_name=‘dhcp.eth0.result’ 
 
     
snprintf(daemon_prop_name, sizeof(daemon_prop_name), "%s_%s", 
 
            DAEMON_PROP_NAME, 
 
            p2p_interface);   
拼接字符串daemon_prop_name=‘init.svc.dhcpcd_eth0’ 
 
    property_set(result_prop_name, ""); 
 
    if (property_get(HOSTNAME_PROP_NAME, prop_value, NULL) && (prop_value[0] != '\0')) 
 
         
snprintf(daemon_cmd, sizeof(daemon_cmd), "%s_%s:-f %s -h %s %s", DAEMON_NAME, 
 
                 p2p_interface, DHCP_CONFIG_PATH, prop_value, interface); 
 
    else 
 
         
snprintf(daemon_cmd, sizeof(daemon_cmd), "%s_%s:-f %s %s", DAEMON_NAME, 
 
                 p2p_interface, DHCP_CONFIG_PATH, interface); 
 
拼接字符串 
daemon_cmd=‘dhcpcd_eth0:-f /system/etc/dhcpcd/dhcpcd.conf eth0’ 
 
    memset(prop_value, '\0', PROPERTY_VALUE_MAX); 
 
     
property_set(ctrl_prop, daemon_cmd); 
 
    if ( 
wait_for_property(daemon_prop_name, desired_status, 10) 
 < 0) { 
 
        snprintf(errmsg, sizeof(errmsg), "%s", "Timed out waiting for dhcpcd to start"); 
 
        return -1; 
 
    } 
 
    if ( 
wait_for_property(result_prop_name, NULL, 60)  
< 0) { 
 
        snprintf(errmsg, sizeof(errmsg), "%s", "Timed out waiting for DHCP to finish"); 
 
        return -1; 
 
    } 
 
    if (!property_get(result_prop_name, prop_value, NULL)) { 
 
        /* shouldn't ever happen, given the success of wait_for_property() */ 
 
        snprintf(errmsg, sizeof(errmsg), "%s", "DHCP result property was not set"); 
 
        return -1; 
 
    } 
 
    if (strcmp(prop_value, "ok") == 0) { 
 
        char dns_prop_name[PROPERTY_KEY_MAX]; 
 
        if (fill_ip_info(interface, ipaddr, gateway, prefixLength, dns, 
 
                server, lease, vendorInfo, domain, mtu) == -1) { 
            return -1;
        }
        return 0;
    } else {
        snprintf(errmsg, sizeof(errmsg), "DHCP result was %s", prop_value);
        return -1;
    }
}

这里说明上述几个拼接字符串的作用, daemon_prop_name是用来同步获取dhcpcd当前启动状态的,result_prop_name是通知dhcpcd获取到非NULL结果的属性,启动dhcpcd则使用了启动属性服务的流程property_set(ctrl_prop, daemon_cmd);以后的章节中会详细介绍属性服务的内容,这里只需知道启动属性服务只需传ctl.start+属性服务名称:参数即可(这里为ctl.start+dhcpcd_eth0:-f /system/etc/dhcpcd/dhcpcd.conf eth0),而停止一个属性服务为ctl.stop+属性服务名称,重启一个属性服务ctl.restart+属性服务名称。

    reason状态的)->send_raw_packet->sendto发送DHCP_DISCOVER给DHCP服务器。而其中穿插着以reason值执行脚本,从而更新系统dhcp属性,脚本如下

if [[ $interface == p2p* ]]    then    intf=p2p    else    intf=$interfacefisetprop dhcp.${intf}.reason "${reason}"case "${reason}" inBOUND|INFORM|REBIND|REBOOT|RENEW|TIMEOUT)    setprop dhcp.${intf}.ipaddress  "${new_ip_address}"    setprop dhcp.${intf}.gateway    "${new_routers%% *}"    setprop dhcp.${intf}.mask       "${new_subnet_mask}"    setprop dhcp.${intf}.leasetime  "${new_dhcp_lease_time}"    setprop dhcp.${intf}.server     "${new_dhcp_server_identifier}"    setprop dhcp.${intf}.vendorInfo "${new_vendor_encapsulated_options}"    setprop dhcp.${intf}.mtu        "${new_interface_mtu}"    setprop dhcp.${intf}.result "ok"    ;;EXPIRE|FAIL|IPV4LL|STOP|NOCARRIER)    setprop dhcp.${intf}.result "failed"    ;;RELEASE)    setprop dhcp.${intf}.result "released"    ;;esac

dhcpcd各个状态的跳转,这里就不赘述了,因为这篇是介绍android的调用,并不是一片介绍dhcpcd流程的文章,如果以后有机会,也会有专门一篇介绍dhcpcd流程的,根据reason,大概有这几种状态STATIC,IPV4LL,INFORM,TIMEOUT,TEST,RENEW,REBIND,REBOOT,BOUND。

        如有那里不对欢迎留言指点,共同探讨,谢谢。