Android以太网框架情景分析之NetworkManagementService和netd交互深入分析一

Android网络框架分析系列文章目录:

Android P适配以太网功能开发指南
Android以太网框架情景分析之启动简介
Android以太网框架情景分析之EthernetServiceImpl和NetworkManagementService交互深入分析
Android以太网框架情景分析之NetworkManagementService和netd交互深入分析二
Android以太网框架情景分析之NetworkManagementService和netd交互深入分析一
Android以太网框架情景分析之NetworkFactory与NetworkAgent深入分析
AsyncChannel原理分析以及实操演练


引言

  在前面的篇章Android以太网框架情景分析之启动简介和Android以太网框架情景分析之NetworkFactory与NetworkAgent深入分析我们介绍了以太网的启动,NetworkFactory与NetworkAgent的注网以及关联。还记得我们最开始的关于以太网相关的时序图吗!其中涉及到了监听以太网状态变化的譬如网线的插拔,以太网络状态的变化等,这个就涉及到NetworkManagementService和netd和交互的过程了(当然关于两者之间的交互并不仅仅只有上述两点)。这个篇章就会带领大伙来理解NetworkManagementService和netd的交互,从而理解以太网框架怎么通过NetworkManagementService来设置IP,路由配置以及监听网络状态变化等。同时NetworkManagementService和netd交互不仅单单是为了以太网而设计的,WIFI,移动网络等网络环境也是借助上述两者来完成各种网络环境变化监控以及网络控制的。

本篇会从三个维度来讲解这个篇章,分别是:

  • Netd深入分析
  • NetworkManagementService和Netd交互分析
  • 以太网络和NetworkManagementService及netd的交互

本篇章演示的源码是在Android 7.1 msm8953平台上,其中涉及的源码路径如下所示:

system/netd/server/---CommandListener.cpp---CommandListener.h---main.cpp---NetlinkHandler.cpp---netd.rc---NetlinkManager.cpp---DnsProxyListener.cpp---MDnsSdListener.cpp---FwmarkServer.cpp...NetdNativeService.cpp...system/core/libsysutils/src/---FrameworkListener.cpp  ---NetlinkListener.cpp---FrameworkCommand.cpp  ---NetlinkEvent.cpp---SocketListener.cpp...frameworks/base/services/java/com/android/server/SystemServer.java
frameworks/base/services/core/java/com/android/server/NetworkManagementService.java
frameworks/opt/net/ethernet/java/com/android/server/ethernet/EthernetNetworkFactory.java
frameworks/opt/net/ethernet/java/com/android/server/ethernet/EthernetService.java
frameworks/opt/net/ethernet/java/com/android/server/ethernet/EthernetServiceImpl.java
frameworks/opt/net/ethernet/java/com/android/server/ethernet/EthernetConfigStore.java
frameworks/base/services//core/java/com/android/server/ConnectivityService.java

一.Netd深入分析

1.1 Netd概述以及前期知识准备

  Netd (Network Daemon)是用于管理和控制Android平台网络的后台进程,如下所示:

XXX:/ # ps -t | grep netd
root      785   1     29192  3468  binder_thr 7f951c39e0 S /system/bin/netd
root      868   785   29192  3468  poll_sched 7f951c3a28 S netd
root      1727  785   29192  3468  poll_sched 7f951c3a28 S netd
root      1728  785   29192  3468  poll_sched 7f951c3a28 S netd
root      1729  785   29192  3468  poll_sched 7f951c3a28 S netd
root      1730  785   29192  3468  poll_sched 7f951c3a28 S netd
root      1731  785   29192  3468  poll_sched 7f951c3a10 S netd
root      1732  785   29192  3468  poll_sched 7f951c3a28 S netd
root      1733  785   29192  3468  poll_sched 7f951c3a28 S netd

其按照工作逻辑可分为如下两部分:

  • Netd接收并处理来自Framework层中NetworkManagementService或NsdService的命令。这些命令最终由Netd中对应的Command对象去处理。譬如我们对以太网进行相关设置(静态IP,动态IP,以太网代理等等,以太网的开启关闭等)。
  • Net接收并解析来自Kernel的UEvent消息,然后再转发给Framework层中对应Service去处理。譬如以太网相关服务监听以太网络状态的变化(网线的插拔,以台网的开启关闭ifconfig ethx up/down等)。

  通过上述简单的描述,我们应该知道了Netd位于Framework层和Kernel层之间,它是Android系统中网络相关消息和命令转发及处理的中枢模块起到了承上启下的作用。我们先来看看Netd的整体框架图,如下所示:

  好了,经过上述的描述我想小伙伴们对Netd应该已经有了一个概述了,下面章节我们将开始真正的分析。

1.2 Netd的启动

  通过前面的章节我们知道Netd进程是一个可执行的bin文件程序,而通常它的启动方式一般都是init.xxx.rc进行启动的,但是在Android 5以及之后Android已经将rc权力下放到各个子模块中然后通过import导入了,Netd的启动也不例外下放到了netd.rc中,其配置如下所示:

//netd.rc
service netd /system/bin/netdclass mainsocket netd stream 0660 root systemsocket dnsproxyd stream 0660 root inetsocket mdns stream 0660 root systemsocket fwmarkd stream 0660 root inet

  从上面的配置文件可以看出,Netd进程启动之后将会创建四个TCP的LocalSocket(至于什么是LocalSocket可以参见篇章 Android Framework层和Native层通过LocalSocket实现通信,总之LocalSocket可以用一句话来概括就是Android定制版本的精简Socket),这四个socket如下所示:

而Netd正是通过这四个创建的socket完成了承上启下的作用。通过后面的章节分析我们会看到:

  • /dev/socket/netd将用于和Netd中的CommandListener进行双向通信,而CommandListener主要用来完成接收配置ip,路由,iptables的命令
  • dev/socket/dnsproxyd将用于和Netd中的DnsproxyListener来进行双向通信,而DnsproxyListener主要用来完成Dns的相关操作
  • /dev/socket/mdns将用于和Netd中的MdnsSdListener来进行双向通信,而MdnsSdListener主要用来完成mdnsd的操作
  • /dev/socket/fwmarkd将用于和Netd中的FwmarkServer来进行双向通信,而FwmarkServer用于对特定的socket设置mark值

1.3 Netd进程main函数分析

  对于native进程我们通常从main函数开始入手,对于Netd进程也不例外,其源码路径为system/netd/server/main.c,其代码逻辑如:

