从【连接受限】看Android网络

news2024/9/25 17:15:05

从连接受限看Android网络

  • 现象
  • 摸索
    • 从通知开始
    • 是Handler发的通知
      • 看看NetworkStateTrackerHandler
    • NetworkMonitor做了什么
      • NetworkMonitor是一个状态机
      • CaptivePortalProbeResult从何而来
      • 连接受限的直接原因
    • 嗅探是怎样进行的
      • ProbeThread
    • 回过头看看InternalHander
      • registerNetworkAgentInternal的来由
      • NetworkAgent的作用
  • 总结时序

现象

连接受限
谷歌手机,连接受限,经典现象了。
都知道是谷歌网络验证没过,但具体的流程脉络呢?
不如来从现象开始摸一下本质。

摸索

从通知开始

首先找“连接受限”四个字符准没错;
于是发现相关的字符资源在packages/modules/Connectivity/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml中;

<string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 的连接受限"</string>

顺藤摸瓜又找到使用这个字符资源的地方,即packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkNotificationManager.java,一个位于Connectivity模块中的通知管理类(官方逐渐将各类组件移出核心包,作为模块包使用);

 public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
        //...略
		if (notifyType == NotificationType.PARTIAL_CONNECTIVITY
                && transportType == TRANSPORT_WIFI) {
            title = r.getString(R.string.network_partial_connectivity, name);
            details = r.getString(R.string.network_partial_connectivity_detailed);
        } 
     	//...略
    }

在这个包里搜索,找到使用这个通知管理类的地方,即packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java
关注NotificationType.PARTIAL_CONNECTIVITY即可;

    private void showNetworkNotification(NetworkAgentInfo nai, NotificationType type) {
        final String action;
        final boolean highPriority;
        switch (type) {
            //...略
            case PARTIAL_CONNECTIVITY:
                action = ConnectivityManager.ACTION_PROMPT_PARTIAL_CONNECTIVITY;
                highPriority = nai.networkAgentConfig.explicitlySelected;
                break;
        }
		//...略
        mNotifier.showNotification(
                nai.network.getNetId(), type, nai, null, pendingIntent, highPriority);
    }

接着往上摸,发现是由handlePromptUnvalidated方法来处理是否发出连接受限的通知;

    private void handlePromptUnvalidated(Network network) {
        if (VDBG || DDBG) log("handlePromptUnvalidated " + network);
        NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);

        if (nai == null || !shouldPromptUnvalidated(nai)) {
            return;
        }
        nai.onPreventAutomaticReconnect();
        if (nai.partialConnectivity) {
            showNetworkNotification(nai, NotificationType.PARTIAL_CONNECTIVITY);
        } else {
            showNetworkNotification(nai, NotificationType.NO_INTERNET);
        }
    }

而且可以明显看到,是否受限是由NetworkAgentInfopartialConnectivity属性来进行直接判断的——这说明,在此之前,这个属性可能就已经进行了赋值。

是Handler发的通知

先抛开赋值不管,继续看谁在调用handlePromptUnvalidated这个方法;
然后发现有两处,两处都在Handler中;

一处在InternalHandler中,看起来就是会在某个时间点发消息,延迟8秒后去处理不正常的网络;

    private class InternalHandler extends Handler {

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
				//...略
				case EVENT_PROMPT_UNVALIDATED: {
                    handlePromptUnvalidated((Network) msg.obj);
                    break;
                }
                //...略
            }
        }
    }

    private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000;

    private void scheduleUnvalidatedPrompt(NetworkAgentInfo nai) {
        mHandler.sendMessageDelayed(
                mHandler.obtainMessage(EVENT_PROMPT_UNVALIDATED, nai.network),
                PROMPT_UNVALIDATED_DELAY_MS);
    }

另一处则像是专门的网络状态的监听NetworkStateTrackerHandler,会经由多重判断处理,最终由handleNetworkTested方法调用;

    private class NetworkStateTrackerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            if (!maybeHandleNetworkMonitorMessage(msg)
                    && !maybeHandleNetworkAgentInfoMessage(msg)) {
                maybeHandleNetworkAgentMessage(msg);
            }
        }

        private boolean maybeHandleNetworkMonitorMessage(Message msg) {
            final int netId = msg.arg2;
            final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);

            if (nai != null && nai.destroyed) return true;
            switch (msg.what) {
               	//...略
                case EVENT_NETWORK_TESTED: {
                    final NetworkTestedResults results = (NetworkTestedResults) msg.obj;

                    if (nai == null) break;

                    handleNetworkTested(nai, results.mTestResult,
                            (results.mRedirectUrl == null) ? "" : results.mRedirectUrl);
                    break;
                }
                //...略 
            }
            return true;
        }

        
		//按照源码注释理解,这个方法如果先行测试出网络有问题,会先一步通知并取消Handler的通知
        private void handleNetworkTested(
                @NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
			final boolean wasPartial = nai.partialConnectivity;
            nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
			//...略
            if (!wasPartial && nai.partialConnectivity) {
                // Remove delayed message if there is a pending message.
                mHandler.removeMessages(EVENT_PROMPT_UNVALIDATED, nai.network);
                handlePromptUnvalidated(nai.network);
            }
			//...略
        }
    }

这里注意到partialConnectivity值很可能经过了一次变动,在这个阶段并不清楚是否为首次赋值操作,并且与之相关的testResult值似乎也只是一个传递下来的结果,只能暂且记下;

看看NetworkStateTrackerHandler

两个Handler中下面这个Handler看着专业一点,就先看看这个吧;

那么主要关注EVENT_NETWORK_TESTED消息是在哪些地方发起的;

