Android11 DNS解析流程

news2024/12/22 19:23:41

Android11 DNS解析

1. DNS解析概念

​ DNS的全称是domain name system,即域名系统。主要目的是将域名解析为IP地址,域名是方便用户记忆,但网络传输中源目地址使用IP地址来进行标识的,所以Android中的网络应用程序在发起http请求之前必然要经历DNS解析过程。

2. Android11的DNS解析过程

2.1 Inet6AddressImpl.lookupHostByName

Android app不管用什么网络框架,dns解析都会走到Inet6AddressImpl.lookupHostByName方法,我们就以这个方法为入口开始看代码:

/**
 * Resolves a hostname to its IP addresses using a cache.
 *
 * @param host the hostname to resolve.
 * @param netId the network to perform resolution upon.
 * @return the IP addresses of the host.
 */
private static InetAddress[] lookupHostByName(String host, int netId)
        throws UnknownHostException {
    //参数host代表dns解析的域名,netId代表使用的网络,如wifi,数据网络等等都是可以自己选择的
    BlockGuard.getThreadPolicy().onNetwork();
    // 缓存中是否已经有了,这里的缓存只是应用进程的缓存(TTL为2s),并不是DNS的TTL缓存
        ...
    try {
        StructAddrinfo hints = new StructAddrinfo();
        //不指定协议族,返回包括 IPv4 和 IPv6 的所有地址
        hints.ai_flags = AI_ADDRCONFIG;
        hints.ai_family = AF_UNSPEC;
        //指定socket类型为TCP
        hints.ai_socktype = SOCK_STREAM;
        //真正进行DNS解析的方法
        InetAddress[] addresses = Libcore.os.android_getaddrinfo(host, hints, netId);
        ...
        return addresses;
    } catch (GaiException gaiException) {
          ...
        }
        // 常见的无网报错
        String detailMessage = "Unable to resolve host \"" + host + "\": " + Libcore.os.gai_strerror(gaiException.error);
        addressCache.putUnknownHost(host, netId, detailMessage);
        throw gaiException.rethrowAsUnknownHostException(detailMessage);
    }
}

2.2 BlockGuardOs.android_getaddrinfo

@Override public InetAddress[] android_getaddrinfo(String node, StructAddrinfo hints, int netId) throws GaiException {
    // With AI_NUMERICHOST flag set, the node must a numerical network address, therefore no
    // host address lookups will be performed. In this case, it is fine to perform on main
    // thread.
    boolean isNumericHost = (hints.ai_flags & AI_NUMERICHOST) != 0;
    if (!isNumericHost) {
        BlockGuard.getThreadPolicy().onNetwork();
    }
    return super.android_getaddrinfo(node, hints, netId);
}

2.3 Linux.android_getaddrinfo

public native InetAddress[] android_getaddrinfo(String node, StructAddrinfo hints, int netId) throws GaiException;

这里是一个native方法,对应的native层函数是libcore_io_Linux.Linux_android_getaddrinfo

2.4 libcore_io_Linux.Linux_android_getaddrinfo

static jobjectArray Linux_android_getaddrinfo(JNIEnv* env, jobject, jstring javaNode,
          jobject javaHints, jint netId) {
      ScopedUtfChars node(env, javaNode);
      if (node.c_str() == NULL) {
          return NULL;
      }
  
      static jfieldID flagsFid = env->GetFieldID(JniConstants::GetStructAddrinfoClass(env), "ai_flags", "I");
      static jfieldID familyFid = env->GetFieldID(JniConstants::GetStructAddrinfoClass(env), "ai_family", "I");
      static jfieldID socktypeFid = env->GetFieldID(JniConstants::GetStructAddrinfoClass(env), "ai_socktype", "I");
      static jfieldID protocolFid = env->GetFieldID(JniConstants::GetStructAddrinfoClass(env), "ai_protocol", "I");
  
      addrinfo hints;
      memset(&hints, 0, sizeof(hints));
      //使用java层传递的协议项,socket设置等构造addrinfo对象
      hints.ai_flags = env->GetIntField(javaHints, flagsFid);
      hints.ai_family = env->GetIntField(javaHints, familyFid);
      hints.ai_socktype = env->GetIntField(javaHints, socktypeFid);
      hints.ai_protocol = env->GetIntField(javaHints, protocolFid);
  
      addrinfo* addressList = NULL;
      errno = 0;
      //addressList接收最终的DNS解析结果,这里传的server name是null
      int rc = android_getaddrinfofornet(node.c_str(), NULL, &hints, netId, 0, &addressList);
      //省略后续构造java层对象返回的代码
      ...
      return result;
  }

android_getaddrinfofornet这个函数会调到getaddrinfo.c中:

2.5 getaddrinfo.android_getaddrinfofornet

getaddrinfo.c中经过一系列内部调用,对数据转换,封装之后最终调到android_getaddrinfo_proxy这个函数:

2.6 getaddrinfo.android_getaddrinfo_proxy

#if defined(__ANDROID__)
// Returns 0 on success, else returns on error.
static int
android_getaddrinfo_proxy(
    const char *hostname, const char *servname,
    const struct addrinfo *hints, struct addrinfo **res, unsigned netid)
{
   int success = 0;
   //省略一些判断
    ...
   //这是核心方法,这里会打开一个socket文件
   FILE* proxy = fdopen(__netdClientDispatch.dnsOpenProxy(), "r+");
   if (proxy == NULL) {
      return EAI_SYSTEM;
   }
   netid = __netdClientDispatch.netIdForResolv(netid);

   //向socket发送请求,其实就是向socket对端写入字符串
   if (fprintf(proxy, "getaddrinfo %s %s %d %d %d %d %u",
          hostname == NULL ? "^" : hostname,
          servname == NULL ? "^" : servname,
          hints == NULL ? -1 : hints->ai_flags,
          hints == NULL ? -1 : hints->ai_family,
          hints == NULL ? -1 : hints->ai_socktype,
          hints == NULL ? -1 : hints->ai_protocol,
          netid) < 0) {
      goto exit;
   }
   // literal NULL byte at end, required by FrameworkListener
   if (fputc(0, proxy) == EOF ||
       fflush(proxy) != 0) {
      goto exit;
   }

   char buf[4];
   // 将对端返回的消息放入buf
   if (fread(buf, 1, sizeof(buf), proxy) != sizeof(buf)) {
      goto exit;
   }

   int result_code = (int)strtol(buf, NULL, 10);
   // verify the code itself
   if (result_code != DnsProxyQueryResult) {
      fread(buf, 1, sizeof(buf), proxy);
      goto exit;
   }

   struct addrinfo* ai = NULL;
   struct addrinfo** nextres = res;
   //填充addrinfo数据,这是返回的DNS解析数据,代码省略了,不是重点
   ....
   return EAI_NODATA;
}
#endif

