《深入理解Android:Wi-Fi,NFC和GPS》章节连载[节选]--第二章 深入理解Netd

时间:2021-12-17 04:05:35

首先感谢各位兄弟姐妹们的耐心等待。本书预计在3月中旬上市发售。从今天开始,我将在博客中连载此书的一些内容。注意,此处连载的是未经出版社编辑的原始稿件,所以样子会有些非专业。

注意,如下是本章目录,本文节选2.1-2.3以及2.5节

 

 

为了方便读者深入学习,本系列连载都会将作者研究过

 

 

程中所学习的参考文献列出来

 

 

 

第2章 深入理解Netd

 《深入理解Android:Wi-Fi,NFC和GPS》章节连载[节选]--第二章 深入理解Netd

《深入理解Android:Wi-Fi,NFC和GPS》章节连载[节选]--第二章 深入理解Netd

 

本章主要内容

  • 介绍Netd;
  • 介绍MDNSApple Bonjour技术;
  • 介绍iptablestcipLinux系统中常用的网络管理工具;
  • 介绍Netd中的各个命令对象和相关的背景知识;
  • 介绍NetworkManagmentService。

2.1  概述 

NetdAndroid系统中专门负责网络管理和控制的后台daemon程序,其功能主要分三大块:

  • 设置防火墙(Firewall)、网络地址转换(NAT)、带宽控制、无线网卡软接入点(Soft Access Point)控制,网络设备绑定(Tether)等。
  • Android系统中DNS信息的缓存和管理。
  • 网络服务搜索(Net Service Discovery,简称NSD)功能,包括服务注册(Service Registration)、服务搜索(Service Browse)和服务名解析(Service Resolve)等。
  • Netd的工作流程和Vold类似[1],其工作可分成两部分:
  • Netd接收并处理来自Framework层中NetworkManagementServiceNsdService的命令。这些命令最终由Netd中对应的Command对象去处理。
  • Net接收并解析来自KernelUEvent消息,然后再转发给Framework层中对应Service去处理。

由上述内容可知,Netd位于Framework层和Kernel层之间,它是Android系统中网络相关消息和命令转发及处理的中枢模块。

Netd的代码量不大,难度较低,但其所涉及的相关背景知识却比较多。本章对Netd的分析将从以下几个方面入手:

  • 首先介绍Netd的大体工作流程以及DNSMDns相关的背景知识。关于Netd的工作流程分析,读者也可参考中的内容。
  • 然后本章将集中介绍Netd中涉及到的Android系统中网络管理和控制的相关工具。它们是iptablestcip
  • 最后将介绍NetdCommandListener的命令处理。这些命令的正常工作依赖于上面介绍的iptables等工具。

最后,我们将介绍Java Framework中的NetworkManagementService服务。

提示:NsdService比较简单,感兴趣的读者不妨阅读作者的一篇博文”Android Says Bonjour”中的第2.2NsdService介绍”一节。地址位于http://blog.csdn.net/innost/article/details/8629139

2.2 Netd工作流程分析

Netd进程由init进程根据init.rc的对应配置项[1]而启动,其配置项如图2-1所示。

《深入理解Android:Wi-Fi,NFC和GPS》章节连载[节选]--第二章 深入理解Netd

2-1  Netd启动配置参数

由图2-1可知:

  • Netd启动时将创建三个TCP监听socket,其名称分别为"netd""dnsproxyd""mdns"

根据本章后续分析,读者将会看到:

  • Framework层中的NetworkManagementServiceNsdService将分别和"netd""mdns"监听socket建立链接并交互。
  • 每一个调用和域名解析相关的socket API(如getaddrinfogethostbyname等)的进程都会借由"dnsproxyd"监听socketnetd建立链接。

下面开始分析Netd进程。

2.2.1  main函数分析

Netd进程的入口函数是其main函数,代码如下所示:

[-->main.cpp]

int main() {

 

    CommandListener *cl;

    NetlinkManager *nm;

    DnsProxyListener *dpl;

    MDnsSdListener *mdnsl;

 

    ALOGI("Netd 1.0 starting");

 

    //为Netd进程屏蔽SIGPIPE信号

    blockSigpipe();

 

   //①创建NetlinkManager

    nm = NetlinkManager::Instance();

    //②创建CommandListener,它将创建名为"netd"的监听socket

    cl = new CommandListener();

    //设置NetlinkManager的消息发送者(Broadcaster)为CommandListener。

    nm->setBroadcaster((SocketListener *) cl);

   //启动NetlinkManager

    nm->start();

   ......

    //注意下面这行代码,它为本Netd设置环境变量ANDROID_DNS_MODE为"local",其作用将在2.2.4节介绍

    setenv("ANDROID_DNS_MODE", "local", 1);

    //③创建DnsProxyListener,它将创建名为"dnsproxyd"的监听socket

    dpl = new DnsProxyListener();

    dpl->startListener();

 

   //④创建MDnsSdListener并启动监听,它将创建名为"mdns"的监听socket

    mdnsl = new MDnsSdListener();

    mdnsl->startListener();

 

   cl->startListener();

 

    while(1) {

        sleep(1000);

    }

    exit(0);

}

Netdmain函数非常简单,主要是创建几个重要成员并启动相应的工作,这几个重要成员分别是:

  • NetlinkManager:它将接收并处理来自KernelUEvent消息。这些消息经NetlinkManager解析后将借助它的Broadcaster(也就是代码中为NetlinkManager设置的CommandListener)发送给Framework层的NetworkManagementService
  • CommandListenerDnsProxyListenerMDnsSdListener:分别创建名为"netd""dnsproxyd""mdns"的监听socket,并处理来客户端的命令。

下面将分别讨论这四位成员的作用。

2.2.2  NetlinkManager分析