然后就会发现是从另一个网络监听NetworkMonitorCallbacks的回调中发起的;

    private class NetworkMonitorCallbacks extends INetworkMonitorCallbacks.Stub {
    
        @Override
        public void notifyNetworkTestedWithExtras(NetworkTestResultParcelable p) {
            final Message msg = mTrackerHandler.obtainMessage(
                    EVENT_NETWORK_TESTED,
                    0, mNetId,
                    new NetworkTestedResults(
                            mNetId, p.result, p.timestampMillis, p.redirectUrl));
            mTrackerHandler.sendMessage(msg);

            //...略
        }

是从notifyNetworkTestedWithExtras方法中发起的;

那么依旧顺藤摸瓜,看这个监听类是从哪里创建的以及相关方法是在什么时候触发的。

创建的地方找到了registerNetworkAgentInternal方法;

    private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
            LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
            NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId,
            int uid) {
        
        final NetworkAgentInfo nai = new NetworkAgentInfo(na,
                new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo),
                linkProperties, networkCapabilities,
                currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
                this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
                mQosCallbackTracker, mDeps);

        final String extraInfo = networkInfo.getExtraInfo();
        final String name = TextUtils.isEmpty(extraInfo)
                ? nai.networkCapabilities.getSsid() : extraInfo;
        if (DBG) log("registerNetworkAgent " + nai);
        
        mDeps.getNetworkStack().makeNetworkMonitor(
                nai.network, name, new NetworkMonitorCallbacks(nai));
        return nai.network;
    }

一看到register字眼就知道可能抓到大鱼了,事实也确实如此;

registerNetworkAgentInternal方法中创建了上文中的关键信息NetworkAgentInfo,并在此创建了网络状态监听类NetworkMonitor及相关回调类NetworkMonitorCallbacks

触发notifyNetworkTestedWithExtras的地方则找到了packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java中;

    private void notifyNetworkTested(NetworkTestResultParcelable result) {
        try {
            if (mCallbackVersion <= 5) {
                mCallback.notifyNetworkTested(
                        getLegacyTestResult(result.result, result.probesSucceeded),
                        result.redirectUrl);
            } else {
                mCallback.notifyNetworkTestedWithExtras(result);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "Error sending network test result", e);
        }
    }

这也和上面需要创建NetworkMonitor对应上了,这个方法的参数NetworkTestResultParcelable也可以推断大致就是网络测试的结果信息。

那么现在又有两条分支了;

一条是可以检查NetworkAgentInfo创建后,哪些地方进行了赋值操作,尤其是partialConnectivity属性(不忘初心),因为从构造方法上看是没有这条属性的,说明在其他地方进行了处理;

一条是关注NetworkMonitor的生命周期,以及关注NetworkMonitorCallbacks的相关调用。

先从partialConnectivity属性开始追查,发现除了上文中的ConnectivityService#handleNetworkTested方法中,没有其他地方进行过赋值操作,那么基本可以锁定这个方法;

        private void handleNetworkTested(
                @NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
			final boolean wasPartial = nai.partialConnectivity;
            nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
            //...略
        }

常量值INetworkMonitor中定义了常量值NETWORK_VALIDATION_RESULT_PARTIAL

    const int NETWORK_VALIDATION_RESULT_PARTIAL = 0x02;

也就是说,只要testResult的值为2,就会被判定为连接受限

而这个方法中的的testResult又是NetworkMonitor和NetworkMonitorCallbacks相关的;

两条分支其实是一条路;

于是目光再次集中到NetworkMonitor这边来。

NetworkMonitor做了什么

那么接着上面的NetworkMonitor#notifyNetworkTested方法继续往上顺,发现了内部类EvaluationState中的reportEvaluationResult方法;

    protected class EvaluationState {
        
		protected void reportEvaluationResult(int result, @Nullable String redirectUrl) {
            if (mCaptivePortalWantedAsIs) {
                result = NETWORK_VALIDATION_RESULT_VALID;
            } else if (!isValidationRequired() && mProbeCompleted == 0 && mCallbackVersion >= 11) {
                result |= NETWORK_VALIDATION_RESULT_SKIPPED;
            }

            mEvaluationResult = result;
            final NetworkTestResultParcelable p = new NetworkTestResultParcelable();
            p.result = result;
            p.probesSucceeded = mProbeResults;
            p.probesAttempted = mProbeCompleted;
            p.redirectUrl = redirectUrl;
            p.timestampMillis = SystemClock.elapsedRealtime();
            notifyNetworkTested(p);
            recordValidationResult(result, redirectUrl);
        }
    }

这里在创建NetworkTestResultParcelable,但result依旧是传递下来的,同样也受其他值影响;

只能再往上追,追出几个状态类;

public class NetworkMonitor extends StateMachine {
	
    private class DefaultState extends State {
        //...略
    }
    private class ValidatedState extends State {
    	//...略
    }
	private class ProbingState extends State {
        //...略
    }
    private class EvaluatingState extends State {
		//...略     
    }
    private class EvaluatingPrivateDnsState extends State {
        //...略
    }
    //...略
}

代码过多,不作纠结,只需要知道是状态机就行,毕竟NetworkMonitor直接继承了frameworks/base/core/java/com/android/internal/util/StateMachine,这一段代码中大部分操作都是凭借StateMachine来处理的;

先关注ProbingState,因为其中会有关键常量值NETWORK_VALIDATION_RESULT_PARTIAL出现;

    private class ProbingState extends State {

        private Thread mThread;

        @Override
        public void enter() {
            //...略
            final int token = ++mProbeToken;
            final ValidationProperties deps = new ValidationProperties(mNetworkCapabilities);
            final URL fallbackUrl = nextFallbackUrl();
            final URL[] httpsUrls = Arrays.copyOf(
                    mCaptivePortalHttpsUrls, mCaptivePortalHttpsUrls.length);
            final URL[] httpUrls = Arrays.copyOf(
                    mCaptivePortalHttpUrls, mCaptivePortalHttpUrls.length);
            mThread = new Thread(() -> sendMessage(obtainMessage(CMD_PROBE_COMPLETE, token, 0,
                    isCaptivePortal(deps, httpsUrls, httpUrls, fallbackUrl))));
            mThread.start();
        }
        
        @Override
        public boolean processMessage(Message message) {
            switch (message.what) {
                case CMD_PROBE_COMPLETE:
                    if (message.arg1 != mProbeToken) {
                        return HANDLED;
                    }

                    final CaptivePortalProbeResult probeResult =
                            (CaptivePortalProbeResult) message.obj;
					//...略

                    if (probeResult.isSuccessful()) {
                        transitionTo(mEvaluatingPrivateDnsState);
                    } else if (isTermsAndConditionsCaptive(
                            mInfoShim.getCaptivePortalData(mLinkProperties))) {
                        //...略
                    } else if (probeResult.isPortal()) {
                       //...略
                    } else if (probeResult.isPartialConnectivity()) {
                        mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_PARTIAL,
                                null /* redirectUrl */);
                        //...略
                    } else {
                        //...略
                    }
                    return HANDLED;
                //...略
            }
        }
    }