上述函数的,主要作用是打开一个socket描述符,然后向此socket写入一堆字符串,我们需要关注的是打开的是哪个socket?监听此socket的进程是哪个?对端如何处理写入的字符串?

首先来看__netdClientDispatch.dnsOpenProxy()是什么东西?

来看下面一段代码:

template <typename FunctionType>
static void netdClientInitFunction(void* handle, const char* symbol, FunctionType* function) {
    typedef void (*InitFunctionType)(FunctionType*);
    InitFunctionType initFunction = reinterpret_cast<InitFunctionType>(dlsym(handle, symbol));
    if (initFunction != nullptr) {
        initFunction(function);
    }
}

static void netdClientInitImpl() {
    // Prevent netd from looping back fwmarkd connections to itself. It would work, but it's
    // a deadlock hazard and unnecessary overhead for the resolver.
    if (getuid() == 0 && strcmp(basename(getprogname()), "netd") == 0) {
        async_safe_format_log(ANDROID_LOG_INFO, "netdClient",
                              "Skipping libnetd_client init since *we* are netd");
        return;
    }

    void* handle = dlopen("libnetd_client.so", RTLD_NOW);
    if (handle == nullptr) {
        // If the library is not available, it's not an error. We'll just use
        // default implementations of functions that it would've overridden.
        return;
    }

    netdClientInitFunction(handle, "netdClientInitAccept4", &__netdClientDispatch.accept4);
    netdClientInitFunction(handle, "netdClientInitConnect", &__netdClientDispatch.connect);
    netdClientInitFunction(handle, "netdClientInitSendmmsg", &__netdClientDispatch.sendmmsg);
    netdClientInitFunction(handle, "netdClientInitSendmsg", &__netdClientDispatch.sendmsg);
    netdClientInitFunction(handle, "netdClientInitSendto", &__netdClientDispatch.sendto);
    netdClientInitFunction(handle, "netdClientInitSocket", &__netdClientDispatch.socket);

    netdClientInitFunction(handle, "netdClientInitNetIdForResolv",
                           &__netdClientDispatch.netIdForResolv);
    netdClientInitFunction(handle, "netdClientInitDnsOpenProxy",
                           &__netdClientDispatch.dnsOpenProxy);
}

上面代码用到了dlopendlsym函数,dlopen打开的libnetd_client.so定义在netd进程中:

在这里插入图片描述

dlsym函数获取了libnetd_client.so中的netdClientInitDnsOpenProxy函数地址,其代码实现如下所示:

extern "C" void netdClientInitDnsOpenProxy(DnsOpenProxyType* function) {
    if (function) {
        *function = dns_open_proxy;
    }
}

&__netdClientDispatch.dnsOpenProxy地址被传入上述函数,作为函数指针指向了dns_open_proxy函数,所以最终我们知道了前面的fdopen函数打开的socket的是在dns_open_proxy函数中返回的

2.7 NetdClient.dns_open_proxy

int dns_open_proxy() {
    ....
    const auto socketFunc = libcSocket ? libcSocket : socket;
    int s = socketFunc(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (s == -1) {
        return -1;
    }
    const int one = 1;
    setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    
    static const struct sockaddr_un proxy_addr = {
            .sun_family = AF_UNIX,
            .sun_path = "/dev/socket/dnsproxyd",
    };

    const auto connectFunc = libcConnect ? libcConnect : connect;
    if (TEMP_FAILURE_RETRY(
                connectFunc(s, (const struct sockaddr*) &proxy_addr, sizeof(proxy_addr))) != 0) {
        // Store the errno for connect because we only care about why we can't connect to dnsproxyd
        int storedErrno = errno;
        close(s);
        errno = storedErrno;
        return -1;
    }

    return s;
}

上面代码一目了然,作为客户端打开/dev/socket/dnsproxyd并连接,回到getaddrinfo.android_getaddrinfo_proxy函数中,socket连接上之后,发送**“getaddrinfo %s %s %d %d %d %d %u”**字符串,流程图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MP8J1uYg-1686898811898)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230616144316150.png)]

接着需要知道监听/dev/socket/dnsproxyd的服务端是哪个进程?如何处理客户端的字符串?

这个socket的服务端在netd进程里面通过SocketListener监听的,所以DNS相关的请求字符串发给了netd:

在这里插入图片描述

接着看字符串的处理,服务端对这些客户端发送的字符处理方式是通过FrameworkListener注册的命令来处理的,getaddrinfo对应的是GetAddrInfoCmd,直接上代码:

2.8 DnsProxyListener::GetAddrInfoCmd::runCommand

DnsProxyListener::GetAddrInfoCmd::GetAddrInfoCmd() : FrameworkCommand("getaddrinfo") {}
int DnsProxyListener::GetAddrInfoCmd::runCommand(SocketClient *cli,
                                            int argc, char **argv) {
    //省略一些合法性判断
    ...
        
    
    addrinfo* hints = nullptr;
    int ai_flags = strtol(argv[3], nullptr, 10);
    int ai_family = strtol(argv[4], nullptr, 10);
    int ai_socktype = strtol(argv[5], nullptr, 10);
    int ai_protocol = strtol(argv[6], nullptr, 10);
    unsigned netId = strtoul(argv[7], nullptr, 10);
    const bool useLocalNameservers = checkAndClearUseLocalNameserversFlag(&netId);
    const uid_t uid = cli->getUid();
    const int pid = cli->getPid();
    
    android_net_context netcontext;
    //构造netcontext
    gResNetdCallbacks.get_network_context(netId, uid, &netcontext);
    
    if (ai_flags != -1 || ai_family != -1 ||
        ai_socktype != -1 || ai_protocol != -1) {
        //构造addrinfo
        hints = (addrinfo*) calloc(1, sizeof(addrinfo));
        hints->ai_flags = ai_flags;
        hints->ai_family = ai_family;
        hints->ai_socktype = ai_socktype;
        hints->ai_protocol = ai_protocol;
    }

    DnsProxyListener::GetAddrInfoHandler* handler =
            new DnsProxyListener::GetAddrInfoHandler(cli, name, service, hints, netcontext);
    tryThreadOrError(cli, handler);
    return 0;
}