int main() {using android::net::gCtls;...//为Netd进程屏蔽SIGPIPE信号blockSigpipe();NetlinkManager *nm = NetlinkManager::Instance();//创建NetlinkManager,详见章节1.4if (nm == nullptr) {ALOGE("Unable to create NetlinkManager");exit(1);};gCtls = new android::net::Controllers();//初始化相关ControllCommandListener cl;//初始化Command,它将创建名为"netd"的监听socket。注意c++中的初始化和Java中的区别,不一定要new才能初始化,这个会在章节1.7重点介绍//设置NetlinkManager的消息发送者(Broadcaster)为CommandListener。nm->setBroadcaster((SocketListener *) &cl);//启动NetlinkManager ,详见章节1.4if (nm->start()) {...}//它为本Netd设置环境变量ANDROID_DNS_MODE为"local"setenv("ANDROID_DNS_MODE", "local", 1);//创建DnsProxyListener,它将创建名为"dnsproxyd"的监听socket,这个不是本文关注的重点DnsProxyListener dpl(&gCtls->netCtrl);if (dpl.startListener()) {...}//创建MDnsSdListener并启动监听,它将创建名为"mdns"的监听socket,这个不是本文关注的重点MDnsSdListener mdnsl;if (mdnsl.startListener()) {...}//创建FwmarkServer并启动监听,它将创建“fwmarkd”的监听socket,这个不是本文关注重点FwmarkServer fwmarkServer(&gCtls->netCtrl);if (fwmarkServer.startListener()) {...}status_t ret;//启动Netd的Natvive Binder Serviceif ((ret = NetdNativeService::start()) != android::OK) {...}//CommandListener 开始监听if (cl.startListener()) {//详见章节1.7.2...}...//开启Binder线程IPCThreadState::self()->joinThreadPool();exit(0);
}

  可以看到Netd进程的main函数非常精简,主要就是创建初始化关键的成员并启动相关的工作,其中涉及的重要成员如下:

  • NetlinkManager:它将接收并处理来自Kernel的UEvent消息。这些消息经NetlinkManager解析后将借助它的Broadcaster(也就是代码中为NetlinkManager设置的CommandListener)发送给Framework层的NetworkManagementService。
  • CommandListener、DnsProxyListener、MDnsSdListener,FwmarkServer:分别创建名为"netd"、“dnsproxyd”、“mdns”,"fwmarkd"的监听socket,并处理来客户端的命令,这里我们会重点讲解CommandListener。
  • 创建NetdNativeService的Native Binder服务,关于该服务的作用暂时还没有研究。

1.4 NetlinkManager

  通过前面的章节我们可知,NetlinkManager主要用于接收并处理来自Kernel的UEvent消息,在NetlinkManager将会启动3个socket,用于监听3种不同的event:Uevent,RouteEvent,QuotaEvent。而对这种监听是通过SocketListener实现监听的,最终通过/dev/socket/netd送到NetworkManagementService(java代码),分配到注册到它里面的各个观察者实例(Ethernet,WIFI等)。感觉用文字描述还是太抽象了,我们可以通过下面的NetlinkManager的架构图来概括上述之间的关系,如下所示:

  下面老规矩分析源码:

//NetlinkManager.cpp
int NetlinkManager::start() {//创建接收NETLINK_KOBJECT_UEVENT消息的socket,其值保存在mUeventSock中//其中,NETLINK_FORMAT_ASCII代表UEvent消息的内容为ASCII字符串if ((mUeventHandler = setupSocket(&mUeventSock, NETLINK_KOBJECT_UEVENT,0xffffffff, NetlinkListener::NETLINK_FORMAT_ASCII, false)) == NULL) {return -1;}// //创建接收RTMGPR_LINK消息的socket,其值保存在mRouteSock中//其中,NETLINK_FORMAT_BINARY代表UEvent消息的类型为结构体,故需要进行二进制解析if ((mRouteHandler = setupSocket(&mRouteSock, NETLINK_ROUTE,RTMGRP_LINK |RTMGRP_IPV4_IFADDR |RTMGRP_IPV6_IFADDR |RTMGRP_IPV6_ROUTE |(1 << (RTNLGRP_ND_USEROPT - 1)),NetlinkListener::NETLINK_FORMAT_BINARY, false)) == NULL) {return -1;}//创建接收NETLINK_NFLOG消息的socket,其值保存在mQuotaSock中if ((mQuotaHandler = setupSocket(&mQuotaSock, NETLINK_NFLOG,NFLOG_QUOTA_GROUP, NetlinkListener::NETLINK_FORMAT_BINARY, false)) == NULL) {ALOGW("Unable to open qlog quota socket, check if xt_quota2 can send via UeventHandler");// TODO: return -1 once the emulator gets a new kernel.}//创建接收NETLINK_NFLOG消息的socket,其值保存在mStrictHandler 中if ((mStrictHandler = setupSocket(&mStrictSock, NETLINK_NETFILTER,0, NetlinkListener::NETLINK_FORMAT_BINARY_UNICAST, true)) == NULL) {ALOGE("Unable to open strict socket");// TODO: return -1 once the emulator gets a new kernel.}return 0;
}

通过源码我们看到NetlinkManager的start函数主要向内核注册了4个接收UEvent事件的socket,这4个socket分别对应于:

  • NETLINK_KOBJECT_UEVENT:代表kobject事件,由于这些事件包含的信息由ASCII字符串表达,故上述代码中使用了NETLINK_FOMRAT_ASCII。它表示将采用字符串解析的方法去解析接收到的UEvent消息。kobject一般用来通知内核中某个模块的加载或卸载。对NetlinkManager来说,其关注的是/sys/class/net下相应模块的加载或卸载消息,譬如以太网模块的加载和卸载。
  • NETLINK_ROUTE:代表kernel中routing或link改变时对应的消息。NETLINK_ROUTE包含很多子项,上述代码中使用了RTMGRP_LINK项。二者结合起来使用,表示NM希望收到网络链路断开或接通时对应的UEvent消息(当网卡上拔掉或插入网线时,会触发这些UEvent消息的发送)。由于对应UEvent消息内部封装了nlmsghdr等相关结构体,故上述代码使用了NETLINK_FORMAT_BINARY来指示解析UEvent消息时将使用二进制的解析方法。
  • NETLINK_NFLOG:和带宽控制有关。Netd中的带宽控制可以设置一个预警值,当网络数据超过一定字节数就会触发kernel发送一个警告
  • NETLINK_NETFILTER:和网络过滤有关系。

  其中的重要的函数setupSocket我们还没有分析,让我们接着对该函数进行分析,其业务逻辑是什么:

//NetlinkManager.cpp
NetlinkHandler *NetlinkManager::setupSocket(int *sock, int netlinkFamily,int groups, int format, bool configNflog) {struct sockaddr_nl nladdr;int sz = 64 * 1024;int on = 1;memset(&nladdr, 0, sizeof(nladdr));nladdr.nl_family = AF_NETLINK;nladdr.nl_pid = getpid();nladdr.nl_groups = groups;//设置socket通信类型if ((*sock = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, netlinkFamily)) < 0) {ALOGE("Unable to create netlink socket: %s", strerror(errno));return NULL;}//设置socket接收缓冲区大小if (setsockopt(*sock, SOL_SOCKET, SO_RCVBUFFORCE, &sz, sizeof(sz)) < 0) {ALOGE("Unable to set uevent socket SO_RCVBUFFORCE option: %s", strerror(errno));close(*sock);return NULL;}//设置socket认证if (setsockopt(*sock, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on)) < 0) {SLOGE("Unable to set uevent socket SO_PASSCRED option: %s", strerror(errno));close(*sock);return NULL;}//对socket执行bind操作if (bind(*sock, (struct sockaddr *) &nladdr, sizeof(nladdr)) < 0) {ALOGE("Unable to bind netlink socket: %s", strerror(errno));close(*sock);return NULL;}...//将前面已经创建并设置好相关配置的socket参数形式传入创建NetlinkHandler,然后调用其start方法NetlinkHandler *handler = new NetlinkHandler(this, *sock, format);//详见章节1.5if (handler->start()) {ALOGE("Unable to start NetlinkHandler: %s", strerror(errno));close(*sock);return NULL;}return handler;
}

这里我们可以看到setupSocket函数的逻辑处理比较简单,就是进行一些基本的socket配置操作,然后将配置好的socket作为参数创建用于接收UEvent消息的NetlinkHandler对象。

1.5 NetlinkHandler

  在正式开始NetlinkHandler这个类的分析前,我们还是先缓一缓,为了读者能更好的进行下面的代码阅读理解,还是先理理其类图关系,不然后续的分析中会被其中的继承关系绕晕的(虽然说这也是面向对象语言的优点,但是有时候也会搞得人稀里糊涂的)。NetlinkManager的类图关系如下所示,可以看出这里的SocketListener在该关系类图中的重要性,它几乎是该类图关系中最原始的起点。


  下面我们从整体上来分析分析该类图中牵涉到的各种关系如下所示:

  • NetlinkHandler和CommandListener均间接从SocketListener派生。其中,NetlinkHandler收到的socket消息将通过onEvent回调处理。
  • 通过前面的章节我们可知NetlinkManager分别注册了四个用于接收UEvent的socket,其对应的NetlinkHandler分别是mUeventHandler、mRouteHandler和mQuotaHandler及StrictHandler。
  • NetlinkHandler将接收到的UEvent消息会转换成一个NetlinkEvent对象。NetlinkEvent对象封装了对UEvent消息的解析方法。对于NETLINK_FOMRAT_ASCII类型,其parseAsciiNetlinkMessage函数会被调用,而对于NETLINK_FORMAT_BINARY类型,其parseBinaryNetlinkMessage函数会被调用。
  • NetLinkManager处理流程的输入为一个解析后的NetlinkEvent对象。NetLinkManager完成相应工作后,其处理结果将经由mBroadcaster对象传递给Framework层的接收者,也就是NetworkManagementService。
  • CommandListener从FrameworkListener派生,而FrameworkListener内部有一个mCommands数组,它用来存储注册到FrameworkListener中的命令处理对象。

1.5.1 NetlinkHandler::start

  好了,其中涉及的类图关系也分析清楚了,前进路上的路障也清除了,直接干代码了!我们从NetlinkHandler的start函数开始,代码如下:

//NetlinkHandler.cpp
int NetlinkHandler::start() {return this->startListener();
}

这里的start函数,最终调用的是NetlinkHandler的超类SocketListener中的startListener函数,还能干啥呢,接着继续干呗!SocketListener类在源码中的路径为system/core/libsysutils/src/SocketListener.cpp。