可以看到,转换到此状态后(enter),就发了消息CMD_PROBE_COMPLETE,并将结果由isCaptivePortal方法封装塞进了message;
因此问题变成了怎样去判断probeResult.isPartialConnectivity()

先找到这个类的所在packages/modules/NetworkStack/common/captiveportal/src/android/net/captiveportal/CaptivePortalProbeResult.java,看看相关的方法和属性;

public class CaptivePortalProbeResult {
  
    public static final int PARTIAL_CODE = -1;
    public boolean isPartialConnectivity() {
        return mHttpResponseCode == PARTIAL_CODE;
    }
}

也就是CaptivePortalProbeResultmHttpResponseCode只要被赋值为-1,probeResult.isPartialConnectivity()则为true。

到这里又可以通过两方面来摸索:

一,状态机的启动顺序,什么时候转换到ProbingState;

二,CaptivePortalProbeResult是依据什么来进行创建赋值的。

NetworkMonitor是一个状态机

是一个状态机无疑,可以从其构方法中看出有哪些状态可供转换;

        addState(mDefaultState);
        addState(mMaybeNotifyState, mDefaultState);
            addState(mEvaluatingState, mMaybeNotifyState);
                addState(mProbingState, mEvaluatingState);
                addState(mWaitingForNextProbeState, mEvaluatingState);
            addState(mCaptivePortalState, mMaybeNotifyState);
        addState(mEvaluatingPrivateDnsState, mDefaultState);
        addState(mEvaluatingBandwidthState, mDefaultState);
        addState(mValidatedState, mDefaultState);
        setInitialState(mDefaultState);

默认状态或者初始状态正是DefaultState

    public void notifyNetworkConnected(LinkProperties lp, NetworkCapabilities nc) {
        final NetworkMonitorParameters params = new NetworkMonitorParameters();
        params.linkProperties = lp;
        params.networkCapabilities = nc;
        notifyNetworkConnectedParcel(params);
    }
    
	public void notifyNetworkConnectedParcel(NetworkMonitorParameters params) {
        sendMessage(CMD_NETWORK_CONNECTED, params);
    }

    private class DefaultState extends State {
        @Override
        public void enter() {
            mContext.registerReceiver(mConfigurationReceiver,
                    new IntentFilter(ACTION_CONFIGURATION_CHANGED));
            checkAndRenewResourceConfig();
        }

        @Override
        public boolean processMessage(Message message) {
            switch (message.what) {
                case CMD_NETWORK_CONNECTED:
                    updateConnectedNetworkAttributes(message);
                    logNetworkEvent(NetworkEvent.NETWORK_CONNECTED);
                    transitionTo(mEvaluatingState);
                    return HANDLED;
		}
	}

checkAndRenewResourceConfig方法关系到下文中一些重要参数;
ProbingState是转换EvaluatingState后随即进入的;

    private class EvaluatingState extends State {
        private Uri mEvaluatingCapportUrl;

        @Override
        public void enter() {
            //...略
            sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
            //...略
        }

        @Override
        public boolean processMessage(Message message) {
            switch (message.what) {
                case CMD_REEVALUATE:
                    //...略
                    transitionTo(mProbingState);
                    return HANDLED;
			}
		}

EvaluatingState又是接收到CMD_NETWORK_CONNECTED消息后进入的;
那么很明显,当网络连接成功后,就会进入EvaluatingState;
也就是连接成功后,就会进行评估和嗅探操作;

CaptivePortalProbeResult从何而来

再来关注可以得到嗅探结果的isCaptivePortal方法;

    private CaptivePortalProbeResult isCaptivePortal(ValidationProperties properties,
            URL[] httpsUrls, URL[] httpUrls, URL fallbackUrl) {
        //...略

        final CaptivePortalProbeResult result;
        if (pacUrl != null) {
            result = sendDnsAndHttpProbes(null, pacUrl, ValidationProbeEvent.PROBE_PAC);
            reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result);
        } else if (mUseHttps && httpsUrls.length == 1 && httpUrls.length == 1) {
            result = sendHttpAndHttpsParallelWithFallbackProbes(properties, proxyInfo,
                    httpsUrls[0], httpUrls[0], fallbackUrl);
        } else if (mUseHttps) {
            result = sendMultiParallelHttpAndHttpsProbes(properties, proxyInfo, httpsUrls,
                    httpUrls);
        } else {
            result = sendDnsAndHttpProbes(proxyInfo, httpUrls[0], ValidationProbeEvent.PROBE_HTTP);
            reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result);
        }
		//...略
        return result;
    }

看着一大串if,来一个个排除;

pacUrl,大概理解为代理地址,默认为空;

//packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java 
	final ProxyInfo proxyInfo = mLinkProperties.getHttpProxy();
        if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
            pacUrl = makeURL(proxyInfo.getPacFileUrl().toString());
            if (pacUrl == null) {
                return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
            }
        }    

	//构造方法,LinkProperties创建时为无参构造
    public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network,
            IpConnectivityLog logger, SharedLog validationLogs,
            @NonNull NetworkStackServiceManager serviceManager, Dependencies deps,
            @Nullable TcpSocketTracker tst) {
		//...略
        mLinkProperties = new LinkProperties();
    }
//packages/modules/Connectivity/framework/src/android/net/LinkProperties.java
	public LinkProperties() {
        mParcelSensitiveFields = false;
    }

    public LinkProperties(@Nullable LinkProperties source, boolean parcelSensitiveFields) {
		//...略
        mHttpProxy = (source.mHttpProxy == null) ? null : new ProxyInfo(source.mHttpProxy);
		//...略
    }

    public @Nullable ProxyInfo getHttpProxy() {
        return mHttpProxy;
    }

mUseHttps,大概理解为是否使用https方式,默认为true;

//packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java 
    public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network,
            IpConnectivityLog logger, SharedLog validationLogs,
            @NonNull NetworkStackServiceManager serviceManager, Dependencies deps,
            @Nullable TcpSocketTracker tst) {
		//...略
		mUseHttps = getUseHttpsValidation();
    }

    private boolean getUseHttpsValidation() {
        return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
                CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
    }