NetlinkManager(以后简称NM)主要负责接收并解析来自KernelUEvent消息。其核心代码在start函数中,如下所示。

 [-->NetlinkManager.cpp::start]

int NetlinkManager::start() {

  //创建接收NETLINK_KOBJECT_UEVENT消息的socket,其值保存在mUeventSock中

  //其中,NETLINK_FORMAT_ASCII代表UEvent消息的内容为ASCII字符串

  mUeventHandler = setupSocket(&mUeventSock, NETLINK_KOBJECT_UEVENT,

         0xffffffff, NetlinkListener::NETLINK_FORMAT_ASCII);

  //创建接收RTMGPR_LINK消息的socket,其值保存在mRouteSock中

  //其中,NETLINK_FORMAT_BINARY代表UEvent消息的类型为结构体,故需要进行二进制解析

  mRouteHandler = setupSocket(&mRouteSock, NETLINK_ROUTE, RTMGRP_LINK,

         NetlinkListener::NETLINK_FORMAT_BINARY);

  //创建接收NETLINK_NFLOG消息的socket,其值保存在mQuotaSock中

  mQuotaHandler = setupSocket(&mQuotaSock, NETLINK_NFLOG,

        NFLOG_QUOTA_GROUP, NetlinkListener::NETLINK_FORMAT_BINARY);

 return 0;

}

NMstart函数主要是向Kernel注册了三个用于接收UEvent事件的socket,这三个UEvent[1][2]分别对应于:

  • NETLINK_KOBJECT_UEVENT:代表kobject事件,由于这些事件包含的信息由ASCII字符串表达,故上述代码中使用了NETLINK_FOMRAT_ASCII。它表示将采用字符串解析的方法去解析接收到的UEvent消息。kobject一般用来通知内核中某个模块的加载或卸载。对NM来说,其关注的是/sys/class/net下相应模块的加载或卸载消息。
  • NETLINK_ROUTE:代表kernelroutinglink改变时对应的消息。NETLINK_ROUTE包含很多子项,上述代码中使用了RTMGRP_LINK项。二者结合起来使用,表示NM希望收到网络链路断开或接通时对应的UEvent消息(笔者在Ubuntu PC机上测试过,当网卡上拔掉或插入网线时,会触发这些UEvent消息的发送)。由于对应UEvent消息内部封装了nlmsghdr等相关结构体,故上述代码使用了NETLINK_FORMAT_BINARY来指示解析UEvent消息时将使用二进制的解析方法。
  • NETLINK_NFLOG:和2.3.6节介绍的带宽控制有关。Netd中的带宽控制可以设置一个预警值,当网络数据超过一定字节数就会触发kernel发送一个警告。该功能属于iptables的扩展项,但由于iptables的文档更新速度较慢(这也是很多开源项目的一大弊端),笔者一直未能找到相关的正式说明。值得指出的是,上述代码中有关NETLINK_NFLOG相关socket的设置并非所有kernel版本都支持。同时,NFLOG_QUOTA_GROUP的值是直接定义在NetlinkManager.cpp中的,而非和其他类似系统定义一样定义在系统头文件中。这也表明NFLOG_QUOTA_GROUP的功能比较新。

提示:读者可通过在Linux终端中执行man PF_LINK得到有关NETLINK的详细说明。

上述start函数将调用setupSocket创建用于接收UEvent消息的socket以及一个解析对象NetlinkHandlersetupSocket代码本身比较简单,此处就不拟展开分析。

下面来看NM及其家族成员,它们之间的关系如图2-2所示。

《深入理解Android:Wi-Fi,NFC和GPS》章节连载[节选]--第二章 深入理解Netd

2-2  NetlinkManager家族成员的类图

由图2-2可知:

  • NetlinkHandlerCommandListener均间接从SocketListener派生。其中,NetlinkHandler收到的socket消息将通过onEvent回调处理。
  • 结合前文所述,NetlinkManager分别注册了三个用于接收UEventsocket,其对应的NetlinkHandler分别是mUeventHandlermRouteHandlermQuotaHandler
  • NetlinkHandler接收到的UEvent消息会转换成一个NetlinkEvent对象。NetlinkEvent对象封装了对UEvent消息的解析方法。对于NETLINK_FOMRAT_ASCII类型,其parseAsciiNetlinkMessage函数会被调用,而对于NETLINK_FORMAT_BINARY类型,其parseBinaryNetlinkMessage函数会被调用。
  • NM处理流程的输入为一个解析后的NetlinkEvent对象。NM完成相应工作后,其处理结果将经由mBroadcaster对象传递给Framework层的接收者,也就是NetworkManagementService
  • CommandListenerFrameworkListener派生,而FrameworkListener内部有一个mCommands数组,它用来存储注册到FrameworkListener中的命令处理对象。

下面来简单了解下NetlinkHandleronEvent函数,由于其内部已针对不同属性的NetlinkEvent进行了分类处理,故浏览这段代码能加深对前文所述不同UEvent消息的作用的理解。

[-->NetlinkHandler.cpp::onEvent]