2.9 DnsProxyListener::GetAddrInfoHandler::run

void DnsProxyListener::GetAddrInfoHandler::run() {
    LOG(DEBUG) << "GetAddrInfoHandler::run: {" << mNetContext.app_netid << " "
               << mNetContext.app_mark << " " << mNetContext.dns_netid << " "
               << mNetContext.dns_mark << " " << mNetContext.uid << " " << mNetContext.flags << "}";

     
    addrinfo* result = nullptr;
    Stopwatch s;
    maybeFixupNetContext(&mNetContext, mClient->getPid());
    const uid_t uid = mClient->getUid();
    int32_t rv = 0;
    NetworkDnsEventReported event;
    initDnsEvent(&event, mNetContext);
    if (queryLimiter.start(uid)) {
        if (evaluate_domain_name(mNetContext, mHost)) {
            //核心函数,result用于接收最后的结果
            rv = resolv_getaddrinfo(mHost, mService, mHints, &mNetContext, &result,
                                    &event);
        } else {
            rv = EAI_SYSTEM;
        }
        queryLimiter.finish(uid);
    } else {
        ...
    }
    //IPv4,IPv6地址转换,当 DNS 查询请求中的查询类型为 AAAA(IPv6 地址查询)且查询结果为空时
    //使用IPv4进行查询
    doDns64Synthesis(&rv, &result, &event);
    const int32_t latencyUs = saturate_cast<int32_t>(s.timeTakenUs());
    event.set_latency_micros(latencyUs);
    event.set_event_type(EVENT_GETADDRINFO);
    event.set_hints_ai_flags((mHints ? mHints->ai_flags : 0));

    bool success = true;
    if (rv) {
        // getaddrinfo failed
        success = !mClient->sendBinaryMsg(ResponseCode::DnsProxyOperationFailed, &rv, sizeof(rv));
    } else {
        //DNS解析成功
        success = !mClient->sendCode(ResponseCode::DnsProxyQueryResult);
        addrinfo* ai = result;
        while (ai && success) {
            //DNS解析数据返回给客户端
            success = sendBE32(mClient, 1) && sendaddrinfo(mClient, ai);
            ai = ai->ai_next;
        }
        success = success && sendBE32(mClient, 0);
    }
    ...

}

上面重点函数resolv_getaddrinfo,DNS解析核心函数:

2.10 getaddrinfo.resolv_getaddrinfo

int resolv_getaddrinfo(const char* _Nonnull hostname, const char* servname, const addrinfo* hints,
                       const android_net_context* _Nonnull netcontext, addrinfo** _Nonnull res,
                       NetworkDnsEventReported* _Nonnull event) {
    //省略一些判断
    ...

    addrinfo ai = hints ? *hints : addrinfo{};
    addrinfo sentinel = {};
    addrinfo* cur = &sentinel;
    //explore_options里面定义了一些条件,必须匹配才能进行解析
    for (const Explore& ex : explore_options) {
        
        if (ai.ai_family != ex.e_af) continue;

        if (!MATCH(ai.ai_socktype, ex.e_socktype, WILD_SOCKTYPE(ex))) continue;

        if (!MATCH(ai.ai_protocol, ex.e_protocol, WILD_PROTOCOL(ex))) continue;

        addrinfo tmp = ai;
        if (tmp.ai_socktype == ANY && ex.e_socktype != ANY) tmp.ai_socktype = ex.e_socktype;
        if (tmp.ai_protocol == ANY && ex.e_protocol != ANY) tmp.ai_protocol = ex.e_protocol;
        //核心函数
        error = explore_fqdn(&tmp, hostname, servname, &cur->ai_next, netcontext, event);

        while (cur->ai_next) cur = cur->ai_next;
    }

    // Propagate the last error from explore_fqdn(), but only when *all* attempts failed.
    if ((*res = sentinel.ai_next)) return 0;

    // TODO: consider removing freeaddrinfo.
    freeaddrinfo(sentinel.ai_next);
    *res = nullptr;
    return (error == 0) ? EAI_FAIL : error;
}

2.11 getaddrinfo.explore_fqdn

// FQDN hostname, DNS lookup
static int explore_fqdn(const addrinfo* pai, const char* hostname, const char* servname,
                        addrinfo** res, const android_net_context* netcontext,
                        NetworkDnsEventReported* event) {
    

    addrinfo* result = nullptr;
    int error = 0;

    // If the servname does not match socktype/protocol, return error code.
    if ((error = get_portmatch(pai, servname))) return error;

    if (!files_getaddrinfo(netcontext->dns_netid, hostname, pai, &result)) {
        ALOGE("djtang....files_getaddrinfo == false.");
        error = dns_getaddrinfo(hostname, pai, netcontext, &result, event);
    }
    if (error) {
        freeaddrinfo(result);
        return error;
    }

    for (addrinfo* cur = result; cur; cur = cur->ai_next) {
        // canonname should be filled already
        if ((error = get_port(cur, servname, 0))) {
            freeaddrinfo(result);
            return error;
        }
    }
    *res = result;
    return 0;
}

上面代码首先会去缓存文件/system/etc/hosts去查看是否已经有域名对应IP地址了,如果没有,再调dns_getaddrinfo函数获取,测试了一下dns解析成功之后也不会将解析结果写入/system/etc/hosts,关于files_getaddrinfo函数查缓存的细节就不展开了,不是重点,继续分析dns_getaddrinfo

2.12 getaddrinfo.dns_getaddrinfo

static int dns_getaddrinfo(const char* name, const addrinfo* pai,
                           const android_net_context* netcontext, addrinfo** rv,
                           NetworkDnsEventReported* event) {
    //IPv6,IPv4的解析结果
    res_target q = {};
    res_target q2 = {};
    //省略一堆ipv4,ipv6判断
    ...

    ResState res;
    //将netcontext的值赋给res
    res_init(&res, netcontext, event);

    int he;
    //进行dns解析
    if (res_searchN(name, &q, &res, &he) < 0) {
        //dns解析失败
        return herrnoToAiErrno(he);
    }
    
    addrinfo sentinel = {};
    addrinfo* cur = &sentinel;
    //获取解析结果
    addrinfo* ai = getanswer(q.answer, q.n, q.name, q.qtype, pai, &he);
    if (ai) {
        cur->ai_next = ai;
        while (cur && cur->ai_next) cur = cur->ai_next;
    }
    if (q.next) {
        //获取解析结果
        ai = getanswer(q2.answer, q2.n, q2.name, q2.qtype, pai, &he);
        if (ai) cur->ai_next = ai;
    }
    if (sentinel.ai_next == NULL) {
        // Note that getanswer() doesn't set the pair NETDB_INTERNAL and errno.
        // See also herrnoToAiErrno().
        return herrnoToAiErrno(he);
    }

    _rfc6724_sort(&sentinel, netcontext->app_mark, netcontext->uid);

    *rv = sentinel.ai_next;
    return 0;
}