//SocketListener.cpp
int SocketListener::startListener() {return startListener(4);
}int SocketListener::startListener(int backlog) {if (!mSocketName && mSock == -1) {SLOGE("Failed to start unbound listener");errno = EINVAL;return -1;} else if (mSocketName) {//忽略if ((mSock = android_get_control_socket(mSocketName)) < 0) {SLOGE("Obtaining file descriptor socket '%s' failed: %s",mSocketName, strerror(errno));return -1;}SLOGV("got mSock = %d for %s", mSock, mSocketName);fcntl(mSock, F_SETFD, FD_CLOEXEC);}//如果设置了mListen则监听socket,如果没有设置则新建一个socketClient放入客户端集合,很显然mListen的值为false(通过NetlinkListener.cpp的构造函数传入)if (mListen && listen(mSock, backlog) < 0) {SLOGE("Unable to listen on socket (%s)", strerror(errno));return -1;} else if (!mListen)//以mSock作为参数构建SocketClient对象,并将创建的对象push_back到SocketClientCollection列表中mClients->push_back(new SocketClient(mSock, false, mUseCmdNum));//调用pipe创建一个匿名管道,mCtrlPipe[0]和mCtrlPipe[1]分别代表管道的读写端,if (pipe(mCtrlPipe)) {SLOGE("pipe failed (%s)", strerror(errno));return -1;}//创建线程threadStart监听socket,这里其实并没有所谓的“监听socket”,因为是NETLINK型的socket,从这里可以看出每个SocketListener都会开辟一个新的线程if (pthread_create(&mThread, NULL, SocketListener::threadStart, this)) {//详见章节1.5.2SLOGE("pthread_create (%s)", strerror(errno));return -1;}return 0;
}

1.5.2 SocketListener::threadStart

//SocketListener.cpp
void *SocketListener::threadStart(void *obj) {//这里的obj是创建线程时候传递过来的参数,可以看到它是SocketListener SocketListener *me = reinterpret_cast<SocketListener *>(obj);me->runListener();//调用runListener,这个是关键pthread_exit(NULL);return NULL;
}void SocketListener::runListener() {//创建SocketClientCollection列表pendingListSocketClientCollection pendingList;while(1) {SocketClientCollection::iterator it;fd_set read_fds;//看到这个是不是很熟悉的感觉,肯定和多路复用有关系了int rc = 0;int max = -1;FD_ZERO(&read_fds);//将指定的文件描述符集read_fds清空if (mListen) {//通过前面的分析我们知道mListen为false,所以不会走这个分支...}FD_SET(mCtrlPipe[0], &read_fds);//将前面创建的管道读通道,增加到文件描述符集合read_fds中if (mCtrlPipe[0] > max)max = mCtrlPipe[0];pthread_mutex_lock(&mClientsLock);//遍历mClients,还记得我们每次startListener都会创建一个SocketClient然后放入列表mClients中吗for (it = mClients->begin(); it != mClients->end(); ++it) {int fd = (*it)->getSocket();//获取SocketClient通信相关的socket信息FD_SET(fd, &read_fds);//加入到read_fds进行监听if (fd > max) {max = fd;}}pthread_mutex_unlock(&mClientsLock);SLOGV("mListen=%d, max=%d, mSocketName=%s", mListen, max, mSocketName);if ((rc = select(max + 1, &read_fds, NULL, NULL, NULL)) < 0) {//重点,多路复用监听read_fds中相关的消息if (errno == EINTR)continue;SLOGE("select failed (%s) mListen=%d, max=%d", strerror(errno), mListen, max);sleep(1);continue;} else if (!rc)continue;if (FD_ISSET(mCtrlPipe[0], &read_fds)) {//判断mCtrlPipe[0]文件描述符是否可以读写char c = CtrlPipe_Shutdown;TEMP_FAILURE_RETRY(read(mCtrlPipe[0], &c, 1));//读取管道if (c == CtrlPipe_Shutdown) {break;}continue;}//我们知道在该分支中mListen 为false,所以不会走入到这个里面if (mListen && FD_ISSET(mSock, &read_fds)) {...}pendingList.clear();pthread_mutex_lock(&mClientsLock);//遍历mClients,将所有活动的fd都放入pendingListfor (it = mClients->begin(); it != mClients->end(); ++it) {SocketClient* c = *it;int fd = c->getSocket();if (FD_ISSET(fd, &read_fds)) {//判断是否可以读写pendingList.push_back(c);//放入到pendingList中c->incRef();}}pthread_mutex_unlock(&mClientsLock);//处理pendingList,次是此处表示内核有事件了比如网线的插拔,网络状态的变化等,需要上层处理while (!pendingList.empty()) {it = pendingList.begin();SocketClient* c = *it;pendingList.erase(it);if (!onDataAvailable(c)) {//这个在其子类中被实现,即在NetlinkHandler中,详见章节1.5.3release(c, false);}c->decRef();}}
}

  通过代码分析我们看到runListener函数里面主要通过多路复用对三个类型的fd进行了相关的监听,这三个类型的fd分别是:

  • 第一种类型就是监听类型socket,即通用型的socket
  • 第二类是SocketClient类型socket,并且这类socket被封装成SocketClient集中在一个集合之内。
  • 最后一类就是pipe类型的fd

而我们这里重点关注的是第二个类型SocketClient的socket,从前面分析可知在NetlinkManager.start()中已经启动了四个这样的SocketClient结构,这些socket都是PF_NETLINK类型的socket,并不是监听socket,具体一点就是他们对应的mListen均为false。最终这四个socket被当做SocketClient添加进了mClients中。

  接着将上述的socket对应的fd句柄添加到多路复用监听文件描述符集合中,当检测到上述socket有可读事件发生时,也就是内核有上层感兴趣的事件发生时,对应SocketListen的onDataAvailable()被调用,可是很遗憾,该函数是一个纯虚函数,那么只能从其子类中寻找了,通过查找我们看到在其子类NetlinkListener中实现了onDataAvailable函数(注意SocketListen还有一个子类onDataAvailable也实现了onDataAvailable不要搞错了,至于为什么这里调用的是NetlinkListener大伙可以回忆一下this指针的传递是从NetlinkHandler传递过来的,然后再NetlinkHandler的构造过程中初始化了NetlinkListener),我们接着继续分析。

1.5.3 NetlinkListener::onDataAvailable

//NetlinkListener.cpp
bool NetlinkListener::onDataAvailable(SocketClient *cli)
{int socket = cli->getSocket();ssize_t count;uid_t uid = -1;bool require_group = true;if (mFormat == NETLINK_FORMAT_BINARY_UNICAST) {require_group = false;}//调用uevent_kernel_recv接收内核发送过来的消息,并将消息存放在mBuffer中count = TEMP_FAILURE_RETRY(uevent_kernel_recv(socket,mBuffer, sizeof(mBuffer), require_group, &uid));if (count < 0) {if (uid > 0)LOG_EVENT_INT(65537, uid);SLOGE("recvmsg failed (%s)", strerror(errno));return false;}NetlinkEvent *evt = new NetlinkEvent();//构建一个NetlinkEvent对象if (evt->decode(mBuffer, count, mFormat)) {//调用NetlinkEvent对象evt的decode函数解析mBufferonEvent(evt);//调用onEvent函数,详见章节1.5.4} else if (mFormat != NETLINK_FORMAT_BINARY) {// Don't complain if parseBinaryNetlinkMessage returns false. That can// just mean that the buffer contained no messages we're interested in.SLOGE("Error decoding NetlinkEvent");}delete evt;return true;
}

  这个函数真懒,通过调用uevent_kernel_recv接收内核发送来的消息并将消息封装在mBuffer中,然后构建一个NetlinkEvent对象evt 调用其函数decode解析相关数据,解析完成之后调用onEvent函数,一看又是一个纯虚函数,继续追查最后在其子类NetlinkHandler被实现。

1.5.4 NetlinkHandler::onEvent

//NetlinkHandler.cpp
void NetlinkHandler::onEvent(NetlinkEvent *evt) {const char *subsys = evt->getSubsystem();if (!subsys) {ALOGW("No subsystem found in netlink event");return;}//处理对应NETLINK_KOBJECT_UEVENT和NETLINK_ROUTE的信息if (!strcmp(subsys, "net")) {NetlinkEvent::Action action = evt->getAction();const char *iface = evt->findParam("INTERFACE");//查找消息中携带的网络设备名if (action == NetlinkEvent::Action::kAdd) {notifyInterfaceAdded(iface);//我们关心的以太网络的添加} else if (action == NetlinkEvent::Action::kRemove) {notifyInterfaceRemoved(iface);//移除} else if (action == NetlinkEvent::Action::kChange) {evt->dump();notifyInterfaceChanged("nana", true);//状态变化} else if (action == NetlinkEvent::Action::kLinkUp) {//下面两个消息来自NETLINK_ROUTEnotifyInterfaceLinkChanged(iface, true);链路启用(类似插网线)} else if (action == NetlinkEvent::Action::kLinkDown) {notifyInterfaceLinkChanged(iface, false);链路断开(类似拔网线)} else if (action == NetlinkEvent::Action::kAddressUpdated ||action == NetlinkEvent::Action::kAddressRemoved) {const char *address = evt->findParam("ADDRESS");const char *flags = evt->findParam("FLAGS");const char *scope = evt->findParam("SCOPE");if (action == NetlinkEvent::Action::kAddressRemoved && iface && address) {// Note: if this interface was deleted, iface is "" and we don't notify.SockDiag sd;if (sd.open()) {char addrstr[INET6_ADDRSTRLEN];strncpy(addrstr, address, sizeof(addrstr));char *slash = strchr(addrstr, '/');if (slash) {*slash = '\0';}int ret = sd.destroySockets(addrstr);if (ret < 0) {ALOGE("Error destroying sockets: %s", strerror(ret));}} else {ALOGE("Error opening NETLINK_SOCK_DIAG socket: %s", strerror(errno));}}if (iface && iface[0] && address && flags && scope) {notifyAddressChanged(action, address, iface, flags, scope);}} else if (action == NetlinkEvent::Action::kRdnss) {const char *lifetime = evt->findParam("LIFETIME");const char *servers = evt->findParam("SERVERS");if (lifetime && servers) {notifyInterfaceDnsServers(iface, lifetime, servers);}} else if (action == NetlinkEvent::Action::kRouteUpdated ||action == NetlinkEvent::Action::kRouteRemoved) {const char *route = evt->findParam("ROUTE");const char *gateway = evt->findParam("GATEWAY");const char *iface = evt->findParam("INTERFACE");if (route && (gateway || iface)) {notifyRouteChange(action, route, gateway, iface);}}} else if (!strcmp(subsys, "qlog") || !strcmp(subsys, "xt_quota2")) {//对应NETLINK_NFLOGconst char *alertName = evt->findParam("ALERT_NAME");const char *iface = evt->findParam("INTERFACE");notifyQuotaLimitReached(alertName, iface);} else if (!strcmp(subsys, "strict")) {//不关心const char *uid = evt->findParam("UID");const char *hex = evt->findParam("HEX");notifyStrictCleartext(uid, hex);} else if (!strcmp(subsys, "xt_idletimer")) {//不关心const char *label = evt->findParam("INTERFACE");const char *state = evt->findParam("STATE");const char *timestamp = evt->findParam("TIME_NS");const char *uid = evt->findParam("UID");if (state)notifyInterfaceClassActivity(label, !strcmp("active", state),timestamp, uid);#if !LOG_NDEBUG} else if (strcmp(subsys, "platform") && strcmp(subsys, "backlight")) {/* It is not a VSYNC or a backlight event */ALOGV("unexpected event from subsystem %s", subsys);
#endif}
}

  该函数有点多啊,我们得好好理一理其逻辑:

  • 首先调用NetlinkEvent的getSubsystem函数对内核上报的消息进行一次大的划分,这个划分也是有依据的,还记得我们大明湖畔的夏雨荷吗!错了,是在NetlinkManager中创建的四个NetlinkHandler,而这里的就是依据这个进行相关分支进行的。

  • 大类划分完成以后,接着调用NetlinkEvent的getAction函数接收上述四个大类socket类型的具体数据,然后根据Action的不同调用不同的notifyxxx进行对应的处理。

1.5.4 NetlinkHandler::notify

//NetlinkHandler.cpp
void NetlinkHandler::notifyInterfaceAdded(const char *name) {notify(ResponseCode::InterfaceChange, "Iface added %s", name);
}void NetlinkHandler::notify(int code, const char *format, ...) {char *msg;va_list args;va_start(args, format);if (vasprintf(&msg, format, args) >= 0) {mNm->getBroadcaster()->sendBroadcast(code, msg, false);//最终调用CommandListener的sendBroadcast发送,详见章节1.7.4free(msg);} else {SLOGE("Failed to send notification: vasprintf: %s", strerror(errno));}va_end(args);
}

  可以看到最后都是统一调用了NetlinkHandler的nofity将内核上报的消息,发送给上层。这里的mNm显而易见是NetlinkManager,而getBroadcaster获取的是CommandListener!什么,这里通过getBroadcaster获取到的是CommandListener,这个是什么逻辑,还记的Netd的main函数里面的nm->setBroadcaster吗,关键就在此处。至于CommandListener我们会在后续章节中继续分析!

  这里可以看出最后Netd是借助CommandListener向NetworkManagementService发送消息的。当然这里的消息可能有两种:一种是底层主动上报的消息(即我们这里的这种),另一种是上层请求的response(这个会在后续分析)。

1.6 NetlinkManager和NetlinkHandler小结

  NetlinkManager和NetlinkHandler通过前面的章节已经分析完毕,是不是感觉内容有点多啊。这里谈谈我的心得,一定抓住其中各种类的继承关系已经函数调用逻辑不然真的很容易搞糊涂。在开启CommandListener之前我们还是对前面的NetlinkManager和NetlinkHandler小结一下:

  • NetlinkManager新建了四个个PF_NETLINK类型的socket,然后根据前面创建的socket作为参数创建对应的NetlinkHandler,并且调用start函数启动它

  • 这里的NetlinkHandler继承 NetlinkListener 继承 SocketListener,最后start调用的是SocketListener的startListener

  • startListener将会启动一个单独线程,通过多路复用监听内核的消息,当内核有消息上报时调用onDataAvailable来处理, onDataAvailable方法在NetlinkListener实现

  • NetlinkListener的onDataAvailable将内核的uEvent消息转化为NetlinkEvent调用onEvent来处理消息,onEvent方法在NetlinkHandler实现

  • NetlinkHandler的onEvent方法中根据前面NetlinkManager创建的四个socket来进行大类划分,然后再小类划分,最后调用notify函数。而notify函数最终调用NetlinkManager的Broadcaster也就是CommandListener的sendBroadcast方法,发送消息给发送给Framework层的NetworkManagermentService。

  好吗!NetlinkManager和NetlinkHandler到这里已经撸完了,如果还有不明白的,可以跟着下面的时序图再撸一把!

1.7 CommandListener分析

  前面章节1.5后面还留了一个大尾巴就是关于CommandListener的,即Netd借助CommandListener向NetworkManagementService发送消息的。一种是前面分析的通过NetlinkManager接受底层驱动主动上报的消息(即我们这里的这种),另一种是上层请求的respons消息,这个章节统统给你分析清楚!

1.7.1 CommandListener::CommandListener

  对于CommandListener我们还是从其构造函数开始来分析(面相对象语言吗,通常是这个套路!),在章节1.3中我们知道CommandListener是在Netd的main中被创建的,看招!

//CommandListener.cpp
void CommandListener::registerLockingCmd(FrameworkCommand *cmd, android::RWLock& lock) {registerCmd(new LockingFrameworkCommand(cmd, lock));
}
CommandListener::CommandListener() :FrameworkListener("netd", true) {registerLockingCmd(new InterfaceCmd());//创建了14个命令类对象registerLockingCmd(new IpFwdCmd());registerLockingCmd(new TetherCmd());registerLockingCmd(new NatCmd());registerLockingCmd(new ListTtysCmd());registerLockingCmd(new PppdCmd());registerLockingCmd(new SoftapCmd());registerLockingCmd(new BandwidthControlCmd(), gCtls->bandwidthCtrl.lock);registerLockingCmd(new IdletimerControlCmd());registerLockingCmd(new ResolverCmd());registerLockingCmd(new FirewallCmd(), gCtls->firewallCtrl.lock);registerLockingCmd(new ClatdCmd());registerLockingCmd(new NetworkCommand());registerLockingCmd(new StrictCmd());registerLockingCmd(getQtiConnectivityCmd(this));...//初始化对应的控制类对象,控制类对象的创建,其初始化是在Netd的main函数中gCtls->firewallCtrl.setupIptablesHooks();gCtls->natCtrl.setupIptablesHooks();gCtls->bandwidthCtrl.setupIptablesHooks();gCtls->idletimerCtrl.setupIptablesHooks();gCtls->bandwidthCtrl.enableBandwidthControl(false);...
}

  通过源码我们看到CommandListener构造函数的逻辑可以分为如下两个部分:

  • 的初始化时注册了15个命令类对象, 这些类的定义在CommandListener.h中,都从NetdCommand继承, NetdCommand从FrameworkCommand继承,如下所示:
CommandListener.h
class CommandListener : public FrameworkListener {public:CommandListener();virtual ~CommandListener() {}private:void registerLockingCmd(FrameworkCommand *cmd, android::RWLock& lock);void registerLockingCmd(FrameworkCommand *cmd) {registerLockingCmd(cmd, android::net::gBigNetdLock);}class SoftapCmd : public NetdCommand {public:SoftapCmd();virtual ~SoftapCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);};class InterfaceCmd : public NetdCommand {public:InterfaceCmd();virtual ~InterfaceCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);};class IpFwdCmd : public NetdCommand {public:IpFwdCmd();virtual ~IpFwdCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);};class TetherCmd : public NetdCommand {public:TetherCmd();virtual ~TetherCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);};class NatCmd : public NetdCommand {public:NatCmd();virtual ~NatCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);};class ListTtysCmd : public NetdCommand {public:ListTtysCmd();virtual ~ListTtysCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);};class PppdCmd : public NetdCommand {public:PppdCmd();virtual ~PppdCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);};class BandwidthControlCmd : public NetdCommand {public:BandwidthControlCmd();virtual ~BandwidthControlCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);protected:void sendGenericOkFail(SocketClient *cli, int cond);void sendGenericOpFailed(SocketClient *cli, const char *errMsg);void sendGenericSyntaxError(SocketClient *cli, const char *usageMsg);};class IdletimerControlCmd : public NetdCommand {public:IdletimerControlCmd();virtual ~IdletimerControlCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);};class ResolverCmd : public NetdCommand {public:ResolverCmd();virtual ~ResolverCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);private:bool parseAndExecuteSetNetDns(int netId, int argc, const char** argv);};class FirewallCmd: public NetdCommand {public:FirewallCmd();virtual ~FirewallCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);protected:int sendGenericOkFail(SocketClient *cli, int cond);static FirewallRule parseRule(const char* arg);static FirewallType parseFirewallType(const char* arg);static ChildChain parseChildChain(const char* arg);};class ClatdCmd : public NetdCommand {public:ClatdCmd();virtual ~ClatdCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);};class StrictCmd : public NetdCommand {public:StrictCmd();virtual ~StrictCmd() {}int runCommand(SocketClient *c, int argc, char ** argv);protected:int sendGenericOkFail(SocketClient *cli, int cond);static StrictPenalty parsePenalty(const char* arg);};class NetworkCommand : public NetdCommand {public:NetworkCommand();virtual ~NetworkCommand() {}int runCommand(SocketClient* client, int argc, char** argv);private:int syntaxError(SocketClient* cli, const char* message);int operationError(SocketClient* cli, const char* message, int ret);int success(SocketClient* cli);};
};
  • 接着调用registerCmd函数,该函数的实现是在其父类FrameworkListener中实现,逻辑比较简单就是讲上述创建的命令类存放在列表mCommands中供后续使用。