void NetlinkHandler::onEvent(NetlinkEvent *evt) {

    const char *subsys = evt->getSubsystem();

    ......

    //处理对应NETLINK_KOBJECT_UEVENT和NETLINK_ROUTE的信息

    if (!strcmp(subsys, "net")) {

        int action = evt->getAction();

        const char *iface = evt->findParam("INTERFACE");//查找消息中携带的网络设备名

        if (action == evt->NlActionAdd) {

            notifyInterfaceAdded(iface);//添加NIC(Network Interface Card)的消息

        } else if (action == evt->NlActionRemove) {

            notifyInterfaceRemoved(iface);//NIC被移除的消息

        } else if (action == evt->NlActionChange) {

            evt->dump();

            notifyInterfaceChanged("nana", true);//NIC变化消息

        } else if (action == evt->NlActionLinkUp) {//下面两个消息来自NETLINK_ROUTE

            notifyInterfaceLinkChanged(iface, true);//链路启用(类似插网线)

        } else if (action == evt->NlActionLinkDown) {

            notifyInterfaceLinkChanged(iface, false);//链路断开(类似拔网线)

        }

    } else if (!strcmp(subsys, "qlog")) {//对应NETLINK_NFLOG

        const char *alertName = evt->findParam("ALERT_NAME");

        const char *iface = evt->findParam("INTERFACE");

        notifyQuotaLimitReached(alertName, iface);//当数据量超过预警值,则会收到该通知

    } else if (!strcmp(subsys, "xt_idletimer")) {

        //这个和后文的idletimer有关,用于跟踪某个NIC的工作状态,即是“idle”还是“active”

       //检测时间按秒计算

        int action = evt->getAction();

        const char *label = evt->findParam("LABEL");

        const char *state = evt->findParam("STATE");

        if (label == NULL) {

            label = evt->findParam("INTERFACE");

        }

        if (state)

            notifyInterfaceClassActivity(label, !strcmp("active", state));

    }

   ......

}

由上边代码可知:

  • NETLINK_KOBJECT_UEVENTNETLINK_ROUTE主要反映网络设备的事件和状态,包括NIC的添加、删除和修改,以及链路的连接状态等。
  • NETLINK_NFLOG用于反映设置的log是否超过配额。
  • 另外,上边代码中还处理了“xt_idletimer”的uevent消息,它和后文要介绍的IdleTimerCmd有关,主要用来监视网络设备的收发工作状态。当对应设备工作或空闲时间超过设置的监控时间后,Kernel将会发送携带其状态("idle""active")的UEvent消息。

2-3所示为NetlinkHandler的工作流程。

《深入理解Android:Wi-Fi,NFC和GPS》章节连载[节选]--第二章 深入理解Netd

2-3  NM工作流程图

由图2-3可知:

  • NM创建NetlinkHandler后,工作便转交给NetlinkHandler来完成,而每个NetlinkHandler对象均会单独创建一个线程用于接收Socket消息。
  • Kernel发送UEvent消息后,NetlinkHandler便从select调用中返回,然后调用其onDataAvailable函数,该函数内部会创建一个NetlinkEvent对象。
  • NetlinkEvent对象根据socket创建时指定的解析类型去解析来自KernelUEvent消息。
  • 最终NetlinkHandleronEvent将被调用,不同的UEvent消息将在此函数中进行分类处理。
  • NetlinkHandler最终将处理结果经由NM内部变量mBroadcaster转发给NetworkManagementService
提醒:请读者结合上文所述流程自行研读相关代码。


[1]关于init工作原理以及init.rc的分析方法,读者可参考《深入理解Android:卷1》第3章关于init进程的分析。



[1]读者可参考《深入理解Android:卷1》第9章关于Vold的分析。


2.2.3  CommandListener分析

Netd中第二个重要成员是CommandListener(以后简称CL),其主要作用是接收来自FrameworkNetworkManageService的命令。从角色来看,CL仅是一个Listener。它在收到命令后,只是将它们转交给对应的命令处理对象去处理。CL内部定义了许多命令,而这些命令都有较深的背景知识。本节拟以分析CL的工作流程为主,而相关的命令处理则放到后文再集中分析。

CL中的图2-4所示为CL中的Command对象及对应的Controller对象。

《深入理解Android:Wi-Fi,NFC和GPS》章节连载[节选]--第二章 深入理解Netd

2-4  CL中的命令及控制类

由图2-4可知:

  • CL定义了11个和网络相关的Command类。这些类均从NetdCommand派生(注意,为保持绘图简洁,这11Command的派生关系由1个派生箭头表达)。
  • CL还定义了10个控制类,这些控制类将和命令类共同完成相应的命令处理工作。

结合前面图2-2中对NM家族成员的介绍,CL创建时,需要注册自己支持的命令类。这部分代码在其构造函数中实现,代码如下所示:

[-->CommandListener::CommandListener构造函数]

CommandListener::CommandListener() :

                 FrameworkListener("netd", true) {

    registerCmd(new InterfaceCmd());//注册11个命令类对象

    registerCmd(new IpFwdCmd());

    registerCmd(new TetherCmd());

    registerCmd(new NatCmd());

    registerCmd(new ListTtysCmd());

    registerCmd(new PppdCmd());

    registerCmd(new SoftapCmd());

    registerCmd(new BandwidthControlCmd());

    registerCmd(new IdletimerControlCmd());

    registerCmd(new ResolverCmd());

    registerCmd(new FirewallCmd());

    //创建对应的控制类对象

    if (!sSecondaryTableCtrl)

        sSecondaryTableCtrl = new SecondaryTableController();

    if (!sTetherCtrl)

        sTetherCtrl = new TetherController();

    if (!sNatCtrl)

        sNatCtrl = new NatController(sSecondaryTableCtrl);

    if (!sPppCtrl)

        sPppCtrl = new PppController();

    if (!sSoftapCtrl)

        sSoftapCtrl = new SoftapController();

    if (!sBandwidthCtrl)

        sBandwidthCtrl = new BandwidthController();

    if (!sIdletimerCtrl)

        sIdletimerCtrl = new IdletimerController();

    if (!sResolverCtrl)

        sResolverCtrl = new ResolverController();

    if (!sFirewallCtrl)

        sFirewallCtrl = new FirewallController();

    if (!sInterfaceCtrl)

        sInterfaceCtrl = new InterfaceController();

    //其他重要工作,后文再分析

}

由于CL的间接基类也是SocketListener,所以其工作流程和NetlinkHandler类似。

为了方便读者理解,图2-5给出了CL的工作流程图:

《深入理解Android:Wi-Fi,NFC和GPS》章节连载[节选]--第二章 深入理解Netd

2-5  CL的工作流程示意图

图2-5中,假设Client端发送的命令名是"nat",当CL收到这个命令后,首先会从其构造函数中注册的那些命令对象中找到对应该名字(即"nat")的命令对象,其结果就是图中的NatCmd对象。而该命令最终的处理工作将由此NatCmd对象的runCommand函数完成。

2.2.4 DnsProxyListener分析

DnsProxyListenerAndroid系统中的DNS管理有关。什么是DNS呢?Android系统中DNS又有什么特点呢?来看下文。

1. Android DNS介绍[3]

DNS是Domain Name System(域名系统)的缩写。其主要目的是在域名和IP地址之间建立一种映射。简单点说,DNS的功能类似于电话簿,它可将人名映射到相应的电话号码。在DNS中,人名就是域名,电话号码就是IP地址。域名系统的管理由DNS服务器来完成。全球范围内的DNS服务器共同构成了一个分布式的域名-IP数据库。

对使用域名来发起网络操作的网络程序来说,其域名解析工作主要分两步:

1)第一步工作就是需要将域名转换成IP。由于域名和IP的转换关系存储在DNS服务器上,所以该网络程序要向DNS服务器发起请求,以获取域名对应的IP地址。

2DNS服务器根据DNS解析规则解析并得到该域名对应的IP地址,然后返回给客户端。在DNS中,每一个域名和IP的对应关系被称之为一条记录。客户端一般会缓存这条记录以备后续之用。

提醒:DNS解析规则比较复杂,感兴趣的读者可研究DNS的相关协议。

对软件开发者来说,常用的域名解析socket API有两个:

  • getaddrinfo:它根据指定的host名或service名得到对应的IP地址(该IP地址由结构体addrinfo表达)。
  • getnameinfo:根据指定的IP地址(由结构体sockaddr表达)得到对应的hostservice的名称。

Android中,这两个函数均由Bionic C实现。其代码实现基于NetBSD的解析库(resolver library),并经过一些修改。这些修改包括:

  • 没有实现name-server-switch功能。这是为了保持Bionic C库的轻便性而做的裁剪。
  • DNS服务器的配置文件由/etc/resolv.conf变成/system/etc/resolv.conf[1]。在Android系统中,/etc目录实际上为/system/etc目录的链接。resolv.conf存储的是DNS服务器的IP地址。
  • 系统属性中保存了一些DNS服务器的地址,它们通过诸如"net.dns1""net.dns2"之类的属性来表达。这些属性由dhcpd进程或其他系统模块负责维护。
  • 每个进程还可以设置进程特定的DNS服务器地址。它们通过诸如"net.dns1.<pid>""net.dns2.<pid>"的系统属性来表达。
  • 不同的网络设备也有对应的DNS服务器地址,例如通过wlan接口发起的网络操作,其对应的DNS服务器由系统属性“net.wlan.dns1”表示。

2-6所示为三星Galaxy Note2中有关dns信息的示意图。

《深入理解Android:Wi-Fi,NFC和GPS》章节连载[节选]--第二章 深入理解Netd

2-6  net.dns设置示意图

由图2-6可知:

  • 系统中有些进程有自己特定的DNS服务器。
  • 不同网络设备也设置了对应的DNS服务器地址。

2. getaddrinfo函数分析

本节将介绍Android中getaddrinfo的实现,我们将只关注Android对其做的改动。

[-->getaddrinfo.c::getaddrinfo]

int  getaddrinfo(const char *hostname, const char *servname,

    const struct addrinfo *hints, struct addrinfo **res)

{

    ......//getaddrinfo的正常处理

   //Android平台的特殊定制

   if (android_getaddrinfo_proxy(hostname, servname, hints, res) == 0) {

        return 0;

   }

   ......//如果上述函数处理失败,则继续getaddrinfo的正常处理

  return error

}

由上述代码可知,Android平台中的getaddrinfo会调用其定制的android_getaddrinfo_proxy函数完成一些特殊操作,该函数的实现如下所示:

[-->getaddrinfo.c::android_getaddrinfo_proxy]

static int android_getaddrinfo_proxy(const char *hostname, const char *servname,

               const struct addrinfo *hints, struct addrinfo **res)

{

    .......

    //取ANDROID_DNS_MODE环境变量。只有Netd进程设置了它

    const char* cache_mode = getenv("ANDROID_DNS_MODE");

    ......

   //由于Netd进程设置了此环境变量,故Netd进程调用getaddrinfo的话,将不会采用这套定制的方法

    if (cache_mode != NULL && strcmp(cache_mode, "local") == 0) {

        return -1;

    }

   //获取本进程对应的DNS地址

    snprintf(propname, sizeof(propname), "net.dns1.%d", getpid());

    if (__system_property_get(propname, propvalue) > 0) {

        return -1;

    }

 

    //建立和Netd中DnsProxyListener的连接,将请求转发给它去执行

    sock = socket(AF_UNIX, SOCK_STREAM, 0);

    if (sock < 0) {

        return -1;

    }

   ......

    strlcpy(proxy_addr.sun_path, "/dev/socket/dnsproxyd",

          sizeof(proxy_addr.sun_path));

    ......//发送请求,处理回复等

    return -1;

}

由上述代码可知:

  • Netd进程调用getaddrinfo时,由于其设置了ANDROID_DNS_MODE环境变量,所以该函数会继续原来的流程。
  • 当非Netd进程调用getaddrinfo函数时,首先会开展android_getaddrinfo_proxy中的工作,即判断该进程是否有定制的DNS服务器,如果没有的话它将和位于Netd进程中的"dnsproxyd"监听socket建立连接,然后把请求发给DnsProxyListener去执行。