2.13 getaddrinfo.res_searchN

static int res_searchN(const char* name, res_target* target, res_state res, int* herrno) {
    const char* cp;
    HEADER* hp;
    //定义"."的数量
    uint32_t dots;
    int ret, saved_herrno;
    int got_nodata = 0, got_servfail = 0, tried_as_is = 0;

 

    hp = (HEADER*)(void*)target->answer.data();

    errno = 0;
    *herrno = HOST_NOT_FOUND; /* default, if we never query */
    dots = 0;
    //这段循环的意思是查看name域名中的"."的数量
    for (cp = name; *cp; cp++) dots += (*cp == '.');
    const bool trailing_dot = (cp > name && *--cp == '.') ? true : false;

    
    saved_herrno = -1;
    //res->ndots在前面初始化为1,当dns请求的域名中的"."数量大于等于1时
    if (dots >= res->ndots) {
        //dns解析
        ret = res_querydomainN(name, NULL, target, res, herrno);
        if (ret > 0) return (ret);
        saved_herrno = *herrno;
        tried_as_is++;
    }
    ...
    }

上面代码主要对DNS要解析的域名中的"."进行计算,目的是在处理 DNS 域名时解析域名中有多少个子域名,以方便后面进行分割和处理。

2.14 getaddrinfo.res_querydomainN

static int res_querydomainN(const char* name, const char* domain, res_target* target, res_state res,
                            int* herrno) {
    char nbuf[MAXDNAME];
    const char* longname = nbuf;
    size_t n, d;
    assert(name != NULL);

    if (domain == NULL) {
        // Check for trailing '.'; copy without '.' if present.
        n = strlen(name);
        if (n + 1 > sizeof(nbuf)) {
            *herrno = NO_RECOVERY;
            return -1;
        }
        if (n > 0 && name[--n] == '.') {
            strncpy(nbuf, name, n);
            nbuf[n] = '\0';
        } else
            longname = name;
    } else {
        n = strlen(name);
        d = strlen(domain);
        if (n + 1 + d + 1 > sizeof(nbuf)) {
            *herrno = NO_RECOVERY;
            return -1;
        }
        snprintf(nbuf, sizeof(nbuf), "%s.%s", name, domain);
    }
    ALOGE("djtang...res_querydomainN...name:%s,domain:%s,longname:%s",name,domain,longname);
    return res_queryN_wrapper(longname, target, res, herrno);
}

上面代码会对传入的主机名(name)和域名(domain)进行处理,其中域名可以为 NULL,如果域名为 NULL,就直接使用主机名作为完整域名。如果域名不为 NULL,则将主机名和域名拼接成完整的域名。对于主机名,如果最后有一个点".",则将其删除,以避免后续拼接出问题,然后使用 snprintf() 函数将主机名和域名拼接成完整域名,并将结果存储到 nbuf 缓冲区中,如下是日志打印结果,供参考:

在这里插入图片描述

2.15 getaddrinfo.res_queryN_wrapper

static int res_queryN_wrapper(const char* name, res_target* target, res_state res, int* herrno) {
    const bool parallel_lookup =
            android::net::Experiments::getInstance()->getFlag("parallel_lookup", 0);
    if (parallel_lookup) return res_queryN_parallel(name, target, res, herrno);

    return res_queryN(name, target, res, herrno);
}

2.16 getaddrinfo.res_queryN

static int res_queryN(const char* name, res_target* target, res_state res, int* herrno) {
    uint8_t buf[MAXPACKET];
    int n;
    struct res_target* t;
    int rcode;
    int ancount;

    assert(name != NULL);
    /* XXX: target may be NULL??? */

    rcode = NOERROR;
    ancount = 0;

    for (t = target; t; t = t->next) {
        HEADER* hp = (HEADER*)(void*)t->answer.data();
        bool retried = false;
    again:
        hp->rcode = NOERROR; /* default */

        /* make it easier... */
        int cl = t->qclass;
        int type = t->qtype;
        const int anslen = t->answer.size();

        LOG(DEBUG) << __func__ << ": (" << cl << ", " << type << ")";
        //DNS 查询报文的组装函数,可以将指定类别和类型的 DNS 查询报文组装为二进制格式,并存储到指定缓冲区中,
        //首先对 DNS 查询报文的头部进行初始化,包括设置报文 ID、报文类型、查询类型等字段。然后根据操作类型 op,
        //分别组装并添加报文的查询部分、附加部分和回答部分
        //此函数比较复杂,不必关注
        n = res_nmkquery(QUERY, name, cl, type, /*data=*/nullptr, /*datalen=*/0, buf, sizeof(buf),
                         res->netcontext_flags);
         
        ...
        //发送请求
        n = res_nsend(res, buf, n, t->answer.data(), anslen, &rcode, 0);
        
        ...
        
    }
    ...
    return ancount;
}

上面函数会对dns查询报文进行组装,然后通过res_nsend发送请求报文:

2.17 res_send.res_nsend

这个函数很长,很复杂,作用于 DNS 查询,此函数接收一个 DNS 查询请求并返回相应的 DNS 响应,优先通过查询缓存来优化查询速度,如果查询在缓存中找到,则会在不执行实际查询的情况下返回缓存中的响应。如果启用了 DNS-over-TLS,则使用 DNS-over-TLS 与 DNS 服务器进行通信。否则使用 UDP 或 TCP 协议与 DNS 服务器进行通信,如果在所有 DNS 服务器上都无法获得响应,则该函数将返回错误。