httpsUrls和httpUrls,是传递进来的参数,往回退一下,看从哪里取值;

   		final URL[] httpsUrls = Arrays.copyOf(
                    mCaptivePortalHttpsUrls, mCaptivePortalHttpsUrls.length);
        final URL[] httpUrls = Arrays.copyOf(
                    mCaptivePortalHttpUrls, mCaptivePortalHttpUrls.length);

发现在checkAndRenewResourceConfig方法中有过赋值,与上文中的DefaultState呼应;

    private boolean checkAndRenewResourceConfig() {
        //...略
        final URL[] captivePortalHttpsUrls = makeCaptivePortalHttpsUrls(customizedContext);
        if (!Arrays.equals(mCaptivePortalHttpsUrls, captivePortalHttpsUrls)) {
            mCaptivePortalHttpsUrls = captivePortalHttpsUrls;
            reevaluationNeeded = true;
            log("checkAndRenewResourceConfig: update captive portal https urls to "
                    + Arrays.toString(mCaptivePortalHttpsUrls));
        }
        
        final URL[] captivePortalHttpUrls = makeCaptivePortalHttpUrls(customizedContext);
        if (!Arrays.equals(mCaptivePortalHttpUrls, captivePortalHttpUrls)) {
            mCaptivePortalHttpUrls = captivePortalHttpUrls;
            reevaluationNeeded = true;
            log("checkAndRenewResourceConfig: update captive portal http urls to "
                    + Arrays.toString(mCaptivePortalHttpUrls));
        }
		//...略
    }

这里又直接抄了captivePortalHttpsUrls和mCaptivePortalHttpUrls的值;

    private URL[] makeCaptivePortalHttpsUrls(@NonNull Context context) {
        final URL testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL);
        if (testUrl != null) return new URL[] { testUrl };

        final String firstUrl = getCaptivePortalServerHttpsUrl(context);
        try {
            final URL[] settingProviderUrls =
                combineCaptivePortalUrls(firstUrl, CAPTIVE_PORTAL_OTHER_HTTPS_URLS);
            // firstUrl will at least be default configuration, so default value in
            // getProbeUrlArrayConfig is actually never used.
            return getProbeUrlArrayConfig(context, settingProviderUrls,
                    R.array.config_captive_portal_https_urls,
                    DEFAULT_CAPTIVE_PORTAL_HTTPS_URLS, this::makeURL);
        } catch (Exception e) {
            // Don't let a misconfiguration bootloop the system.
            Log.e(TAG, "Error parsing configured https URLs", e);
            // Ensure URL aligned with legacy configuration.
            return new URL[]{makeURL(firstUrl)};
        }
    }

    private String getCaptivePortalServerHttpsUrl(@NonNull Context context) {
        return getSettingFromResource(context,
                R.string.config_captive_portal_https_url, mCaptivePortalHttpsUrlFromSetting,
                context.getResources().getString(
                R.string.default_captive_portal_https_url));
    }

    private URL[] makeCaptivePortalHttpUrls(@NonNull Context context) {
        final URL testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTP_URL);
        if (testUrl != null) return new URL[] { testUrl };

        final String firstUrl = getCaptivePortalServerHttpUrl(context);
        try {
            final URL[] settingProviderUrls =
                    combineCaptivePortalUrls(firstUrl, CAPTIVE_PORTAL_OTHER_HTTP_URLS);
            // firstUrl will at least be default configuration, so default value in
            // getProbeUrlArrayConfig is actually never used.
            return getProbeUrlArrayConfig(context, settingProviderUrls,
                    R.array.config_captive_portal_http_urls,
                    DEFAULT_CAPTIVE_PORTAL_HTTP_URLS, this::makeURL);
        } catch (Exception e) {
            // Don't let a misconfiguration bootloop the system.
            Log.e(TAG, "Error parsing configured http URLs", e);
            // Ensure URL aligned with legacy configuration.
            return new URL[]{makeURL(firstUrl)};
        }
    }

    public String getCaptivePortalServerHttpUrl(@NonNull Context context) {
        return getSettingFromResource(context,
                R.string.config_captive_portal_http_url, mCaptivePortalHttpUrlFromSetting,
                context.getResources().getString(
                R.string.default_captive_portal_http_url));
    }

最终又是在资源文件中取值,而且源码中的注释也说得很清楚,url[]应当只有firstUrl一个值,因此只要default_captive_portal_http_urldefault_captive_portal_https_url的值不为空,那么整个url[]确实就只有一个值;

连接受限的直接原因

packages/modules/NetworkStack/res/values/config.xml中;

    <string name="config_captive_portal_https_url" translatable="false"></string>
    <string name="default_captive_portal_https_url" translatable="false">https://www.google.com/generate_204</string>

    <string name="config_captive_portal_http_url" translatable="false"></string>
    <string name="default_captive_portal_http_url" translatable="false">http://connectivitycheck.gstatic.com/generate_204</string>

有值,两个值的长度恰好为1,mUseHttps又为true,于是满足条件:

    private CaptivePortalProbeResult isCaptivePortal(ValidationProperties properties,
            URL[] httpsUrls, URL[] httpUrls, URL fallbackUrl) {
        //...略
		if (mUseHttps && httpsUrls.length == 1 && httpUrls.length == 1) {
            result = sendHttpAndHttpsParallelWithFallbackProbes(properties, proxyInfo,
                    httpsUrls[0], httpUrls[0], fallbackUrl);
        }
        return result;
    }

到这里,已经初步证实了最初的猜想,https://www.google.com/generate_204这个验证地址,国内肯定连接不上,因此被判定为连接受限
也就是如果能改变default_captive_portal_https_url的值,连接受限4个字将不再出现。

嗅探是怎样进行的