3. DnsProxyListener命令介绍

下面来介绍DnsProxyListener(以后简称DPL),图2-7所示为其家族成员示意图:

《深入理解Android:Wi-Fi,NFC和GPS》章节连载[节选]--第二章 深入理解Netd

2-7  DPL家族示意图

由图2-7可知,DPL仅定义了两个命令:

  • GetAddrInfoCmd,和Bionic C库的getaddrinfo函数对应。
  • GetHostByAddrCmd,和Bionic C库的gethostbyaddr函数对应。

这个两条命令的处理比较简单,此处就不拟展开详细的代码。

为方便读者理解,我们将给出调用序列图,如图2-8所示。

《深入理解Android:Wi-Fi,NFC和GPS》章节连载[节选]--第二章 深入理解Netd

2-8  GetAddrInfoCmd处理流程示意图

由图2-8所示,GetAddrInfoHandler最终的处理还是交由Bionic Cgetaddrinfo函数来完成。根据前文所述,由于Netd进程设置了ANDROID_DNS_MODE环境变量,故Netd调用的getaddrinfo将走正常的流程。这个正常流程就是Netd进程将向指定的DNS服务器发起请求以解析域名。

Android系统中,通过这种方式来管理DNS的好处是所有解析后得到的DNS记录都将缓存在Netd进程中,从而使这些信息成为了一个公共的资源,最大程度内做到了信息共享。

 

2.2.5 MDnsSdListener分析

 

MDnsSdMulticast DNS Service Discovery的简称,它和Apple公司的Bonjour技术有关,故本节将先介绍Apple Bonjour技术。

1. Apple Bonjour技术介绍[4][5][6]

Bonjour是法语中的Hello之意。它是Apple公司为基于组播域名服务(multicast DNS)的开放性零配置网络标准所起的名字。使用Bonjour的设备在网络中自动组播它们自己的服务信息并监听其他设备的服务信息,设备之间就像在打招呼,这也是该技术命名为Bonjour的原因。Bonjour使得局域网中的系统和服务即使在没有网络管理员的情况下也很容易被找到。

举一个简单的例子:在局域网中,如果要进行打印服务,就必须先知道打印服务器的IP地址。此IP地址一般由IT部门的人负责分配,然后他还得全员发邮件以公示此地址。有了Bonjour以后,打印服务器自己会依据零配置网络标准在局域网内部找到一个可用的IP并注册一个打印服务,名为“print service”之类的。当客户端需要打印服务时,会先搜索网络内部的打印服务器。由于不知道打印服务器的IP地址,客户端只能根据诸如"print service"的名字去查找打印机。在Bonjour的帮助下,客户端最终能找到这台注册了“print service”名字的打印机,并获得它的IP地址以及端口号。

Bonjour角度来看,该技术主要解决了三个问题:

  • Addressing:即为主机分配IPBonjourAddressing处理比较简单,即每个主机在网络内部的地址可选范围内找一个IP,然后查看下网络内部是否有其他主机再用。如果该IP没有被分配的话,它将使用此IP
  • NamingNaming解决的就是hostIP地址的对应关系。Bonjour采用的是Multiple DNS技术,即DNS查询消息将通过UDP组播方式发送。一旦网络内部某个机器发现查询的机器名和自己设置的一样,就回复这条请求。此外,Bonjour还拓展了MDNS的用途,即除了能查找host外,还支持对service的查找。不过,BonjourNaming有一个限制,即网络内部不能有重名的hostservice
  • Service DiscoverySD基于上面的Naming工作,它使得应用程序能查找到网络内部的服务,并解析该服务对应的IP地址和端口号。应用程序一旦得到服务的IP地址和端口号,就可以直接和该服务建立交互关系。

Bonjour技术在Mac OS以及ItunesIphone上都得到了广泛应用。为了进一步推广,Apple通过开源工程mdnsresponder将其开源出来。在Windows平台上,它将生成一个后台程序mdnsresponder。在Android平台上(或者说支持POSIXLinux平台)它是一个名为mdnsd的程序。不过,不论是mdnsresponder还是mdnsd,应用开发者要做的仅仅是利用BonjourAPI向它们发起服务注册、服务查询和服务解析等请求并接收来自它们的处理结果。

下面我们将介绍Bonjour API中使用最多的三个函数,它们分别是服务注册、服务查询和服务解析。理解这三个函数的功能也是理解MDnsSdListener的基础。

使用Bonjour API必须包含如下的头文件和动态库,并连接到:

#include <dns_sd.h>  //必须包含此头文件

libmdnssd.so  //链接到此so

Bonjour中,服务注册的APIDNSServiceRegister,原型如下:

DNSServiceErrorType DNSSD_API DNSServiceRegister

(

    DNSServiceRef                       *sdRef,

    DNSServiceFlags                     flags,

    uint32_t                            interfaceIndex,

    const char                          *name,         /* may be NULL */

    const char                          *regtype,

    const char                          *domain,       /* may be NULL */

    const char                          *host,         /* may be NULL */

    uint16_t                            port,          /* In network byte order */

    uint16_t                            txtLen,

    const void                          *txtRecord,    /* may be NULL */

    DNSServiceRegisterReply             callBack,      /* may be NULL */

    void                                *context       /* may be NULL */

);