//FrameworkListener.cpp
typedef android::sysutils::List<FrameworkCommand *> FrameworkCommandCollection;
FrameworkCommandCollection *mCommands;
void FrameworkListener::registerCmd(FrameworkCommand *cmd) {mCommands->push_back(cmd);
}
  • 最后调用控制类进行相关的初始化,这些控制类将和命令类共同完成相应的命令处理工作。这里的gCtls的创建是在Netd的main函数中创建的。是不是对控制类和命令类之间的关系还是有点迷糊,好吗,翠花上图!

1.7.2 CommandListener::startListener

  在Netd中的main函数最后,CommandListener调用startListener启动监听流程, 它的启动逻辑和NetlinkHandler基本一样。这是因为CommandListener继承于FrameworkListener,FrameworkListener继承于SocketListene,从而和NetlinkHandler有相同的超类,。通过前面分析NetlinkHandler可知,在调用startListener最后会在SocketListener启动一个线程。执行runListener方法来处理接收到的消息。但是其和NetlinkHandler处理的逻辑有两个不同点:

  • CommandListener的构造函数中传入的参数有两个分别是”netd“和true,这样就使得SocketListener中的mListen的值被赋值为true,进而runListener函数中监听的是真正的socket(其节点路径为/dev/socket/netd)而不是PF_NETLINK类型的socket。

  • CommandListener最后的onDataAvailable方法是在FrameworkListener实现的,最后调用dispatchCommand处理命令,最后调用runCommand来处理,最后消息通过前面创建的不同类型的命令处理里执行对应的runCommand方法。