int res_nsend(res_state statp, const uint8_t* buf, int buflen, uint8_t* ans, int anssiz, int* rcode,
              uint32_t flags, std::chrono::milliseconds sleepTimeMs) {
    
    /*
    将dns请求打印出来,大概长这样:
    # Query packet
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29827
    ;; flags: rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
    ;; QUERY SECTION:
    ;;    www.google.com, type = A, class = IN
    */
    res_pquery(buf, buflen);

    int anslen = 0;
    Stopwatch cacheStopwatch;
    //查询缓存
    ResolvCacheStatus cache_status =
            resolv_cache_lookup(statp->netid, buf, buflen, ans, anssiz, &anslen, flags);
    const int32_t cacheLatencyUs = saturate_cast<int32_t>(cacheStopwatch.timeTakenUs());
    if (cache_status == RESOLV_CACHE_FOUND) {
        HEADER* hp = (HEADER*)(void*)ans;
        *rcode = hp->rcode;
        DnsQueryEvent* dnsQueryEvent = addDnsQueryEvent(statp->event);
        dnsQueryEvent->set_latency_micros(cacheLatencyUs);
        dnsQueryEvent->set_cache_hit(static_cast<CacheStatus>(cache_status));
        dnsQueryEvent->set_type(getQueryType(buf, buflen));
        return anslen;
    } else if (cache_status != RESOLV_CACHE_UNSUPPORTED) {

        resolv_populate_res_for_net(statp);
    }
    if (statp->nameserverCount() == 0) {

        _resolv_cache_query_failed(statp->netid, buf, buflen, flags);

        // TODO: Remove errno once callers stop using it
        errno = ESRCH;
        return -ESRCH;
    }

    // If parallel_lookup is enabled, it might be required to wait some time to avoid
    // gateways drop packets if queries are sent too close together
    if (sleepTimeMs != 0ms) {
        std::this_thread::sleep_for(sleepTimeMs);
    }
    // DoT
    if (!(statp->netcontext_flags & NET_CONTEXT_FLAG_USE_LOCAL_NAMESERVERS)) {
        bool fallback = false;
        //DNS-over-TLS查询
        int resplen = res_tls_send(statp, Slice(const_cast<uint8_t*>(buf), buflen),
                                   Slice(ans, anssiz), rcode, &fallback);
        if (resplen > 0) {
            LOG(DEBUG) << __func__ << ": got answer from DoT";
            res_pquery(ans, resplen);
            if (cache_status == RESOLV_CACHE_NOTFOUND) {
                resolv_cache_add(statp->netid, buf, buflen, ans, resplen);
            }
            return resplen;
        }
        if (!fallback) {
            _resolv_cache_query_failed(statp->netid, buf, buflen, flags);
            return -ETIMEDOUT;
        }
    }

    res_stats stats[MAXNS]{};
    res_params params;
    int revision_id = resolv_cache_get_resolver_stats(statp->netid, &params, stats, statp->nsaddrs);
    if (revision_id < 0) {
        // TODO: Remove errno once callers stop using it
        errno = ESRCH;
        return -ESRCH;
    }
    bool usable_servers[MAXNS];
    int usableServersCount = android_net_res_stats_get_usable_servers(
            &params, stats, statp->nameserverCount(), usable_servers);

    if ((flags & ANDROID_RESOLV_NO_RETRY) && usableServersCount > 1) {
        auto hp = reinterpret_cast<const HEADER*>(buf);

        // Select a random server based on the query id
        int selectedServer = (hp->id % usableServersCount) + 1;
        res_set_usable_server(selectedServer, statp->nameserverCount(), usable_servers);
    }

    // Send request, RETRY times, or until successful.
    int retryTimes = (flags & ANDROID_RESOLV_NO_RETRY) ? 1 : params.retry_count;
    int useTcp = buflen > PACKETSZ;
    int gotsomewhere = 0;
    // Use an impossible error code as default value
    int terrno = ETIME;

    for (int attempt = 0; attempt < retryTimes; ++attempt) {
        for (size_t ns = 0; ns < statp->nsaddrs.size(); ++ns) {
            if (!usable_servers[ns]) continue;

            *rcode = RCODE_INTERNAL_ERROR;

            // Get server addr
            const IPSockAddr& serverSockAddr = statp->nsaddrs[ns];
            LOG(DEBUG) << __func__ << ": Querying server (# " << ns + 1
                       << ") address = " << serverSockAddr.toString();

            ::android::net::Protocol query_proto = useTcp ? PROTO_TCP : PROTO_UDP;
            time_t query_time = 0;
            int delay = 0;
            bool fallbackTCP = false;
            const bool shouldRecordStats = (attempt == 0);
            int resplen;
            Stopwatch queryStopwatch;
            int retry_count_for_event = 0;
            size_t actualNs = ns;
            // Use an impossible error code as default value
            terrno = ETIME;
            if (useTcp) {
                // TCP查询
                attempt = retryTimes;
                resplen = send_vc(statp, &params, buf, buflen, ans, anssiz, &terrno, ns,
                                  &query_time, rcode, &delay);

                if (buflen <= PACKETSZ && resplen <= 0 &&
                    statp->tc_mode == aidl::android::net::IDnsResolver::TC_MODE_UDP_TCP) {
                    // reset to UDP for next query on next DNS server if resolver is currently doing
                    // TCP fallback retry and current server does not support TCP connectin
                    useTcp = false;
                }
                LOG(INFO) << __func__ << ": used send_vc " << resplen << " terrno: " << terrno;
            } else {
                // UDP查询
                resplen = send_dg(statp, &params, buf, buflen, ans, anssiz, &terrno, &actualNs,
                                  &useTcp, &gotsomewhere, &query_time, rcode, &delay);
                fallbackTCP = useTcp ? true : false;
                retry_count_for_event = attempt;
                LOG(INFO) << __func__ << ": used send_dg " << resplen << " terrno: " << terrno;
            }

            const IPSockAddr& receivedServerAddr = statp->nsaddrs[actualNs];
            DnsQueryEvent* dnsQueryEvent = addDnsQueryEvent(statp->event);
            dnsQueryEvent->set_cache_hit(static_cast<CacheStatus>(cache_status));
            // When |retryTimes| > 1, we cannot actually know the correct latency value if we
            // received the answer from the previous server. So temporarily set the latency as -1 if
            // that condition happened.
            // TODO: make the latency value accurate.
            dnsQueryEvent->set_latency_micros(
                    (actualNs == ns) ? saturate_cast<int32_t>(queryStopwatch.timeTakenUs()) : -1);
            dnsQueryEvent->set_dns_server_index(actualNs);
            dnsQueryEvent->set_ip_version(ipFamilyToIPVersion(receivedServerAddr.family()));
            dnsQueryEvent->set_retry_times(retry_count_for_event);
            dnsQueryEvent->set_rcode(static_cast<NsRcode>(*rcode));
            dnsQueryEvent->set_protocol(query_proto);
            dnsQueryEvent->set_type(getQueryType(buf, buflen));
            dnsQueryEvent->set_linux_errno(static_cast<LinuxErrno>(terrno));

            // Only record stats the first time we try a query. This ensures that
            // queries that deterministically fail (e.g., a name that always returns
            // SERVFAIL or times out) do not unduly affect the stats.
            if (shouldRecordStats) {
                // (b/151166599): This is a workaround to prevent that DnsResolver calculates the
                // reliability of DNS servers from being broken when network restricted mode is
                // enabled.
                // TODO: Introduce the new server selection instead of skipping stats recording.
                if (!isNetworkRestricted(terrno)) {
                    res_sample sample;
                    res_stats_set_sample(&sample, query_time, *rcode, delay);
                    // KeepListening UDP mechanism is incompatible with usable_servers of legacy
                    // stats, so keep the old logic for now.
                    // TODO: Replace usable_servers of legacy stats with new one.
                    resolv_cache_add_resolver_stats_sample(
                            statp->netid, revision_id, serverSockAddr, sample, params.max_samples);
                }
                resolv_stats_add(statp->netid, receivedServerAddr, dnsQueryEvent);
            }

            if (resplen == 0) continue;
            if (fallbackTCP) {
                ns--;
                continue;
            }
            if (resplen < 0) {
                _resolv_cache_query_failed(statp->netid, buf, buflen, flags);
                statp->closeSockets();
                return -terrno;
            };

            /*
            将dns响应打印出来,大概长这样:
            # Response packet
            ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29827
            ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
            ;; QUERY SECTION:
            ;;    www.google.com, type = A, class = IN

            ;; ANSWER SECTION:
            ;; www.google.com.        2m19s IN A  172.217.160.100
            */
            res_pquery(ans, (resplen > anssiz) ? anssiz : resplen);

            if (cache_status == RESOLV_CACHE_NOTFOUND) {
                resolv_cache_add(statp->netid, buf, buflen, ans, resplen);
            }
            statp->closeSockets();
            return (resplen);
        }  // for each ns
    }  // for each retry
    statp->closeSockets();
    terrno = useTcp ? terrno : gotsomewhere ? ETIMEDOUT : ECONNREFUSED;
    // TODO: Remove errno once callers stop using it
    errno = useTcp ? terrno
                   : gotsomewhere ? ETIMEDOUT /* no answer obtained */
                                  : ECONNREFUSED /* no nameservers found */;

    _resolv_cache_query_failed(statp->netid, buf, buflen, flags);
    return -terrno;
}