该函数的解释如下:

  • sdRef:代表一个未初始化的DNSService实体。其类型DNSServiceRef是指针。该参数最终由DNSServiceRegister函数分配内存并初始化。
  • flags:表示当网络内部有重名服务时的冲突处理。默认是按顺序修改服务名。例如要注册的服务名为“printer”,当检测到重名冲突时,就可改名为“printer(1)”。
  • interfaceIndex:表示该服务输出到主机的哪些网络接口上。值-1表示仅对本机支持,也就是该服务的用在loop接口上。
  • name:表示服务名,为空的话就取机器名。
  • regtype:服务类型,用字符串表达。Bonjour要求格式为"_服务名._传输协议",例如"_ftp._tcp"。目前传输协议仅支持TCPUDP
  • domianhost一般都为空。
  • port表示该服务的端口。如果为0的话,Bonjour会自动分配一个。
  • txtLen以及txtRecord字符串用来描述该服务。一般都设置为空。
  • callBack:设置回调函数。该服务注册的请求结果都会通过它回调给客户端。
  • context:上下文指针,由应用程序设置。

当客户端需要搜索网络内部特定服务时,需要使用DNSServiceBrowser API,其原型如下:

DNSServiceErrorType DNSSD_API DNSServiceBrowse

(

    DNSServiceRef                       *sdRef,

    DNSServiceFlags                     flags,

    uint32_t                            interfaceIndex,

    const char                          *regtype,

    const char                          *domain,    /* may be NULL */

    DNSServiceBrowseReply               callBack,

    void                                *context    /* may be NULL */

);

其中:

  • sdrefinterfaceIndexregtypedomain以及context含义与DNSServiceRegister一样。
  • flags:在本函数中没有作用。
  • callBack:为DNSServiceBrowser处理结果的回调通知接口。

当客户端想获得指定服务的IP和端口号时,需要使用DNSServiceResolve API,其原型如下:

DNSServiceErrorType DNSSD_API DNSServiceResolve

(

    DNSServiceRef                       *sdRef,

    DNSServiceFlags                     flags,

    uint32_t                            interfaceIndex,

    const char                          *name,

    const char                          *regtype,

    const char                          *domain,

    DNSServiceResolveReply              callBack,

    void                                *context  /* may be NULL */

);

其中:

  • nameregtypedomain都从DNSServiceBrowse函数的处理结果中获得。
  • callBack用于通知DNSServiceResolve的处理结果。该回调函数将返回服务的IP地址和端口号。

2. MDnsSdListener分析

MDnsSdListener对应的Framework层服务为NsdServiceNsdNetwork Service Discovery的缩写),它是Android 4.1新增的一个FrameworkService。该服务的实现比较简单,故本书不拟详细讨论它。感兴趣的读者不妨首先阅读SDK中关于NsdService的相关文档。

提示:SDK中有一个基于Nsd技术开发的NsdChat例程,读者也可先学习它的实现。相关文档位置为http://developer.android.com/training/connect-devices-wirelessly/nsd.html

2-9所示为MDnsSdListener的家族成员示意图。

《深入理解Android:Wi-Fi,NFC和GPS》章节连载[节选]--第二章 深入理解Netd

2-9  MDnsSdListener家族成员

由图2-9可知:

  • MDnsSdListener的内部类Monitor用于和mdnsd后台进程通信,它将调用前面提到的Bonjour API
  • Monitor内部针对每个DNSService都会建立一个Element对象,该对象通过MonitormHead指针保存在一个list中。
  • HandlerMDnsSdListener注册的Command

下面将简单介绍MDnsSdListener的运行过程,主要工作可分成三步:

1Netd创建MDnsSdListener对象,其内部会创建Monitor对象,而Monitor对象将启动一个线程用于和mdnsd通信,并接收来自Handler的请求。

2NsdService启动完毕后将向MDnsSdListener发送"start-service"命令。

3NsdService响应应用程序的请求,向MDnsSdListener发送其他命令,例如"discovery"等。Monitor将最终处理这些请求。

先来看第一步,当MDnsSdListener构造时,会创建一个Monitor对象,代码如下所示:

[-->MDnsSdListener.cpp::Monitor:Monitor]

MDnsSdListener::Monitor::Monitor() {

    mHead = NULL;

    pthread_mutex_init(&mHeadMutex, NULL);

    //创建两个socket,用于接收MDnsSdListener对象的指令

    socketpair(AF_LOCAL, SOCK_STREAM, 0, mCtrlSocketPair);

    //创建线程,线程函数是threadStart,其内部会调用run

    pthread_create(&mThread, NULL, MDnsSdListener::Monitor::threadStart, this);

}

MonitorthreadStart线程将调用其run函数,该函数通过poll方式侦听包括mCtrlSocketPair在内的socket信息。这部分代码属于基本的Linux socket编程,本书不拟开展深入讨论。

NsdService发送"start-service"命令后,HandlerrunCommand将执行MonitorstartService函数,代码如下所示:

[-->MDnsSdListener.cpp::Monitor:startService]

int MDnsSdListener::Monitor::startService() {

    int result = 0;

    char property_value[PROPERTY_VALUE_MAX];

    pthread_mutex_lock(&mHeadMutex);

    //MDNS_SERVICE_STATUS是一个字符串,值为“init.svc.mdnsd”,在init.rc配置文件中,mdnsd是一个

   //service,而“init.svc.mdnsd”将记录mdnsd进程的运行状态。

    property_get(MDNS_SERVICE_STATUS, property_value, "");

    if (strcmp("running", property_value) != 0) {

         //如果mdnsd的状态不为"running",则通过设置“ctl.start”命令启动mdnsd

         property_set("ctl.start", MDNS_SERVICE_NAME);

        //如果mdnsd成功启动,则属性值变成"running"

        wait_for_property(MDNS_SERVICE_STATUS, "running", 5);

        result = -1;

    } else {

        result = 0;

    }

    pthread_mutex_unlock(&mHeadMutex);

    return result;

}

startService的实现比较有趣,它充分利用了init的属性控制以启动mdnsd进程。

NsdService发送注册服务请求时,HandlerserviceRegister函数将被调用,代码如下所示:

[-->MDnsSdListener.cpp::Handler:serviceRegister]