嗅探的结果才是最终的判断依据,那么了解嗅探是怎样进行的也是有必要的了;
先看sendHttpAndHttpsParallelWithFallbackProbes方法;

    private static final int PROBE_TIMEOUT_MS  = 3000;
    
    private CaptivePortalProbeResult sendHttpAndHttpsParallelWithFallbackProbes(
            ValidationProperties properties, ProxyInfo proxy, URL httpsUrl, URL httpUrl,
            URL fallbackUrl) {
        // Number of probes to wait for. If a probe completes with a conclusive answer
        // it shortcuts the latch immediately by forcing the count to 0.
        final CountDownLatch latch = new CountDownLatch(2);

        final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties);
        final ProbeThread httpsProbe = new ProbeThread(latch, properties, proxy, httpsUrl,
                ValidationProbeEvent.PROBE_HTTPS, capportApiUrl);
        final ProbeThread httpProbe = new ProbeThread(latch, properties, proxy, httpUrl,
                ValidationProbeEvent.PROBE_HTTP, capportApiUrl);

        try {
            httpsProbe.start();
            httpProbe.start();
            latch.await(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            validationLog("Error: probes wait interrupted!");
            return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
        }

        final CaptivePortalProbeResult httpsResult = httpsProbe.result();
        final CaptivePortalProbeResult httpResult = httpProbe.result();
		
		if (isConclusiveResult(httpResult, capportApiUrl)) {
            reportProbeResult(httpProbe.result());
            return httpResult;
        }

        if (isConclusiveResult(httpsResult, capportApiUrl)) {
            reportProbeResult(httpsProbe.result());
            return httpsResult;
        }
		//...略
		
        try {
            httpProbe.join();
            reportProbeResult(httpProbe.result());

            if (httpProbe.result().isPortal()) {
                return httpProbe.result();
            }

            httpsProbe.join();
            reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTPS, httpsProbe.result());

            if (httpsProbe.result().isFailed() && httpProbe.result().isSuccessful()) {
                return CaptivePortalProbeResult.PARTIAL;
            }
            return httpsProbe.result();
        } catch (InterruptedException e) {
            validationLog("Error: http or https probe wait interrupted!");
            return CaptivePortalProbeResult.failed(CaptivePortalProbeResult.PROBE_UNKNOWN);
        }
    }

代码较多,但逻辑比较清晰,就是通过httpProbehttpsProbe两个探针同时进行嗅探,如果3秒内不能得到确切结果,就先后嗅探http和https的结果,http有问题就直接返回结果;
而如果http成功https失败,就返回PARTIAL结果,也就是连接受限
这里要结合CaptivePortalProbeResult类的一些属性和方法看才行:

public class CaptivePortalProbeResult {
    public static final int SUCCESS_CODE = 204;
    public static final int FAILED_CODE = 599;
    public static final int PORTAL_CODE = 302;
    
    public static final int PARTIAL_CODE = -1;

    public static final CaptivePortalProbeResult PRIVATE_IP =
            new CaptivePortalProbeResult(DNS_PRIVATE_IP_RESPONSE_CODE, 1 << PROBE_HTTP);
    // Partial connectivity should be concluded from both HTTP and HTTPS probes.
    @NonNull
    public static final CaptivePortalProbeResult PARTIAL = new CaptivePortalProbeResult(
            PARTIAL_CODE, 1 << PROBE_HTTP | 1 << PROBE_HTTPS);

    @ProbeType
    public final int probeType;