1.7.3 FrameworkListener::onDataAvailable

//FrameworkListener.cpp
bool FrameworkListener::onDataAvailable(SocketClient *c) {char buffer[CMD_BUF_SIZE];int len;//读取远程客户端发送过来的消息len = TEMP_FAILURE_RETRY(read(c->getSocket(), buffer, sizeof(buffer)));if (len < 0) {SLOGE("read() failed (%s)", strerror(errno));return false;} else if (!len) {return false;} else if (buffer[len-1] != '\0') {SLOGW("String is not zero-terminated");android_errorWriteLog(0x534e4554, "29831647");c->sendMsg(500, "Command too large for buffer", false);mSkipToNextNullByte = true;return false;}int offset = 0;int i;for (i = 0; i < len; i++) {if (buffer[i] == '\0') {/* IMPORTANT: dispatchCommand() expects a zero-terminated string */if (mSkipToNextNullByte) {mSkipToNextNullByte = false;} else {dispatchCommand(c, buffer + offset);//调用dispatchCommand}offset = i + 1;}}mSkipToNextNullByte = false;return true;
}void FrameworkListener::dispatchCommand(SocketClient *cli, char *data) {FrameworkCommandCollection::iterator i;int argc = 0;char *argv[FrameworkListener::CMD_ARGS_MAX];char tmp[CMD_BUF_SIZE];char *p = data;char *q = tmp;char *qlimit = tmp + sizeof(tmp) - 1;bool esc = false;bool quote = false;bool haveCmdNum = !mWithSeq;memset(argv, 0, sizeof(argv));memset(tmp, 0, sizeof(tmp));while(*p) {if (*p == '\\') {if (esc) {if (q >= qlimit)goto overflow;*q++ = '\\';esc = false;} elseesc = true;p++;continue;} else if (esc) {if (*p == '"') {if (q >= qlimit)goto overflow;*q++ = '"';} else if (*p == '\\') {if (q >= qlimit)goto overflow;*q++ = '\\';} else {cli->sendMsg(500, "Unsupported escape sequence", false);goto out;}p++;esc = false;continue;}if (*p == '"') {if (quote)quote = false;elsequote = true;p++;continue;}if (q >= qlimit)goto overflow;*q = *p++;if (!quote && *q == ' ') {*q = '\0';if (!haveCmdNum) {char *endptr;int cmdNum = (int)strtol(tmp, &endptr, 0);if (endptr == NULL || *endptr != '\0') {cli->sendMsg(500, "Invalid sequence number", false);goto out;}cli->setCmdNum(cmdNum);haveCmdNum = true;} else {if (argc >= CMD_ARGS_MAX)goto overflow;argv[argc++] = strdup(tmp);}memset(tmp, 0, sizeof(tmp));q = tmp;continue;}q++;}*q = '\0';if (argc >= CMD_ARGS_MAX)goto overflow;argv[argc++] = strdup(tmp);
#if 0for (int k = 0; k < argc; k++) {SLOGD("arg[%d] = '%s'", k, argv[k]);}
#endifif (quote) {cli->sendMsg(500, "Unclosed quotes error", false);goto out;}if (errorRate && (++mCommandCount % errorRate == 0)) {/* ignore this command - let the timeout handler handle it */SLOGE("Faking a timeout");goto out;}for (i = mCommands->begin(); i != mCommands->end(); ++i) {FrameworkCommand *c = *i;//查找到匹配合适的命令类if (!strcmp(argv[0], c->getCommand())) {if (c->runCommand(cli, argc, argv)) {//通过命令类来处理,而runCommand的具体实现在CommandListener.cpp中SLOGW("Handler '%s' error (%s)", c->getCommand(), strerror(errno));}goto out;}}cli->sendMsg(500, "Command not recognized", false);
out:int j;for (j = 0; j < argc; j++)free(argv[j]);return;overflow:LOG_EVENT_INT(78001, cli->getUid());cli->sendMsg(500, "Command too long", false);goto out;
}

  通过前面的源码我们可以看到在onDataAvailable函数中通过read读取远端socket客户端发送过来的消息,然后调用dispatchCommand函数匹配前面创建的合适命令类,然后调用命令类的runCommand函数处理客户端发送过来的请求。