void MDnsSdListener::Handler::serviceRegister(SocketClient *cli, int requestId,

        const char *interfaceName, const char *serviceName, const char *serviceType,

        const char *domain, const char *host, int port, int txtLen, void *txtRecord) {

    Context *context = new Context(requestId, mListener);

    DNSServiceRef *ref = mMonitor->allocateServiceRef(requestId, context);

    port = htons(port);

   ......

    DNSServiceFlags nativeFlags = 0;

    int interfaceInt = ifaceNameToI(interfaceName);

   //调用Bonjour API DNSServiceRegister,并注册回调函数MDnsSdListenerRegisterCallback

    DNSServiceErrorType result = DNSServiceRegister(ref, interfaceInt,

              nativeFlags, serviceName, serviceType, domain, host, port,

               txtLen, txtRecord, &MDnsSdListenerRegisterCallback, context);

    if (result != kDNSServiceErr_NoError) {

        .....//错误处理

    }

    //通知Monitor对象进行rescan,请读者自行研究该函数

    mMonitor->startMonitoring(requestId);

    cli->sendMsg(ResponseCode::CommandOkay, "serviceRegister started", false);

    return;

}

DNSServiceRegister内部将把请求发送给mdnsd去处理,处理的结果通过MDnsSdListenerRegisterCallback返回,该函数代码如下所示:

[-->MDnsSdListener.cpp::MDnsSdListenerRegisterCallback]

void MDnsSdListenerRegisterCallback(DNSServiceRef sdRef, DNSServiceFlags flags,

        DNSServiceErrorType errorCode, const char *serviceName, const char *regType,

        const char *domain, void *inContext) {

    MDnsSdListener::Context *context =

                einterpret_cast<MDnsSdListener::Context *>(inContext);

    char *msg;

    int refNumber = context->mRefNumber;

    if (errorCode != kDNSServiceErr_NoError) {

       ......//错误处理

    } else {

        char *quotedServiceName = SocketClient::quoteArg(serviceName);

        asprintf(&msg, "%d %s", refNumber, quotedServiceName);

        free(quotedServiceName);

       //将处理结果返回给NsdService

        context->mListener->sendBroadcast(ResponseCode::ServiceRegistrationSucceeded,

                                          msg,false);

    }

    free(msg);

}

 

提示   本节对Netd的工作流程进行了相关分析,这部分代码相对简单,处理流程也比较固定:

1NM接收KernelUEvent消息,然后转发给Framework层的客户端。

2CLDPL以及MDnsSdListener接收来自客户端的请求并处理它们。

唯一有趣的地方是AndroidDNS的管理以及Apple Bonjour技术。感兴趣的读者不妨阅读本章列出的参考资料以加深理解。



[1]此处结论来自bionic/libc/docs/OVERVIEW.txt文件,不过根据同目录下CHANGES.txt的说明,resolv.conf将不再使用

====================================================================================

=========================略略略略略略略略略略略略略略==================================

2.5 本章总结和参考资料说明

2.5.1 本章总结

本章对Netd进行了详细讨论。相信读者读完此章的第一感受一定是代码这么容易的模块,竟然涉及如此多复杂的背景知识。确实,这也是专题卷所述内容的核心特点。从代码上看也许它们并不复杂,但是其背后的理论知识却可能大有来头。对于这些内容而言,代码只是外在的表现形式,其核心一定在其背后的那些知识中。所以,读者在阅读专题卷的时候,一定要考察自己是否对背景知识有所掌握。

概况而言,Netd涉及的内容和网络管理与控制有关,例如DNSApple Bonjour、利用iptables等工具实现NAT、防火墙、带宽控制、流量控制、路由控制功能,以及USB绑定Wi-FiSoftAP等。请读者在本节的参考资料一览中找到并继续研究自己感兴趣的内容。

最后,我们对NetworkManagementService进行了介绍。NMService的内容非常简单。

2.5.2 参考资料说明

Linux PF_NETLINK相关资料

[1]  Linux man PF_NETLINK

本文档是Linux系统中的帮助文档。从总体上介绍了PF_NETLINKAF_NETLINK)的作用和相关的数据结构。对熟手比较适用。

[2]  http://www.linuxjournal.com/article/8498

Manipulating the Networking Environment Using RTNETLINK”,这篇文章以RTNETLINK为主要对象,介绍了如何利用它进行编程以操作网络。此文写得非常详细,建议读者深入阅读,甚至自己动手写测试例子。

DNS、Apple Bonjour相关资料

[3]  http://baike.baidu.com/view/22276.htm 

百度百科中关于dns的介绍,属于入门级材料,不清楚的读者可以先了解相关知识。

[4]  http://en.wikipedia.org/wiki/MDNS 

*中关于Multicast DNS的介绍。入门级材料,但包含的信息不是很全,需要跟踪其中的链接才能对MDNS有全面了解。

[5]  https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/NetServices/Introduction.html#//apple_ref/doc/uid/TP40002445-SW1 

Introduction to Bonjour Overview”,苹果开发网站上关于Bonjour基础知识的入口,包含“About Bonjour”、“Bonjour API Architecture”等文档。

[6]  https://developer.apple.com/library/mac/#documentation/Networking/Conceptual/dns_discovery_api/Introduction.html#//apple_ref/doc/uid/TP30000964 

DNS Service Discovery Programming Guide”,苹果开发网站关于NSD API的说明。

iptables相关资料

iptables的相关文档非常多,虽然Linux也提供了帮助文档(man iptables),但对新手来说该文档实在不是学习的好资料。

[7]  http://www.thegeekstuff.com/2011/01/iptables-fundamentals/ 

Linux Firewall Tutorial: IPTables Tables, Chains, Rules Fundamentals”,这篇文章首先从原理上介绍了如何去理解iptables,然后介绍了相关的例子。笔者认为它是iptables最好的入门资料。