上面代码大概分为三块,1. 查询缓存,2. 使用DNS-over-TLS查询,3. 使用TCP或者UDP查询,我们这儿只关注第三点,由于DNS解析走的UDP协议,只需关注send_dg函数:

2.18 res_send.send_dg

static int send_dg(res_state statp, res_params* params, const uint8_t* buf, int buflen,
                   uint8_t* ans, int anssiz, int* terrno, size_t* ns, int* v_circuit,
                   int* gotsomewhere, time_t* at, int* rcode, int* delay) {
  
   
   
    ...
      
    if (statp->nssocks[*ns] == -1) {
        //创建socket
        statp->nssocks[*ns].reset(socket(nsap->sa_family, SOCK_DGRAM | SOCK_CLOEXEC, 0));
        if (statp->nssocks[*ns] < 0) {
            *terrno = errno;
            PLOG(DEBUG) << __func__ << ": socket(dg): ";
            switch (errno) {
                case EPROTONOSUPPORT:
                case EPFNOSUPPORT:
                case EAFNOSUPPORT:
                    return (0);
                default:
                    return (-1);
            }
        }

        const uid_t uid = statp->enforce_dns_uid ? AID_DNS : statp->uid;
        resolv_tag_socket(statp->nssocks[*ns], uid, statp->pid);
        if (statp->_mark != MARK_UNSET) {
            if (setsockopt(statp->nssocks[*ns], SOL_SOCKET, SO_MARK, &(statp->_mark),
                           sizeof(statp->_mark)) < 0) {
                *terrno = errno;
                statp->closeSockets();
                return -1;
            }
        }
        // Use a "connected" datagram socket to receive an ECONNREFUSED error
        // on the next socket operation when the server responds with an
        // ICMP port-unreachable error. This way we can detect the absence of
        // a nameserver without timing out.
        if (random_bind(statp->nssocks[*ns], nsap->sa_family) < 0) {
            *terrno = errno;
            dump_error("bind(dg)", nsap, nsaplen);
            statp->closeSockets();
            return (0);
        }
        //连接到socket,ip地址和端口信息在nsap中,ip地址就是DNS服务器地址,对wifi来说是网关地址,对移动网络来说
        //是网络创建时指定的地址
        if (connect(statp->nssocks[*ns], nsap, (socklen_t)nsaplen) < 0) {
            *terrno = errno;
            dump_error("connect(dg)", nsap, nsaplen);
            statp->closeSockets();
            return (0);
        }
        LOG(DEBUG) << __func__ << ": new DG socket";
    }
    //向DNS服务器发送查询报文
    if (send(statp->nssocks[*ns], (const char*)buf, (size_t)buflen, 0) != buflen) {
        *terrno = errno;
        PLOG(DEBUG) << __func__ << ": send: ";
        statp->closeSockets();
        
        return 0;
    }
    //超时时间
    timespec timeout = get_timeout(statp, params, *ns);
    timespec start_time = evNowTime();
    timespec finish = evAddTime(start_time, timeout);
    for (;;) {
        // 等待回复
        auto result = udpRetryingPollWrapper(statp, *ns, &finish);

        if (!result.has_value()) {
            const bool isTimeout = (result.error().code() == ETIMEDOUT);
            *rcode = (isTimeout) ? RCODE_TIMEOUT : *rcode;
            *terrno = (isTimeout) ? ETIMEDOUT : errno;
            *gotsomewhere = (isTimeout) ? 1 : *gotsomewhere;
            // Leave the UDP sockets open on timeout so we can keep listening for
            // a late response from this server while retrying on the next server.
            if (!isTimeout) statp->closeSockets();
            LOG(DEBUG) << __func__ << ": " << (isTimeout) ? "timeout" : "poll";
            return 0;
        }
        //是否重试
        bool needRetry = false;
        for (int fd : result.value()) {
            needRetry = false;
            sockaddr_storage from;
            socklen_t fromlen = sizeof(from);
            //接收响应消息
            int resplen =
                    recvfrom(fd, (char*)ans, (size_t)anssiz, 0, (sockaddr*)(void*)&from, &fromlen);
             
            if (resplen <= 0) {
                *terrno = errno;
                PLOG(DEBUG) << __func__ << ": recvfrom: ";
                continue;
            }
            *gotsomewhere = 1;
            if (resplen < HFIXEDSZ) {
                // Undersized message.
                LOG(DEBUG) << __func__ << ": undersized: " << resplen;
                *terrno = EMSGSIZE;
                continue;
            }

            int receivedFromNs = *ns;
            if (needRetry =
                        ignoreInvalidAnswer(statp, from, buf, buflen, ans, anssiz, &receivedFromNs);
                needRetry) {
                res_pquery(ans, (resplen > anssiz) ? anssiz : resplen);
                continue;
            }

            HEADER* anhp = (HEADER*)(void*)ans;
            if (anhp->rcode == FORMERR && (statp->netcontext_flags & NET_CONTEXT_FLAG_USE_EDNS)) {
                //  Do not retry if the server do not understand EDNS0.
                //  The case has to be captured here, as FORMERR packet do not
                //  carry query section, hence res_queriesmatch() returns 0.
                LOG(DEBUG) << __func__ << ": server rejected query with EDNS0:";
                res_pquery(ans, (resplen > anssiz) ? anssiz : resplen);
                // record the error
                statp->_flags |= RES_F_EDNS0ERR;
                *terrno = EREMOTEIO;
                continue;
            }

            timespec done = evNowTime();
            *delay = res_stats_calculate_rtt(&done, &start_time);
            if (anhp->rcode == SERVFAIL || anhp->rcode == NOTIMP || anhp->rcode == REFUSED) {
                LOG(DEBUG) << __func__ << ": server rejected query:";
                res_pquery(ans, (resplen > anssiz) ? anssiz : resplen);
                *rcode = anhp->rcode;
                continue;
            }
            if (anhp->tc) {
                // To get the rest of answer,
                // use TCP with same server.
                LOG(DEBUG) << __func__ << ": truncated answer";
                *terrno = E2BIG;
                *v_circuit = 1;
                return 1;
            }
            // All is well, or the error is fatal. Signal that the
            // next nameserver ought not be tried.

            *rcode = anhp->rcode;
            *ns = receivedFromNs;
            *terrno = 0;
            return resplen;
        }
        if (!needRetry) return 0;
    }
}