    @IntDef(value = {
        PROBE_UNKNOWN,
        1 << PROBE_HTTP,
        1 << PROBE_HTTPS,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ProbeType {
    }

    //...略
    
    public boolean isSuccessful() {
        return isSuccessCode(mHttpResponseCode);
    }

    public boolean isPortal() {
        return isPortalCode(mHttpResponseCode);
    }

    private static boolean isSuccessCode(int responseCode) {
        return responseCode == SUCCESS_CODE;
    }
    
    public static boolean isPortalCode(int responseCode) {
        return !isSuccessCode(responseCode) && (responseCode >= 200) && (responseCode <= 399);
    }

    public boolean isFailed() {
        return !isSuccessful() && !isPortal();
    }

    public boolean isPartialConnectivity() {
        return mHttpResponseCode == PARTIAL_CODE;
    }

结合着响应码来看,如果嗅探或请求成功了,响应码会是204,即无内容的响应码,其他2XX或3XX代表至少地址有所反应,除此之外则为失败。

ProbeThread

上面可以看出探网操作被封装为一个可执行的线程类:

    private class ProbeThread extends Thread {
        //...略
        private volatile CaptivePortalProbeResult mResult;

        public CaptivePortalProbeResult result() {
            return mResult;
        }

        @Override
        public void run() {
            mResult = mProbe.sendProbe();
            if (isConclusiveResult(mResult, mProbe.mCaptivePortalApiUrl)) {
                // Stop waiting immediately if any probe is conclusive.
                while (mLatch.getCount() > 0) {
                    mLatch.countDown();
                }
            }
            // Signal this probe has completed.
            mLatch.countDown();
        }
    }

再拿其中一个具体的probe来看嗅探的具体内容,就HttpsProbe 吧;

    final class HttpsProbe extends Probe {
        HttpsProbe(ValidationProperties properties, ProxyInfo proxy, URL url,
                Uri captivePortalApiUrl) {
            super(properties, proxy, url, captivePortalApiUrl);
        }

        @Override
        protected CaptivePortalProbeResult sendProbe() {
            return sendDnsAndHttpProbes(mProxy, mUrl, ValidationProbeEvent.PROBE_HTTPS);
        }
    }

接下来会看到具体的操作,先在sendDnsAndHttpProbes方法中解析地址,再在makeProbeConnection方法中创建连接,最后由sendHttpProbe发送具体的请求并接收结果;

    private CaptivePortalProbeResult sendDnsAndHttpProbes(ProxyInfo proxy, URL url, int probeType) {
        final String host = (proxy != null) ? proxy.getHost() : url.getHost();
        final InetAddress[] resolvedAddr = sendDnsProbe(host);
        if (mPrivateIpNoInternetEnabled && probeType == ValidationProbeEvent.PROBE_HTTP
                && (proxy == null) && hasPrivateIpAddress(resolvedAddr)) {
            recordProbeEventMetrics(NetworkValidationMetrics.probeTypeToEnum(probeType),
                    0 /* latency */, ProbeResult.PR_PRIVATE_IP_DNS, null /* capportData */);
            return CaptivePortalProbeResult.PRIVATE_IP;
        }
        return sendHttpProbe(url, probeType, null);
    }

    protected CaptivePortalProbeResult sendHttpProbe(URL url, int probeType,
            @Nullable CaptivePortalProbeSpec probeSpec) {
        HttpURLConnection urlConnection = null;
        int httpResponseCode = CaptivePortalProbeResult.FAILED_CODE;
        String redirectUrl = null;
        final Stopwatch probeTimer = new Stopwatch().start();
        final int oldTag = TrafficStats.getAndSetThreadStatsTag(
                NetworkStackConstants.TAG_SYSTEM_PROBE);
        try {
            urlConnection = makeProbeConnection(url, followRedirect);
            
            String requestHeader = urlConnection.getRequestProperties().toString();
            
            httpResponseCode = urlConnection.getResponseCode();
            redirectUrl = urlConnection.getHeaderField("location");
			//...略
        } catch (IOException e) {
            validationLog(probeType, url, "Probe failed with exception " + e);
            if (httpResponseCode == CaptivePortalProbeResult.FAILED_CODE) {
                // TODO: Ping gateway and DNS server and log results.
            }
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            TrafficStats.setThreadStatsTag(oldTag);
        }
        //...略
        return probeResult;
    }

    private HttpURLConnection makeProbeConnection(URL url, boolean followRedirects)
            throws IOException {
        final HttpURLConnection conn = (HttpURLConnection) mCleartextDnsNetwork.openConnection(url);
        conn.setInstanceFollowRedirects(followRedirects);
        conn.setConnectTimeout(SOCKET_TIMEOUT_MS);
        conn.setReadTimeout(SOCKET_TIMEOUT_MS);
        conn.setRequestProperty("Connection", "close");
        conn.setUseCaches(false);
        if (mCaptivePortalUserAgent != null) {
            conn.setRequestProperty("User-Agent", mCaptivePortalUserAgent);
        }
        return conn;
    }

回过头看看InternalHander

不要忘记以上都是从NetworkStateTrackerHandler中追踪而得,另一个InternalHander是也可以触发通知的;
现在再回过头看看InternalHander中会如何处理,也就是看谁调用了scheduleUnvalidatedPrompt方法,这个方法的作用是8秒后就向InternalHander发送消息通知当前的网络存在故障;
于是就在当前ConnectivityService中找到updateNetworkInfo方法;

    private void updateNetworkInfo(NetworkAgentInfo networkAgent, NetworkInfo info) {
        //...略
        if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) {
            networkAgent.everConnected = true;
            networkAgent.getAndSetNetworkCapabilities(networkAgent.networkCapabilities);

            handlePerNetworkPrivateDnsConfig(networkAgent, mDnsManager.getPrivateDnsConfig());
            updateLinkProperties(networkAgent, new LinkProperties(networkAgent.linkProperties),
                    null);
			//...略
            if (networkAgent.networkAgentConfig.acceptPartialConnectivity) {
                networkAgent.networkMonitor().setAcceptPartialConnectivity();
            }
            //...略
            scheduleUnvalidatedPrompt(networkAgent);

            //...略
        } else if (state == NetworkInfo.State.DISCONNECTED) {
            //...略
        } else if (networkAgent.created && (oldInfo.getState() == NetworkInfo.State.SUSPENDED ||
                state == NetworkInfo.State.SUSPENDED)) {
            //...略
        }
    }

这方法一看调用的地方就不会只有一处;
果然找到3处;
但最值得关注无疑是下面的handleRegisterNetworkAgent方法:

    private void handleRegisterNetworkAgent(NetworkAgentInfo nai, INetworkMonitor networkMonitor) {
        final NetworkCapabilities nc = new NetworkCapabilities(nai.networkCapabilities);
        final LinkProperties lp = new LinkProperties(nai.linkProperties);
        processCapabilitiesFromAgent(nai, nc);
        nai.getAndSetNetworkCapabilities(mixInCapabilities(nai, nc));
        processLinkPropertiesFromAgent(nai, lp);
        nai.linkProperties = lp;

        nai.onNetworkMonitorCreated(networkMonitor);

        mNetworkAgentInfos.add(nai);
        synchronized (mNetworkForNetId) {
            mNetworkForNetId.put(nai.network.getNetId(), nai);
        }

        try {
            networkMonitor.start();
        } catch (RemoteException e) {
            e.rethrowAsRuntimeException();
        }

        nai.notifyRegistered();
        NetworkInfo networkInfo = nai.networkInfo;
        updateNetworkInfo(nai, networkInfo);
        updateVpnUids(nai, null, nai.networkCapabilities);
    }

在这里启动networkMonitor,似乎又和上文中某些节点对应上了;
并且我们记得NetworkAgentInfoINetworkMonitor正好是在上文中另一个Handler分析中的registerNetworkAgentInternal方法中创建的:

    private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
            LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
            NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId,
            int uid) {
            
        final NetworkAgentInfo nai = new NetworkAgentInfo(na,
                new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo),
                linkProperties, networkCapabilities,
                currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
                this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
                mQosCallbackTracker, mDeps);
                
        mDeps.getNetworkStack().makeNetworkMonitor(
                nai.network, name, new NetworkMonitorCallbacks(nai));
        return nai.network;
    }

来看看是谁把这里创建的两种信息传递到InternalHandler并交由handleRegisterNetworkAgent方法的;

    private class InternalHandler extends Handler {
        public InternalHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case EVENT_REGISTER_NETWORK_AGENT: {
                    final Pair<NetworkAgentInfo, INetworkMonitor> arg =
                            (Pair<NetworkAgentInfo, INetworkMonitor>) msg.obj;
                    handleRegisterNetworkAgent(arg.first, arg.second);
                    break;
                }
            }
        }
     }

然后惊喜地发现,是老熟人了;

    private class NetworkMonitorCallbacks extends INetworkMonitorCallbacks.Stub {

        @Override
        public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) {
            mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT,
                    new Pair<>(mNai.getAndDestroy(), networkMonitor)));
        }
	}

当然这里的回调是进程间的调用,关系到packages/modules/NetworkStack/src/com/android/server/NetworkStackService.java

public class NetworkStackService extends Service {
        @Override
        public void makeNetworkMonitor(Network network, String name, INetworkMonitorCallbacks cb)
                throws RemoteException {
            mPermChecker.enforceNetworkStackCallingPermission();
            updateNetworkStackAidlVersion(cb.getInterfaceVersion(), cb.getInterfaceHash());
            final SharedLog log = addValidationLogs(network, name);
            final NetworkMonitor nm = mDeps.makeNetworkMonitor(mContext, cb, network, log, this);
            cb.onNetworkMonitorCreated(new NetworkMonitorConnector(nm, mPermChecker));
        }
}

于是大致的脉络就有了——
registerNetworkAgentInternal方法中创建了NetworkAgentInfo和INetworkMonitor,并且由于调用了makeNetworkMonitor方法以及NetworkMonitorCallbacks回调存在的原因,NetworkMonitor创建时就回调了onNetworkMonitorCreated方法,随之向InternalHandler发送消息,调用了handleRegisterNetworkAgent方法。