[8]  http://selboo.com.cn/post/721/ 

iptables的相关概念和数据包的流程”,这篇文档介绍了iptables中各个tablechain的处理顺序,请读者结合[7]来理解iptables

[9]  http://www.frozentux.net/iptables-tutorial/cn/iptables-tutorial-cn-1.1.19.html 

Iptables 指南 1.1.19”,这篇文档介绍的iptables版本比较旧(Android 4.2使用的iptables版本是1.4.11),但对iptables常用参数都有非常详细的介绍。适合入门后的读者进行深入阅读。

TC相关资料

tc文献的数量和难度远大于iptables,此处精选几个必读文献。

[10]  http://linux-ip.net/articles/Traffic-Control-HOWTO/intro.html 

Traffic Control HOWTO”,理解traffic control的必读文献,覆盖面很全,理论知识讲解到位。难度稍大,需要仔细琢磨才能完全理解。

[11]  http://wenku.baidu.com/view/f02078db50e2524de5187e45.html 

TC(Linux下流量控制工具)详细说明及应用实例”,百度文库中的一篇文档,篇幅虽然不长,但也做到了理论和实例结合。建议读者先阅读此文献,然后再深入研究[10]

[12]  http://*.chinaunix.net/a1/b1/20010811/0705001103.html 

“在LINUX中实现流量控制器”,介绍TC的一篇博文,主要对tc的命令用法列举了不少实例,属于tc的实战文章。建议放到最后阅读。

[13]  http://www.linuxfoundation.org/collaborate/workgroups/networking/ifb 

这是笔者能找到的关于IFB设备最完整的资料,对IFB的使用、常规用法等进行了全方位的介绍。

IP命令相关资料

ip命令比较简单,这里仅给出一篇文献。

[14]  http://blog.chinaunix.net/uid-24921475-id-2547198.html 

Linux ip命令介绍

NetDevice编程文献

[15]  Linux man netdevice

非常详细的NetDevice编程介绍,建议读者认真阅读。

Linux策略路由相关资料

[16]  http://www.cnblogs.com/iceocean/articles/1594488.html 

Linux策略路由”,中文文档,知识面覆盖较全,属于入门级资料。

[17]  http://www.policyrouting.org/PolicyRoutingBook/ONLINE/TOC.html

Policy Routing With Linux”,这是一本完整的书籍(可见网管是一个复杂的工作)。个人感觉[16]是参考[17]的学习总结。属于高级阅读材料,难度较大。

Linux IPv6控制相关资料

[18]  http://www.ipsidixit.net/2012/08/09/ipv6-temporary-addresses-and-privacy-extensions/ 

IPv6 temporary addresses and privacy extensions”,介绍LinuxIPv6临时地址和privacy extensions方面的知识,知识覆盖面较全。属于入门资料。

 

TTY和ptmx编程相关资料

[19]  http://tldp.org/HOWTO/Text-Terminal-HOWTO.html 

Text-Terminal-HOWTO”,比较旧的资料,覆盖面非常广。读者可仅阅读自己想了解的章节。

[20]  http://blog.tianya.cn/blogger/post_read.asp?BlogID=3616841&PostID=33399981 

Linuxtty/pty/pts/ptmx 详解”,中文写的好材料,还列出了其参考的文献。最后,关于ptmx,读者还可通过man ptmx获得如何用它进行编程的指导。

PPP和Pppd相关资料

[21]  http://tldp.org/HOWTO/PPP-HOWTO/ 

Linux PPP HOWTO”,Linux HowTo系列的内容都简单易懂。虽章节较多,但很多内容仅一两句了事。可做入门参考。

[22]  http://network.51cto.com/art/201009/223784.htm 

“基础解读PPP协议”,中文文档,一页内容,主要介绍PPP框架性的内容。

[23]  http://wenku.baidu.com/view/0c395f15866fb84ae45c8d4a.html 

ppp介绍”,百度文库中的一个关于pppPPT。内容翔实,不仅介绍了ppp协议的数据包,也从框架上介绍了ppp的工作流程。建议读者首先阅读此文献。

[24]  Linux man pppd

介绍pppd中各个选项的作用。

NAT相关资料

[25]  http://oa.jmu.edu.cn/netoa/libq/pubdisc.nsf/66175841be38919248256e35005f4497/7762e8e1056be98f48256e88001ef71d?OpenDocument 

“用iptables实现NAT”,中文文档,简单易懂。

Tether、RNDIS、DHCP、DNSmasq相关资料

[26]  http://en.wikipedia.org/wiki/Tethering 

Tethering”,*中关于Tether的介绍,浅显易懂,属于普及型资料。

[27]   http://msdn.microsoft.com/en-us/library/windows/hardware/gg463293.aspx 

Remote NDIS (RNDIS) and Windows”,MSDN文档,非常翔实(不得不说微软在文档方面的工作真的是一丝不苟)。

[28]  http://baike.baidu.com/view/7992.htm?subLemmaId=7992&fromenter=%A3%C4%A3%C8%A3%C3%A3%D0 

百度百科中关于DHCP的解释,入门资料。

[29]  http://baike.baidu.com/view/6681631.htm 

百度百科中关于DNSmasq的解释。

[30]  http://wenku.baidu.com/view/662b536b561252d380eb6ec1.html

关于DHCP协议中option字段的详细介绍。

Softap和hostapd相关资料

[31]  802.11 无线网络权威指南中文第二版》

读者可先阅读第12章中关于Wi-Fi技术中的一些基本概念,例如APStation

[32]  http://baike.baidu.com/view/2475889.htm 

百度百科关于SoftAp的入门级介绍。

[33]  关于hostapd,读者可利用man hostapd得到各个选项的用法。

提示,读者必须先安装hostapd,然后才能查阅其帮助文档。