1.7.4 CommandListener::sendBroadcast

  还记得前面章节中NetlinkHandler::notify最后调用mNm->getBroadcaster()->sendBroadcast将内核消息发送给客户端吗!通过前面的分析我们可知这里getBroadcaster获取的就是CommandListener,然后调用其sendBroadcast函数发送消息。个人觉得这个BroadCaster名字也容易让人产生误解,以为是通过Android中类似的广播或者UDP中的广播。而实际上这个socket是netd.rc中配置的名字为“netd”的socke所accept出来的SocketClient,是一个TCP socket。而我们知道TCP socket是无法广播的。这里其实最后sendBroadCast最后调用的sendMsg通过socket将消息传递到了客户端,这样就解释就比较好理解了。唠了这么多,还是来分析分析下源码逻辑!

//SocketListener.cpp
void SocketListener::sendBroadcast(int code, const char *msg, bool addErrno) {SocketClientCollection safeList;/* Add all active clients to the safe list first */safeList.clear();pthread_mutex_lock(&mClientsLock);SocketClientCollection::iterator i;for (i = mClients->begin(); i != mClients->end(); ++i) {SocketClient* c = *i;c->incRef();safeList.push_back(c);}pthread_mutex_unlock(&mClientsLock);while (!safeList.empty()) {/* Pop the first item from the list */i = safeList.begin();SocketClient* c = *i;safeList.erase(i);// broadcasts are unsolicited and should not include a cmd numberif (c->sendMsg(code, msg, addErrno, false)) {SLOGW("Error sending broadcast (%s)", strerror(errno));}c->decRef();}
}

  不难看出就是遍历已经和CommandListener建立连接的SocketClient,然后通过基本的socket通信操作将消息发送给socket客户端。

1.8 CommandListener小结

  CommandListener也分析完了,趁热打铁对其小结一把:

  • CommandListener同样继承自SocketListener,与之前创建的四个个NetlinkManager socket所不同的是此类的mListen被设置为true,也就是“netd”为监听socket。

  • 接着CommandListener在Netd中的main函数中调用startListener开启线程用来监听来自java层的连接。当上层有连接时,select返回,accpet得到一个SocketClient,之后将其封装成SocketClient添加到list列表中,并添加进select的监听队列。当java层下发命令,SocketClient的可读事件被检测到,然后调用onDataAvailable来处理,。

  • 和NetlinkHandler不同的是其onDataAvailable方法在FrameworkListener实现的,FrameworkListener的onDataAvailable会调用dispatchCommand来处理消息,而该函数中会匹配前面创建的命令类,然后调用匹配到的命令类的runCommand函数处理

  • 最后将底层处理结果response返回回上层

  • 而内核主动上报的消息也是通过CommandListener中已经连接的SocketClient上发到上层的

  好吗!CommandListener到这里已经撸完了,如果还有不明白的,可以跟着下面的时序图再撸一把!


小结

  本来是想通过一个篇章将该篇章搞定over,然后打卡走人的!但是一看整的有点多啊,这还是在很多内容没有讲述的基础之上,看来还是得另外开垦篇章才行不然就会是老太太的裹脚布又臭又长了!那就先将该篇章的Netd部分讲述完毕,接着后面的篇章接着讲述NetworkManagementService怎么和Netd交互以及以太网络怎么和二者交互的了。万分不舍,也只能是下期见了!希望各位能多多点赞,或者评论,拍砖亦可!关于NetworkManagementService怎么和Netd交互详见篇章Android以太网框架情景分析之NetworkManagementService和netd交互深入分析二。

最后特别鸣谢如下博哥:

https://www.kancloud.cn/alex_wsc/android-wifi-nfc-gps/414021
https://blog.csdn.net/yemin6666/article/details/82318441#Netd%E5%90%AF%E5%8A%A8
https://blog.csdn.net/a34140974/article/details/50717629

Android网络框架情景分析之NetworkManagementService和Netd交互深入分析一相关推荐

  1. Android以太网框架情景分析之EthernetServiceImpl和NetworkManagementService交互深入分析

    EthernetServiceImpl和NetworkManagementService交互深入分析 Android网络框架分析系列文章目录: Android P适配以太网功能开发指南 Android ...

  2. Android以太网框架情景分析之NetworkFactory与NetworkAgent深入分析

    Android以太网框架情景分析之NetworkFactory与NetworkAgent深入分析 Android网络框架分析系列文章目录: Android P适配以太网功能开发指南 Android以太 ...

  3. Android以太网框架情景分析之启动简介

            Android以太网框架情景分析之启动简介 Android网络框架分析系列文章目录: Android P适配以太网功能开发指南 Android以太网框架情景分析之启动简介 Androi ...

  4. ym—— Android网络框架Volley(终极篇)

    转载请注明本文出自Cym的博客(http://blog.csdn.net/cym492224103).谢谢支持! 没看使用过Volley的同学能够,先看看 Android网络框架Volley(体验篇) ...

  5. android网络框架

    https://www.zhihu.com/question/35189851 个人比较推荐Square开源组合,用Retrofit(目前已经是2.0+)+OkHttp基本上已经可以处理任何业务场景了 ...

  6. 老罗的《Android系统源代码情景分析》翻了10遍还看不懂?因为你用错了

    最近老朽又把罗升阳老师的<Android系统源代码情景分析>拿出来啃了一番. 为什么要加个"又"呢?因为从老罗的第一版开始到迄今为止尚未更新的第三版为止,每年有学习冲动 ...

  7. 《Android系统源代码情景分析》一书勘误

    在大家的支持和鼓励下,<Android系统源代码情景分析>一书得以出版了,老罗在此首先谢过大家了.本书的内容来源于博客的文章,经过大半年的整理之后,形成了初稿.在正式出版之前,又经过了三次 ...

  8. Android网络框架Volley项目实战-刘桂林-专题视频课程

    Android网络框架Volley项目实战-5257人已学习 课程介绍         使用Google 2013 I/O大会上发布的Volley请求框架做几个实战项目,归属地查询,QQ测试吉凶,天气 ...

  9. Android 网络框架 Retrofit2.0介绍、使用和封装

    前言 时至今日,Android的网络框架不再像之前那么到处都是,随着Google把 HttpClient直接删掉,似乎意味着Android越来越成熟.网络框架中的佼佼者Volley也不再那么光鲜,取而 ...

  10. Android系统源代码情景分析:基础知识

    老罗(罗升阳)发表在的InfoQ上的好文,最新在学习Android,转载一下,方便学习. 老罗的CSDN blog链接:http://blog.csdn.net/Luoshengyang/ 原文链接: ...

最新文章

  1. 你训练的神经网络不对头的37个原因
  2. 让Updatepanel中的控件触发整个页面Postback
  3. python进阶书籍的推荐 知乎-推荐几本Python3相关书籍?最好分一下基础、进阶、高级...
  4. 2020年“内容、服务”征集
  5. java环境变量搭建
  6. 经典问题:微服务和分布式的区别
  7. 在ASP.NET Core中使用AOP来简化缓存操作
  8. python多线程队列处理_Python线程和队列使用的一点思考
  9. 点广告才可以下载的代码
  10. HBase安装phoenix实战shell操作
  11. 操作系统复习笔记(五)
  12. 培训linux好吗,参加linux专业培训好还是自学linux好
  13. sangerbox使用教程_SangerBox:一款好用的生物信息分析可视化工具
  14. 关于equal和==
  15. iOS 页面的卡顿的原因以及如何解决. 如何优化app的启动速度
  16. unity粗体字+android,[Unity] UGUI加粗字体小记
  17. 什么是大数据及其背后的关键技术
  18. LeetCode 831. Masking Personal Information【字符串,正则表达式】中等
  19. UAT:它也是一种“群体测试”吗?
  20. 对接海康ISC平台API

热门文章

  1. 让你一目了然的商业计划书
  2. 计算机的ps快捷键,ps快捷键常用表
  3. 纯js前端导出Excel表格(Excel科学计数法问题)
  4. 计算机快捷键任务管理器,打开电脑任务管理器快捷键是什么
  5. 如何高效学习,斯科特·扬(全文)
  6. 微信推送封面尺寸_微信公众平台图片尺寸是多少?
  7. prosody xmpp_如何在Ubuntu 18.04上安装Prosody
  8. 扒一扒那些叫欧拉的定理们(五)——平面几何欧拉定理的证明
  9. 制作U盘PE启动盘安装Windows系统
  10. Account locked due to 10 failed logins