DNS解析的大致流程到这儿就结束了,这儿我们只看了大致的函数调用流程,其中的大量细节都没具体看,例如缓存机制,网络选择机制等,大致流程图如下:

在这里插入图片描述

总结一下整个过程吧,整个流程分为如下几个步骤:

  1. 应用程序使用网络框架(如okhttp)发送网络请求时,首先会通过java提供的Inet6AddressImpl.lookupHostByName方法获取域名对应的IP地址。
  2. Inet6AddressImpl.lookupHostByName核心是向"/dev/socket/dnsproxyd"这个socket写入一段字符串"getaddrinfo %s %s %d %d %d %d %u",这段字符串包含方法名+参数。
  3. netd进程的DnsProxyListener通过FrameworkListener监听了**“/dev/socket/dnsproxyd”**,接收到客户端发送的字符串调用对应函数GetAddrInfoCmd::runCommand
  4. 接着就是一系列调用,各种合法判断条件,缓存机制最终其实是创建,连接socket,IP和端口就是DNS解析的地址,wifi一般是路由器地址,移动网络一般是自行配置的地址如114.114.114.114或者8.8.8.8等等。

请求和响应的报文经过解析长这样子:

37 # Query packet
38 resolv : ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29827
39 resolv : ;; flags: rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
40 resolv : ;; QUERY SECTION:
41 resolv : ;; www.google.com, type = A, class = IN
42 resolv :
43 resolv : Hex dump:
44 resolv : 7483010000010000000000000377777706676f6f676c6503636f6d0000010001
45
46 # Response packet
47 resolv : ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29827
48 resolv : ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
49 resolv : ;; QUERY SECTION:
50 resolv : ;; www.google.com, type = A, class = IN
51 resolv :
52 resolv : ;; ANSWER SECTION:
53 resolv : ;; www.google.com. 2m19s IN A 172.217.160.100
54 resolv :
55 resolv : Hex dump:
56 resolv : 7483818000010001000000000377777706676f6f676c6503636f6d0000010001
57 resolv : c00c000100010000008b0004acd9a064

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/654421.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL redo log

redo log介绍 重做日志&#xff0c;用于记录事务操作的变化&#xff0c;确保事务的持久性。redo log是在事务开始后&#xff08;begin; 之后&#xff09;就开始记录&#xff0c;不管事务是否提交都会记录下来&#xff0c;在异常发生时&#xff08;如数据持久化过程中掉电&…

如何在 Vue3 组件中使用 TS 类型(必看)

一、为 props 标注类型 使用 <script setup> 方式一&#xff1a;当使用 <script setup> 时&#xff0c;defineProps() 宏函数支持从它的参数中推导类型&#xff1a; const props defineProps({treeTableProps: {type: Array,default: null,required: false},ms…

华为OD机试真题 JavaScript 实现【玩牌高手】【2023 B卷 100分】,附详细解题思路

一、题目描述 给定一个长度为n的整型数组&#xff0c;表示一个选手在n轮内可选择的牌面分数。 选手基于规则选牌。 请计算所有轮结束后其可以获得的最高总分数。 选择规则如下&#xff1a; 在每轮里选手可以选择获取该轮牌面&#xff0c;则其总分数加上该轮牌面分数&#…

✎Qt-doc——尺寸调整策略(QSizePolicy)

目录 QSizePolicy类详述成员类型文档 QSizePolicy类详述 小部件的尺寸策略是其愿意以各种方式调整大小的表达方式&#xff0c;并影响布局引擎对小部件的处理方式。每个小部件返回一个描述其在布局时首选的水平和垂直调整策略的QSizePolicy。您可以通过更改其QWidget::sizePoli…