那么接下来的问题就是,registerNetworkAgentInternal是干什么的,什么时候会调用呢?

registerNetworkAgentInternal的来由

往上追溯,还在ConnectivityService中,但却罕见的是一个公有方法了,意味着这可能是一个入口;

    public Network registerNetworkAgent(INetworkAgent na, NetworkInfo networkInfo,
            LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
            @NonNull NetworkScore initialScore, NetworkAgentConfig networkAgentConfig,
            int providerId) {
        //...略
        final int uid = mDeps.getCallingUid();
        final long token = Binder.clearCallingIdentity();
        try {
            return registerNetworkAgentInternal(na, networkInfo, linkProperties,
                    networkCapabilities, initialScore, networkAgentConfig, providerId, uid);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

它也确实是一个入口,毕竟ConnectivityService本身就太眼熟了;
packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java中找到了;

    public Network registerNetworkAgent(INetworkAgent na, NetworkInfo ni, LinkProperties lp,
            NetworkCapabilities nc, @NonNull NetworkScore score, NetworkAgentConfig config,
            int providerId) {
        try {
            return mService.registerNetworkAgent(na, ni, lp, nc, score, config, providerId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

ConnectivityManager就更熟悉了;
不过调用这个方法的地方却极少,只在packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java中出现;

public abstract class NetworkAgent {
    public Network register() {
        if (VDBG) log("Registering NetworkAgent");
        synchronized (mRegisterLock) {
            if (mNetwork != null) {
                throw new IllegalStateException("Agent already registered");
            }
            final ConnectivityManager cm = (ConnectivityManager) mInitialConfiguration.context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
                    new NetworkInfo(mInitialConfiguration.info),
                    mInitialConfiguration.properties, mInitialConfiguration.capabilities,
                    mInitialConfiguration.score, mInitialConfiguration.config, providerId);
            mInitialConfiguration = null; // All this memory can now be GC'd
        }
        return mNetwork;
    }
}

NetworkAgent的作用

NetworkAgent,网络代理,注册;
既然是抽象类,那么就肯定有具体的实现类;
如果只讨论wifi的话,就是packages/modules/Wifi/service/java/com/android/server/wifi/WifiNetworkAgent.java

public class WifiNetworkAgent extends NetworkAgent {
    public WifiNetworkAgent(
            @NonNull Context context,
            @NonNull Looper looper,
            @NonNull NetworkCapabilities nc,
            @NonNull LinkProperties lp,
            @NonNull NetworkAgentConfig config,
            @Nullable NetworkProvider provider,
            @NonNull Callback wifiNetworkAgentCallback) {
        super(context, looper, TAG, nc, lp, ConnectedScore.WIFI_INITIAL_SCORE, config, provider);
        mCurrentNetworkCapabilities = nc;
        mCallback = wifiNetworkAgentCallback;
        register();
    }
}

构造方法中就有register方法;
哪里有相关应用呢?
比如packages/modules/Wifi/service/java/com/android/server/wifi/ClientModeImpl.java中;

public class ClientModeImpl extends StateMachine implements ClientMode {
    class L2ConnectedState extends State {

        @Override
        public void enter() {
            //...略
            mNetworkAgent = mWifiInjector.makeWifiNetworkAgent(nc, mLinkProperties, naConfig,
                    mNetworkFactory.getProvider(), new WifiNetworkAgentCallback());
			//...略
        }
        //...略
    }
}

ClientModeImpl也是一个状态机,根据其构造方法中添加的各类状态来看,它关系着wifi的连接以及相关配置;

        addState(mConnectableState); {
            addState(mConnectingOrConnectedState, mConnectableState); {
                addState(mL2ConnectingState, mConnectingOrConnectedState);
                addState(mL2ConnectedState, mConnectingOrConnectedState); {
                    addState(mL3ProvisioningState, mL2ConnectedState);
                    addState(mL3ConnectedState, mL2ConnectedState);
                    addState(mRoamingState, mL2ConnectedState);
                }
            }
            addState(mDisconnectedState, mConnectableState);
        }

        setInitialState(mDisconnectedState);

具体的状态转换不再赘述,理解大致脉络即可。

总结时序

照例来张时序图梳理一下;
连接受限时序
Frameworks里的东西就这样,牵扯太广,凑合着看,其实只要有个模糊的概念就行;
主要是要习惯并掌握追溯流程的方法。
以上。

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

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

相关文章

GRE 隧道协议

1.GRE协议简介 GRE&#xff08;General Routing Encapsulation &#xff0c;通用路由封装&#xff09;是对某些网络层协议(如IP和IPX)的数据报文进行封装&#xff0c;使这些被封装的报文能够在另一网络层协议(如IP)中传输。此外 GRE协议也可以作为VPN的第三层隧道协议连接两个…

ES6之迭代器

文章目录 前言迭代器1.原生具备Iterator接口的数据&#xff08;可用for...of遍历&#xff09;2.工作原理3.自定义遍历数据 总结 前言 迭代器&#xff08;Iterator&#xff09; for…of遍历 迭代器 迭代器是一种接口&#xff0c;为各种不同数据结构提供统一的访问机制。任何数…

c++ 11标准模板(STL) std::vector (八)

定义于头文件 <vector> template< class T, class Allocator std::allocator<T> > class vector;(1)namespace pmr { template <class T> using vector std::vector<T, std::pmr::polymorphic_allocator<T>>; }(2)(C17…

智慧工地烟火识别算法 opencv

智慧工地烟火识别系统应用pythonopencv深度学习算法模型技术分析前端视频信息&#xff0c;智慧工地烟火识别算法模型主动发现工地或者厂区现场区域内的烟雾和火灾苗头及时进行告警。OpenCV的全称是Open Source Computer Vision Library&#xff0c;是一个跨平台的计算机视觉处理…

前端三剑客 - HTML

前言 前面都是一些基础的铺垫&#xff0c;现在就正式进入到web开发环节了。 我们的目标就是通过学习 JavaEE初阶&#xff0c;搭建出一个网站出来。 一个网站分成两个部分&#xff1a; 前端&#xff08;客户端&#xff09; 后端&#xff08;服务器&#xff09; 通常这里的客户端…

ASP.NET Core Web API用户身份验证

一、JWT介绍 ASP.NET Core Web API用户身份验证的方法有很多&#xff0c;本文只介绍JWT方法。JWT实现了服务端无状态&#xff0c;在分布式服务、会话一致性、单点登录等方面凸显优势&#xff0c;不占用服务端资源。简单来说&#xff0c;JWT的验证过程如下所示&#xff1a; &a…

基于微服务架构的水果销售系统的设计与实现

访问【WRITE-BUG数字空间】_[内附完整源码和文档] 整体上为微服务架构&#xff0c;使用 SpringCloud 技术&#xff0c;每个独立的服务为一个单独的 SpringBoot 工程&#xff1b;数据库使用 MySQL 数据库&#xff1b;分布式缓存使用 Redis&#xff0c;消息队列使用 Kafka。包括…

基于matlab的相控阵系统仿真场景可视化

一、前言 此示例演示如何使用方案查看器可视化系统级仿真。 二、介绍 相控阵系统仿真通常包括许多移动物体。例如&#xff0c;阵列和目标都可以处于运动状态。此外&#xff0c;每个移动物体可能都有自己的方向&#xff0c;因此当模拟中出现更多玩家时&#xff0c;簿记变得越来越…

是人就能学会的Spring源码教学-Spring的简单使用

是人就能学会的Spring源码教学-Spring的简单使用 Spring的最简单入门使用第一步 创建项目第二步 配置项目第三步 启动项目 Spring的最简单入门使用 各位道友且跟我一道来学习Spring的最简单的入门使用&#xff0c;为了方便和简单&#xff0c;我使用了Spring Boot项目&#xff…

解决NixOS在Vmware中无法自适应显示缩放问题

解决NixOS在Vmware中无法自适应显示缩放问题 此方法同样适用于所有虚拟机&#xff0c;主要解决的是 虚拟机界面显示无法自适应操作虚拟机时&#xff0c;过渡动画卡顿看视频时&#xff0c;分辨率不高&#xff0c;伴随卡顿 起因 在为 NixOS安 装完 Vmware Tools 后&#xff0c;…

2023年最新水果DAW编曲软件fl studio21 macOS - 21.0.3.3036简体中文版免费下载支持苹果M1/M2处理器

一直梦想制作自己的音乐(无论是作为一名制作人还是艺术家)&#xff0c;你可能会想你出生在这个时代是你的幸运星。这个水果圈工作室和上一版之间的改进水平确实令人钦佩。这仅仅是FL Studio 21所提供的皮毛。你的音乐项目的选择真的会让你大吃一惊。你以前从未有过这样的多才多…

【LeetCode】《LeetCode 101》第七章:动态规划

文章目录 7.1 算法解释7.2 基本动态规划&#xff1a;一维70. 爬楼梯&#xff08;简单&#xff09;198.打家劫舍&#xff08;中等&#xff09;413. 等差数列划分&#xff08;中等&#xff09; 7.3 基本动态规划&#xff1a;二维64. 最小路径和&#xff08;中等&#xff09;542. …

【项目经理】论项目经理的自我修养

项目经理的非职权领导力 文章目录 项目经理的非职权领导力一、权利的类型二、构成权利的三要素三、沟通是实施影响力的重要手段3.1 沟通的主要类型3.2 沟通的内容和形式3.3 沟通的主要困难 四、综合沟通协调的技巧4.1 常见的负面反馈4.2 沟通技巧 五、论项目经理的自我修养5.1 …

PyCharm2023.1下载、安装、注册以及简单使用【全过程讲解】

在使用PyCharm IDE之前&#xff0c;请确保自己的计算机里面安装了Python解释器环境&#xff0c;若没有下载和安装可以看看我之前的文章>>>Python环境设置>>>或者还可以观看视频讲解。 注意&#xff1a;本文软件的配置方式仅供个人学习使用&#xff0c;如有侵…

如何将PDF文件转换为Excel表格?这两个方法方便实用!

如何将PDF文件转换为Excel表格&#xff1f; 很多人在编辑和处理表格内容时&#xff0c;需要将PDF文件转换为Excel表格&#xff0c;以更好地修改和排版。虽然PDF文件往往起到展示整体效果的作用&#xff0c;但是PDF转Excel也是办公中老生常谈的文档处理操作。如果您还不知道如何…

Java架构中VO、DTO、DO、BO的区别与联系(超详解)

VO、DTO、DO、BO的区别与联系 前言一、概念1、VO (View Object)2、DTO(Data Transfer Object)3、DO(Data Object)4、BO&#xff08;Business Object&#xff09; 二、为什么会存在Vo&#xff1f;三、总结 前言 本博主将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知识…

深入理解 node 中的文件流

为什么要使用文件流 想象这样一个场景&#xff0c;我要处理一个 10G 的文件&#xff0c;但我的内存大小只有 2G&#xff0c;该怎么办&#xff1f; 我们可以分 5 次读取文件&#xff0c;每次只读取 2G 的数据&#xff0c;这样就可以解决这个问题&#xff0c;那么这个分段读取的过…

HTML基本标签介绍

HTML的基本认识&#xff01; 文章目录 HTML基本标签介绍1. HTML是什么&#xff1f;1.1 HTML代码的样子1.2 HTML文件的展示1.3 VSCode配置 2. HTML常用标签介绍2.1 注释标签2.2 标题标签2.3 段落标签2.4 换行标签2.5 格式化标签2.6 图片标签2.7 超链接标签2.8 表格标签2.9 列表标…

Windows下 ffmpeg 的 “Protocol not found“ 的解决

文章目录 1. 问题描述2. 排查方法记录2.1 检查代码中编码器是否安装2.2 确定ffmpeg版本号2.3 打印编译参数2.4 查看运行中调用dll 1. 问题描述 调用ffmpeg库中&#xff0c;如果使用 avformat_open_input 打开返回 -1330794744,使用 av_strerror char buf[1024]{0};int result …

JavaScript高阶项目—组件化的可编辑表格

1. 任务要求 JSON数据,表格中数据来自服务端&#xff0c;由JSON格式表示。通过JSON数据生成可编辑表格&#xff0c;并且灵活配置可编辑得到数据列。输入数据时打开开发者模式有提示&#xff0c;并且设置判断&#xff0c;要求输入正确的成绩。要求表格的可编辑列&#xff0c;计…