SpringCloud:分布式事务Seata事务模式

Seata会有 4 种分布式事务解决方案&#xff0c;分别是AT模式、TCC模式、Saga模式和XA模式。 1.AT 模式 2019 年 1 月份&#xff0c;Seata开源了AT模式。AT模式是一种无侵入的分布式事务解决方案。在AT模式下&#xff0c;用户只需关注自己的业务SQL&#xff0c;用户的 业务SQL …

2023-06-16 Android app 使用opencv 调用jni在图片上添加文字,对图片进行模糊处理,源码实例学习。

一、要理解还是得自己看代码 1.1 完整的测试代码路径如下 https://download.csdn.net/download/qq_37858386/87916944 1.2 代码架构 1.3 app 运行效果 二、android studio 添加 opencv module可以参考下面的文章&#xff0c;比较详细。 Android OpenCV 入门教程笔记&#x…

华为OD机试真题 JavaScript 实现【计算最大乘积】【2022Q4 100分】,附详细解题思路

一、题目描述 给定一个元素类型为小写字符串的数组&#xff0c;请计算两个没有相同字符的元素长度乘积的最大值&#xff0c; 如果没有符合条件的两个元素&#xff0c;返回0。 二、输入描述 输入为一个半角逗号分隔的小写字符串的数组&#xff0c;2 < 数组长度<100&am…

Linux之RPM管理工具

目录 Linux之RPM管理工具 定义 作用 RPM软件包 RPM软件包的经典命名格式 RPM安装 语法格式 参数及作用 有关rpm包相关网站 RPM查询功能 语法格式 参数及作用 案例 rpm软件包安装 软件包升级 rpm软件包卸载 rpm卸载 强制卸载 rpm包签名验证 用途 查看签名 …

程序员如何转型成为个人开发者

作者&#xff1a;哈桑c&#xff08;CSDN平台&#xff09; 文章目录 1、什么是个人开发者&#xff1f;2、个人开发者如何赚钱&#xff1f;3、程序员如何转型成为个人开发者&#xff1f;4、成为个人开发者需要学习哪些技能&#xff1f;结语 1、什么是个人开发者&#xff1f; 个人…

redhat安装oracle11g单实例软件建库

1、打开xmanager-passive 2、oracle 用户登录&#xff0c;开始安装 [rootrhel64 database]# su - oracle [oraclerhel64 ~]$ evn |grep oracle -bash: evn: command not found [oraclerhel64 ~]$ evn | grep oracle -bash: evn: command not found [oraclerhel64 ~]$ env | g…

首次使用云服务器搭建网站(三)

上回说到&#xff0c;我们已经搞定了服务器问题和网站模板问题&#xff0c;接下来只需要上传模板即可。 一、上传网站代码 1、打开宝塔面板&#xff0c; 点开文件、这里就是我们服务器的所有文件了。 2、依次点击WWW文件夹、wwwroot文件夹、域名文件夹&#xff0c;进入…

Python之私有属性、私有方法、装饰器及属性和类的命名规则

一、私有属性和私有方法(实现封装) Python对于类的成员没有严格的访问控制限制&#xff0c;这与其他面向对象语言有区别。关于私有属性和私有方法&#xff0c;有如下要点&#xff1a; 通常我们约定&#xff0c;两个下划线开头的属性是私有的(private)。其他为公共的(public)。…

Centos环境 使用docker 部署MySQL 8.X详细版本

文章目录 安装docker配置docker 阿里镜像加速阿里云容器镜像服务ACR配置镜像源 安装部署MySQL拉取MySQL镜像创建挂载文件测试部署部署MySQL进入容器将它的mysql配置同步给宿主机删除test1测试容器 正式部署MySQL查看正式部署的容器状态配置远程连接字符集以及关闭跳过密码验证等…

Aop详解

AOP简介 AOP是一种编程思想&#xff0c;就如同面向对象这种编程思想一样&#xff0c;是一种编程范式&#xff0c;用来指导开发者如何组织程序更好的运行 AOP&#xff08;面向切面编程&#xff09; 作用&#xff1a;在不改变原代码的前提下&#xff0c;为其增加功能。 连接点…

基于 Yeoman 脚手架技术构建前端项目的实践

NodeJ、CLI 基于 Yeoman 脚手架技术构建前端项目的实践 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details…

torch.optim.lr_scheduler.OneCycleLR 学习与理解

一、功能和参数 1.1、通过图像直观地理解 OneCycleLR 的过程&#xff1a; 补充&#xff1a; 生成该图像的代码&#xff1a; 来自&#xff1a;torch.optim.lr_scheduler.OneCycleLR用法_dxz_tust的博客-CSDN博客 import cv2 import torch.nn as nn import torch from torchv…

Nodejs二、内置模块

零、文章目录 Nodejs二、内置模块 1、fs 文件系统模块 &#xff08;1&#xff09;fs 文件系统模块是什么 fs 模块是 Node.js 官方提供的、用来操作文件的模块。它提供了一系列的方法和属性&#xff0c;用来满足用户对文件的操作需求。 fs.readFile() &#xff1a;用来读取指…

【深度学习-第2篇】CNN卷积神经网络30分钟入门!足够通俗易懂了吧(图解)

网络上有着很多关于CNN入门的教程&#xff0c;但是总还是觉得缺少足够简易、直观、全面的文章&#xff0c;能让人通读下来酣畅淋漓&#xff0c;将CNN概念尽收囊中。本篇文章就想尝试一下&#xff0c;真正地带小白同学们轻松入门。 这篇文章包含很多图片&#xff0c;为了花这些…

k8s-containerd容器运行时默认50G存储位置更换

containerd作为k8s主要的cri&#xff0c;它默认存储位置是使用的/根目录挂载的资源。当容器运行的越来越多&#xff0c;默认的50G不够使用了。有2种方法可以进行解决。 方式1、增加/根分区的磁盘空间。 方式2、修改containerd配置文件&#xff0c;修改默认配置为/home 这里我…

【汤4操作系统】深入理解信号量的使用-三大问题的变体

主要从生产者消费者、读写者、哲学家问题中的经典变体进行讲述&#xff0c;均使用伪代码实现 生产者消费者变体 顾客看作是生产出的产品&#xff0c;理发师看作是消费者&#xff0c;沙发有空位&#xff0c;顾客就进去&#xff0c;沙发有顾客&#xff0c;理发师就去理发 和生产者…