Android 9.0 网络之netd详解

一、DHCP流程

分析netd之前先了解一下网络自动获取IP流程,借鉴下图流程查看代码:

(1)WIFI扫描到可用网络后进行连接,代码路径:\frameworks\opt\net\wifi\service\java\com\android\server\wifi\WifiStateMachine.java

复制代码
                case WifiMonitor.NETWORK_CONNECTION_EVENT:
                    if (mVerboseLoggingEnabled) log("Network connection established");
                    mLastNetworkId = message.arg1;
                    mWifiConfigManager.clearRecentFailureReason(mLastNetworkId);
                    mLastBssid = (String) message.obj;
                    reasonCode = message.arg2;
                    // TODO: This check should not be needed after WifiStateMachinePrime refactor.
                    // Currently, the last connected network configuration is left in
                    // wpa_supplicant, this may result in wpa_supplicant initiating connection
                    // to it after a config store reload. Hence the old network Id lookups may not
                    // work, so disconnect the network and let network selector reselect a new
                    // network.
                    config = getCurrentWifiConfiguration();
                    if (config != null) {
                        mWifiInfo.setBSSID(mLastBssid);
                        mWifiInfo.setNetworkId(mLastNetworkId);
                        mWifiInfo.setMacAddress(mWifiNative.getMacAddress(mInterfaceName));

                        ScanDetailCache scanDetailCache =
                                mWifiConfigManager.getScanDetailCacheForNetwork(config.networkId);
                        if (scanDetailCache != null && mLastBssid != null) {
                            ScanResult scanResult = scanDetailCache.getScanResult(mLastBssid);
                            if (scanResult != null) {
                                mWifiInfo.setFrequency(scanResult.frequency);
                            }
                        }
                        mWifiConnectivityManager.trackBssid(mLastBssid, true, reasonCode);
                        // We need to get the updated pseudonym from supplicant for EAP-SIM/AKA/AKA'
                        if (config.enterpriseConfig != null
                                && TelephonyUtil.isSimEapMethod(
                                        config.enterpriseConfig.getEapMethod())) {
                            String anonymousIdentity =
                                    mWifiNative.getEapAnonymousIdentity(mInterfaceName);
                            if (anonymousIdentity != null) {
                                config.enterpriseConfig.setAnonymousIdentity(anonymousIdentity);
                            } else {
                                Log.d(TAG, "Failed to get updated anonymous identity"
                                        + " from supplicant, reset it in WifiConfiguration.");
                                config.enterpriseConfig.setAnonymousIdentity(null);
                            }
                            mWifiConfigManager.addOrUpdateNetwork(config, Process.WIFI_UID);
                        }
                        sendNetworkStateChangeBroadcast(mLastBssid);
                        transitionTo(mObtainingIpState);
                    } else {
                        logw("Connected to unknown networkId " + mLastNetworkId
                                + ", disconnecting...");
                        sendMessage(CMD_DISCONNECT);
                    }
                    break;

其中 transitionTo(mObtainingIpState) 即调用如下,根据当前wifi配置文件信息,进行自动或静态IP配置:

复制代码
    class ObtainingIpState extends State {
        @Override
        public void enter() {
            final WifiConfiguration currentConfig = getCurrentWifiConfiguration();
            final boolean isUsingStaticIp =
                    (currentConfig.getIpAssignment() == IpConfiguration.IpAssignment.STATIC);
            if (mVerboseLoggingEnabled) {
                final String key = currentConfig.configKey();
                log("enter ObtainingIpState netId=" + Integer.toString(mLastNetworkId)
                        + " " + key + " "
                        + " roam=" + mIsAutoRoaming
                        + " static=" + isUsingStaticIp);
            }

            // Send event to CM & network change broadcast
            setNetworkDetailedState(DetailedState.OBTAINING_IPADDR);

            // We must clear the config BSSID, as the wifi chipset may decide to roam
            // from this point on and having the BSSID specified in the network block would
            // cause the roam to fail and the device to disconnect.
            clearTargetBssid("ObtainingIpAddress");

            // Stop IpClient in case we're switching from DHCP to static
            // configuration or vice versa.
            //
            // TODO: Only ever enter this state the first time we connect to a
            // network, never on switching between static configuration and
            // DHCP. When we transition from static configuration to DHCP in
            // particular, we must tell ConnectivityService that we're
            // disconnected, because DHCP might take a long time during which
            // connectivity APIs such as getActiveNetworkInfo should not return
            // CONNECTED.
            stopIpClient();

            mIpClient.setHttpProxy(currentConfig.getHttpProxy());
            if (!TextUtils.isEmpty(mTcpBufferSizes)) {
                mIpClient.setTcpBufferSizes(mTcpBufferSizes);
            }
            final IpClient.ProvisioningConfiguration prov;
            if (!isUsingStaticIp) {
                prov = IpClient.buildProvisioningConfiguration()
                            .withPreDhcpAction()
                            .withApfCapabilities(mWifiNative.getApfCapabilities(mInterfaceName))
                            .withNetwork(getCurrentNetwork())
                            .withDisplayName(currentConfig.SSID)
                            .withRandomMacAddress()
                            .build();
            } else {
                StaticIpConfiguration staticIpConfig = currentConfig.getStaticIpConfiguration();
                prov = IpClient.buildProvisioningConfiguration()
                            .withStaticConfiguration(staticIpConfig)
                            .withApfCapabilities(mWifiNative.getApfCapabilities(mInterfaceName))
                            .withNetwork(getCurrentNetwork())
                            .withDisplayName(currentConfig.SSID)
                            .build();
            }
            mIpClient.startProvisioning(prov);
           // Get Link layer stats so as we get fresh tx packet counters
            getWifiLinkLayerStats();
        }

        @Override
        public boolean processMessage(Message message) {
            logStateAndMessage(message, this);

            switch(message.what) {
                case CMD_START_CONNECT:
                case CMD_START_ROAM:
                    messageHandlingStatus = MESSAGE_HANDLING_STATUS_DISCARD;
                    break;
                case WifiManager.SAVE_NETWORK:
                    messageHandlingStatus = MESSAGE_HANDLING_STATUS_DEFERRED;
                    deferMessage(message);
                    break;
                case WifiMonitor.NETWORK_DISCONNECTION_EVENT:
                    reportConnectionAttemptEnd(
                            WifiMetrics.ConnectionEvent.FAILURE_NETWORK_DISCONNECTION,
                            WifiMetricsProto.ConnectionEvent.HLF_NONE);
                    return NOT_HANDLED;
                case CMD_SET_HIGH_PERF_MODE:
                    messageHandlingStatus = MESSAGE_HANDLING_STATUS_DEFERRED;
                    deferMessage(message);
                    break;
                default:
                    return NOT_HANDLED;
            }
            return HANDLED;
        }
    }

将状态设为DetailedState.OBTAINING_IPADDR,初始化好配置后会调用IpClient的startProvisioning

复制代码
    public void startProvisioning(ProvisioningConfiguration req) {
        if (!req.isValid()) {
            doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
            return;
        }
 
        mInterfaceParams = mDependencies.getInterfaceParams(mInterfaceName);
        if (mInterfaceParams == null) {
            logError("Failed to find InterfaceParams for " + mInterfaceName);
            doImmediateProvisioningFailure(IpManagerEvent.ERROR_INTERFACE_NOT_FOUND);
            return;
        }
 
        mCallback.setNeighborDiscoveryOffload(true);
        sendMessage(CMD_START, new ProvisioningConfiguration(req));
    }

IPClient的准备工作是随配置发出CMD_START的消息,进一步执行到RunningState 方法,enter方法区别是配置了ipv6还是ipv4走不同的流程,现在默认一般是ipv4。

复制代码
    class RunningState extends State {
        private ConnectivityPacketTracker mPacketTracker;
        private boolean mDhcpActionInFlight;

        @Override
        public void enter() {
            ApfFilter.ApfConfiguration apfConfig = new ApfFilter.ApfConfiguration();
            apfConfig.apfCapabilities = mConfiguration.mApfCapabilities;
            apfConfig.multicastFilter = mMulticastFiltering;
            // Get the Configuration for ApfFilter from Context
            apfConfig.ieee802_3Filter =
                    mContext.getResources().getBoolean(R.bool.config_apfDrop802_3Frames);
            apfConfig.ethTypeBlackList =
                    mContext.getResources().getIntArray(R.array.config_apfEthTypeBlackList);
            mApfFilter = ApfFilter.maybeCreate(mContext, apfConfig, mInterfaceParams, mCallback);
            // TODO: investigate the effects of any multicast filtering racing/interfering with the
            // rest of this IP configuration startup.
            if (mApfFilter == null) {
                mCallback.setFallbackMulticastFilter(mMulticastFiltering);
            }

            mPacketTracker = createPacketTracker();
            if (mPacketTracker != null) mPacketTracker.start(mConfiguration.mDisplayName);

            if (mConfiguration.mEnableIPv6 && !startIPv6()) {
                doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV6);
                transitionTo(mStoppingState);
                return;
            }

            if (mConfiguration.mEnableIPv4 && !startIPv4()) {
                doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV4);
                transitionTo(mStoppingState);
                return;
            }

            final InitialConfiguration config = mConfiguration.mInitialConfig;
            if ((config != null) && !applyInitialConfig(config)) {
                // TODO introduce a new IpManagerEvent constant to distinguish this error case.
                doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
                transitionTo(mStoppingState);
                return;
            }

            if (mConfiguration.mUsingMultinetworkPolicyTracker) {
                mMultinetworkPolicyTracker = new MultinetworkPolicyTracker(
                        mContext, getHandler(),
                        () -> { mLog.log("OBSERVED AvoidBadWifi changed"); });
                mMultinetworkPolicyTracker.start();
            }

            if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) {
                doImmediateProvisioningFailure(
                        IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR);
                transitionTo(mStoppingState);
                return;
            }
        }

看下startIPv4逻辑:

复制代码
    private boolean startIPv4() {
        // If we have a StaticIpConfiguration attempt to apply it and
        // handle the result accordingly.
        if (mConfiguration.mStaticIpConfig != null) {
            if (mInterfaceCtrl.setIPv4Address(mConfiguration.mStaticIpConfig.ipAddress)) {
                handleIPv4Success(new DhcpResults(mConfiguration.mStaticIpConfig));
            } else {
                return false;
            }
        } else {
            // Start DHCPv4.
            mDhcpClient = DhcpClient.makeDhcpClient(mContext, IpClient.this, mInterfaceParams);
            mDhcpClient.registerForPreDhcpNotification();
            mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP);
        }

        return true;
    }

DhcpClient也处理CMD_START_DHCP开始状态变更,稍微等一下状态切换会回到mDhcpInitState开始。

复制代码
    class StoppedState extends State {
        @Override
        public boolean processMessage(Message message) {
            switch (message.what) {
                case CMD_START_DHCP:
                    if (mRegisteredForPreDhcpNotification) {
                        transitionTo(mWaitBeforeStartState);
                    } else {
                        transitionTo(mDhcpInitState);
                    }
                    return HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }
    }
 
    private State mWaitBeforeStartState = new WaitBeforeStartState(mDhcpInitState);
 
 
    // Sends CMD_PRE_DHCP_ACTION to the controller, waits for the controller to respond with
    // CMD_PRE_DHCP_ACTION_COMPLETE, and then transitions to mOtherState.
    abstract class WaitBeforeOtherState extends LoggingState {
        protected State mOtherState;
 
        @Override
        public void enter() {
            super.enter();
            mController.sendMessage(CMD_PRE_DHCP_ACTION);
        }
 
        @Override
        public boolean processMessage(Message message) {
            super.processMessage(message);
            switch (message.what) {
                case CMD_PRE_DHCP_ACTION_COMPLETE:
                    transitionTo(mOtherState);
                    return HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }
    }

IPClient处理DhcpClient发来的CMD_CONFIGURE_LINKADDRESS

复制代码
                case DhcpClient.CMD_CONFIGURE_LINKADDRESS: {
                    final LinkAddress ipAddress = (LinkAddress) msg.obj;
                    if (mInterfaceCtrl.setIPv4Address(ipAddress)) {
                        mDhcpClient.sendMessage(DhcpClient.EVENT_LINKADDRESS_CONFIGURED);
                    } else {
                        logError("Failed to set IPv4 address.");
                        dispatchCallback(ProvisioningChange.LOST_PROVISIONING,
                                new LinkProperties(mLinkProperties));
                        transitionTo(mStoppingState);
                    }
                    break;
                }

DhcpClient切换到mWaitBeforeStartState,这是由于之前有调用mDhcpClient.registerForPreDhcpNotification();所以这边状态等待其他状态完成,其实是dhcp有些准备工作需要在WifiStateMachine中完成,所以这边流程需要等一下,流程完了自然会切换到DhcpInitState。

复制代码
    class DhcpInitState extends PacketRetransmittingState {
        public DhcpInitState() {
            super();
        }
 
        @Override
        public void enter() {
            super.enter();
            startNewTransaction();
            mLastInitEnterTime = SystemClock.elapsedRealtime();
        }
 
        protected boolean sendPacket() {
            return sendDiscoverPacket();
        }
 
        protected void receivePacket(DhcpPacket packet) {
            if (!isValidPacket(packet)) return;
            if (!(packet instanceof DhcpOfferPacket)) return;
            mOffer = packet.toDhcpResults();
            if (mOffer != null) {
                Log.d(TAG, "Got pending lease: " + mOffer);
                transitionTo(mDhcpRequestingState);
            }
        }
    }

DHCPREQUEST收到回应,向IpClient发出CMD_POST_DHCP_ACTION消息。

复制代码
    private void notifySuccess() {
        mController.sendMessage(
                CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, new DhcpResults(mDhcpLease));
    }
 
 
    private void acceptDhcpResults(DhcpResults results, String msg) {
        mDhcpLease = results;
        mOffer = null;
        Log.d(TAG, msg + " lease: " + mDhcpLease);
        notifySuccess();
    }
 
    class DhcpRequestingState extends PacketRetransmittingState {
        public DhcpRequestingState() {
            mTimeout = DHCP_TIMEOUT_MS / 2;
        }
 
        protected boolean sendPacket() {
            return sendRequestPacket(
                    INADDR_ANY,                                    // ciaddr
                    (Inet4Address) mOffer.ipAddress.getAddress(),  // DHCP_REQUESTED_IP
                    (Inet4Address) mOffer.serverAddress,           // DHCP_SERVER_IDENTIFIER
                    INADDR_BROADCAST);                             // packet destination address
        }
 
        protected void receivePacket(DhcpPacket packet) {
            if (!isValidPacket(packet)) return;
            if ((packet instanceof DhcpAckPacket)) {
                DhcpResults results = packet.toDhcpResults();
                if (results != null) {
                    setDhcpLeaseExpiry(packet);
                    acceptDhcpResults(results, "Confirmed");
                    transitionTo(mConfiguringInterfaceState);
                }
            } else if (packet instanceof DhcpNakPacket) {
                // TODO: Wait a while before returning into INIT state.
                Log.d(TAG, "Received NAK, returning to INIT");
                mOffer = null;
                transitionTo(mDhcpInitState);
            }
        }

IpClient进行处理:

复制代码
                case DhcpClient.CMD_POST_DHCP_ACTION:
                    stopDhcpAction();
 
                    switch (msg.arg1) {
                        case DhcpClient.DHCP_SUCCESS:
                            handleIPv4Success((DhcpResults) msg.obj);
                            break;
                        case DhcpClient.DHCP_FAILURE:
                            handleIPv4Failure();
                            break;
                        default:
                            logError("Unknown CMD_POST_DHCP_ACTION status: %s", msg.arg1);
                    }
                    break;

回调WifiStateMachine的callback:

复制代码
        @Override
        public void onProvisioningSuccess(LinkProperties newLp) {
            mWifiMetrics.logStaEvent(StaEvent.TYPE_CMD_IP_CONFIGURATION_SUCCESSFUL);
            sendMessage(CMD_UPDATE_LINKPROPERTIES, newLp);
            sendMessage(CMD_IP_CONFIGURATION_SUCCESSFUL);
        }

ip配置成功后设置网络状态变为connected并发送广播通知,最后状态切到ConnectedState。

二、netd工作原理详解

NETD是Android一个专门管理网络链接, 路由/带宽/防火墙策略以及iptables的系统Daemon进程, 其在Anroid系统启动时加载:

复制代码
service netd /system/bin/netd
    class main
    socket netd stream 0660 root system
    socket dnsproxyd stream 0660 root inet
    socket mdns stream 0660 root system
    socket fwmarkd stream 0660 root inet
    onrestart restart zygote
    onrestart restart zygote_secondary

启动netd时, 会创建四个socket,用于其他进程与netd进行通信:

  • netd: 主要与Framework的NetworkManagementService交互, 用于控制网口状态, 路由表
  • dnsproxyd: DNS代理的控制与配置,用于私有DNS(DNS Over TLS)的请求转发
  • mdns: 多播DNS(Multicast DNS,参考RFCRFC 6762 - Multicast DNS), 用于基于WIFI连接的服务发现(NSD, Network Service Discovery)
  • fwmarkd: iptables的(fwmark)策略路由的配置(策略路由, 如设置网络权限, 连接打标签等

总的说来, netd进程在Android中间层服务NetworkManagementService以及内核之间建立了一个沟通的桥梁。

1.netd的启动与初始化

netd进程启动时, 主要处理做以下事情:

  • 创建一个NetlinkManager, 用于管理与内核通信的netlink连接
  • 初始化网络控制类, 如路由控制RouteController, 带宽控制BandwidthController
  • 启动各类事件监听类: DnsProxyListener监听DNS代理; CommandListener监听来自NetworkManagement的指令
  • 启动NetdHwService, 为HAL层提供接口
复制代码
int main() {
    using android::net::gCtls;
    Stopwatch s;

    ALOGI("Netd 1.0 starting");
    remove_pid_file();

    blockSigpipe();

    // Before we do anything that could fork, mark CLOEXEC the UNIX sockets that we get from init.
    // FrameworkListener does this on initialization as well, but we only initialize these
    // components after having initialized other subsystems that can fork.
    for (const auto& sock : { CommandListener::SOCKET_NAME,
                              DnsProxyListener::SOCKET_NAME,
                              FwmarkServer::SOCKET_NAME,
                              MDnsSdListener::SOCKET_NAME }) {
        setCloseOnExec(sock);
    }

    NetlinkManager *nm = NetlinkManager::Instance();
    if (nm == nullptr) {
        ALOGE("Unable to create NetlinkManager");
        exit(1);
    };

    gCtls = new android::net::Controllers();
    gCtls->init();

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

    if (nm->start()) {
        ALOGE("Unable to start NetlinkManager (%s)", strerror(errno));
        exit(1);
    }

    std::unique_ptr<NFLogListener> logListener;
    {
        auto result = makeNFLogListener();
        if (!isOk(result)) {
            ALOGE("Unable to create NFLogListener: %s", toString(result).c_str());
            exit(1);
        }
        logListener = std::move(result.value());
        auto status = gCtls->wakeupCtrl.init(logListener.get());
        if (!isOk(result)) {
            ALOGE("Unable to init WakeupController: %s", toString(result).c_str());
            // We can still continue without wakeup packet logging.
        }
    }

    // Set local DNS mode, to prevent bionic from proxying
    // back to this service, recursively.
    setenv("ANDROID_DNS_MODE", "local", 1);
    DnsProxyListener dpl(&gCtls->netCtrl, &gCtls->eventReporter);
    if (dpl.startListener()) {
        ALOGE("Unable to start DnsProxyListener (%s)", strerror(errno));
        exit(1);
    }

    MDnsSdListener mdnsl;
    if (mdnsl.startListener()) {
        ALOGE("Unable to start MDnsSdListener (%s)", strerror(errno));
        exit(1);
    }

    FwmarkServer fwmarkServer(&gCtls->netCtrl, &gCtls->eventReporter, &gCtls->trafficCtrl);
    if (fwmarkServer.startListener()) {
        ALOGE("Unable to start FwmarkServer (%s)", strerror(errno));
        exit(1);
    }

    Stopwatch subTime;
    status_t ret;
    if ((ret = NetdNativeService::start()) != android::OK) {
        ALOGE("Unable to start NetdNativeService: %d", ret);
        exit(1);
    }
    ALOGI("Registering NetdNativeService: %.1fms", subTime.getTimeAndReset());

    /*
     * Now that we're up, we can respond to commands. Starting the listener also tells
     * NetworkManagementService that we are up and that our binder interface is ready.
     */
    if (cl.startListener()) {
        ALOGE("Unable to start CommandListener (%s)", strerror(errno));
        exit(1);
    }
    ALOGI("Starting CommandListener: %.1fms", subTime.getTimeAndReset());

    write_pid_file();

    // Now that netd is ready to process commands, advertise service
    // availability for HAL clients.
    NetdHwService mHwSvc;
    if ((ret = mHwSvc.start()) != android::OK) {
        ALOGE("Unable to start NetdHwService: %d", ret);
        exit(1);
    }
    ALOGI("Registering NetdHwService: %.1fms", subTime.getTimeAndReset());

    ALOGI("Netd started in %dms", static_cast<int>(s.timeTaken()));

    IPCThreadState::self()->joinThreadPool();

    ALOGI("Netd exiting");

    remove_pid_file();

    exit(0);
}

CommandListener用于接收处理来自上层NetworkManagementService指令, 在netd启动时, 会监听netd这个socket, 并允许最多4个客户端请求的处理,netd启动完成后, 就可以处理来自中间层的指令请求以及与内核进行交互了。

2 .netd与NetworkManagerService的交互

SystemServer进程启动时, 创建NetworkManagementService(以下简称(NMS)), 此时NMS会主动与netd建立socket链接:

复制代码
// SystemServer.java
       if (!disableNetwork) {
           traceBeginAndSlog("StartNetworkManagementService");
           try {
               networkManagement = NetworkManagementService.create(context);
               ServiceManager.addService(Context.NETWORKMANAGEMENT_SERVICE, networkManagement);
           } catch (Throwable e) {
               reportWtf("starting NetworkManagement Service", e);
           }
           traceEnd();
       }

创建NMS时, 启动一个新的线程用于与netd通信,

复制代码
 static NetworkManagementService create(Context context, String socket)
        throws InterruptedException {
    final NetworkManagementService service = new NetworkManagementService(context, socket);
    final CountDownLatch connectedSignal = service.mConnectedSignal;
    if (DBG) Slog.d(TAG, "Creating NetworkManagementService");
    service.mThread.start();
    if (DBG) Slog.d(TAG, "Awaiting socket connection");
    connectedSignal.await();
    service.connectNativeNetdService();

    return service;
    }
    

       private NetworkManagementService(Context context, String socket) {
    mContext = context;

    // make sure this is on the same looper as our NativeDaemonConnector for sync purposes
    mFgHandler = new Handler(FgThread.get().getLooper());

    // Don't need this wake lock, since we now have a time stamp for when
    // the network actually went inactive.  (It might be nice to still do this,
    // but I don't want to do it through the power manager because that pollutes the
    // battery stats history with pointless noise.)
    //PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
    PowerManager.WakeLock wl = null; //pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, NETD_TAG);

    mConnector = new NativeDaemonConnector(
        new NetdCallbackReceiver(), socket, 10, NETD_TAG, 160, wl,
        FgThread.get().getLooper());
    mThread = new Thread(mConnector, NETD_TAG);

    mDaemonHandler = new Handler(FgThread.get().getLooper());

    // Add ourself to the Watchdog monitors.
    Watchdog.getInstance().addMonitor(this);

    LocalServices.addService(NetworkManagementInternal.class, new LocalService());

    synchronized (mTetheringStatsProviders) {
        mTetheringStatsProviders.put(new NetdTetheringStatsProvider(), "netd");
    }
}

NMS通过NativeDaemonConnectornetd建立socket通信, NativeDaemonConnector主要做两个事情:

  • netd建立一个数据链接

  • 不断读取socket中的数据流: 一种是netd主动上报的命令, 一种是NMS发送给netd后的指令的响应

    复制代码
    @Override
    public void run() {
        mCallbackHandler = new Handler(mLooper, this);
    
        while (true) {
            try {
                listenToSocket();
            } catch (Exception e) {
                loge("Error in NativeDaemonConnector: " + e);
                SystemClock.sleep(5000);
            }
        }
    }
    
    
        private void listenToSocket() throws IOException {
        LocalSocket socket = null;
    
        try {
            socket = new LocalSocket();
            LocalSocketAddress address = determineSocketAddress();
    
            socket.connect(address);
    
            InputStream inputStream = socket.getInputStream();
            synchronized (mDaemonLock) {
                mOutputStream = socket.getOutputStream();
            }
    
            mCallbacks.onDaemonConnected();
    
            FileDescriptor[] fdList = null;
            byte[] buffer = new byte[BUFFER_SIZE];
            int start = 0;
    
            while (true) {
                int count = inputStream.read(buffer, start, BUFFER_SIZE - start);
                if (count < 0) {
                    loge("got " + count + " reading with start = " + start);
                    break;
                }
                fdList = socket.getAncillaryFileDescriptors();
    
                // Add our starting point to the count and reset the start.
                count += start;
                start = 0;
    
                for (int i = 0; i < count; i++) {
                    if (buffer[i] == 0) {
                        // Note - do not log this raw message since it may contain
                        // sensitive data
                        final String rawEvent = new String(
                                buffer, start, i - start, StandardCharsets.UTF_8);
    
                        boolean releaseWl = false;
                        try {
                            final NativeDaemonEvent event =
                                    NativeDaemonEvent.parseRawEvent(rawEvent, fdList);
    
                            log("RCV <- {" + event + "}");
    
                            if (event.isClassUnsolicited()) {
                                Message msg = mCallbackHandler.obtainMessage(
                                        event.getCode(), uptimeMillisInt(), 0, event.getRawEvent());
                                if (mCallbackHandler.sendMessage(msg)) {
                                    releaseWl = false;
                                }
                            } else {
                                mResponseQueue.add(event.getCmdNumber(), event);
                            }
                        } catch (IllegalArgumentException e) {
                            log("Problem parsing message " + e);
                        } finally {
                            if (releaseWl) {
                                mWakeLock.release();
                            }
                        }
    
                        start = i + 1;
                    }
                }
    
                // We should end at the amount we read. If not, compact then
                // buffer and read again.
                if (start != count) {
                    final int remaining = BUFFER_SIZE - start;
                    System.arraycopy(buffer, start, buffer, 0, remaining);
                    start = remaining;
                } else {
                    start = 0;
                }
            }
        } catch (IOException ex) {
            loge("Communications error: " + ex);
            throw ex;
        } finally {
            synchronized (mDaemonLock) {
                if (mOutputStream != null) {
                    try {
                        loge("closing stream for " + mSocket);
                        mOutputStream.close();
                    } catch (IOException e) {
                        loge("Failed closing output stream: " + e);
                    }
                    mOutputStream = null;
                }
            }
    
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException ex) {
                loge("Failed closing socket: " + ex);
            }
        }
    }

    socket链接建立完成之后, NMS与netd可以相互通信, 发送指令与数据了. NMS通过NativeDaemonConnector执行相应的指令, 比如NMS设置网络接口的配置(打开/关闭网口):

复制代码
@Override
public void setInterfaceConfig(String iface, InterfaceConfiguration cfg) {
    mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
    LinkAddress linkAddr = cfg.getLinkAddress();
    if (linkAddr == null || linkAddr.getAddress() == null) {
        throw new IllegalStateException("Null LinkAddress given");
    }

    final Command cmd = new Command("interface", "setcfg", iface,
            linkAddr.getAddress().getHostAddress(),
            linkAddr.getPrefixLength());
    for (String flag : cfg.getFlags()) {
        cmd.appendArg(flag);
    }

    try {
        mConnector.execute(cmd);
    } catch (NativeDaemonConnectorException e) {
        throw e.rethrowAsParcelableException();
    }

NativeDaemonConnector会将每个指令都指定一个唯一的序列, 并将其响应放到一个阻塞队列, 等待netd返回指令的结果, 如果超过指定的超时时间, 则抛出一个超时的异常.

在第一部分时, 讲到SocketListener拿到上层发过来的指令后, 会将其分发给对应的指令类进行处理(看SocketListener的子类FrameworkListener):

3.NETD与内核进行交互

NETD通过netlink事件与内核进行消息的交换.在第一部分时看到, netd启动时, 会配置socket与内核进行通信:

  • netlink事件NETLINK_KOBJECT_UEVENT: 用于内核向netd发生消息, 如网口的状态变化;

  • netlink事件NETLINK_ROUTE:用于接收路由信息, 如路由表的更新与删除;

  • netlink事件NETLINK_NFLOG:用于接收数据流量使用配额的消息, 如数据使用超限;

  • netlink事件NETLINK_NETFILTER用于接收包过滤(netfilter)的消息;

    复制代码
    int NetlinkManager::start() {
        if ((mUeventHandler = setupSocket(&mUeventSock, NETLINK_KOBJECT_UEVENT,
         0xffffffff, NetlinkListener::NETLINK_FORMAT_ASCII, false)) == NULL) {
        return -1;
        }
    
        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;
        }
    
        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.
        }
    
        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;
    }

    每一个netlink的socket都会新建一个NetlinkHandler, 用于处理内核的消息, 并将该消息广播给上层:

    复制代码
    void NetlinkHandler::onEvent(NetlinkEvent *evt) {
        const char *subsys = evt->getSubsystem();
        if (!subsys) {
        ALOGW("No subsystem found in netlink event");
        return;
        }
    
        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) {
            notifyInterfaceLinkChanged(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")) {
        const 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);
        }
    }

    监听内核网络状态并回调:

    复制代码
        private void listenToSocket() throws IOException {
            LocalSocket socket = null;
    
            try {
                socket = new LocalSocket();
                LocalSocketAddress address = determineSocketAddress();
    
                socket.connect(address);
    
                InputStream inputStream = socket.getInputStream();
                synchronized (mDaemonLock) {
                    mOutputStream = socket.getOutputStream();
                }
    
                mCallbacks.onDaemonConnected();
    
                FileDescriptor[] fdList = null;
                byte[] buffer = new byte[BUFFER_SIZE];
                int start = 0;
    
                while (true) {
                    int count = inputStream.read(buffer, start, BUFFER_SIZE - start);
                    if (count < 0) {
                        loge("got " + count + " reading with start = " + start);
                        break;
                    }
                    fdList = socket.getAncillaryFileDescriptors();
    
                    // Add our starting point to the count and reset the start.
                    count += start;
                    start = 0;
    
                    for (int i = 0; i < count; i++) {
                        if (buffer[i] == 0) {
                            // Note - do not log this raw message since it may contain
                            // sensitive data
                            final String rawEvent = new String(
                                    buffer, start, i - start, StandardCharsets.UTF_8);
    
                            boolean releaseWl = false;
                            try {
                                final NativeDaemonEvent event =
                                        NativeDaemonEvent.parseRawEvent(rawEvent, fdList);
    
                                log("RCV <- {" + event + "}");
    
                                if (event.isClassUnsolicited()) {
                                    // TODO: migrate to sending NativeDaemonEvent instances
                                    if (mCallbacks.onCheckHoldWakeLock(event.getCode())
                                            && mWakeLock != null) {
                                        mWakeLock.acquire();
                                        releaseWl = true;
                                    }
                                    Message msg = mCallbackHandler.obtainMessage(
                                            event.getCode(), uptimeMillisInt(), 0, event.getRawEvent());
                                    if (mCallbackHandler.sendMessage(msg)) {
                                        releaseWl = false;
                                    }
                                } else {
                                    mResponseQueue.add(event.getCmdNumber(), event);
                                }
                            } catch (IllegalArgumentException e) {
                                log("Problem parsing message " + e);
                            } finally {
                                if (releaseWl) {
                                    mWakeLock.release();
                                }
                            }
    
                            start = i + 1;
                        }
                    }
    
                    if (start == 0) {
                        log("RCV incomplete");
                    }
    
                    // We should end at the amount we read. If not, compact then
                    // buffer and read again.
                    if (start != count) {
                        final int remaining = BUFFER_SIZE - start;
                        System.arraycopy(buffer, start, buffer, 0, remaining);
                        start = remaining;
                    } else {
                        start = 0;
                    }
                }
            } catch (IOException ex) {
                loge("Communications error: " + ex);
                throw ex;
            } finally {
                synchronized (mDaemonLock) {
                    if (mOutputStream != null) {
                        try {
                            loge("closing stream for " + mSocket);
                            mOutputStream.close();
                        } catch (IOException e) {
                            loge("Failed closing output stream: " + e);
                        }
                        mOutputStream = null;
                    }
                }
    
                try {
                    if (socket != null) {
                        socket.close();
                    }
                } catch (IOException ex) {
                    loge("Failed closing socket: " + ex);
                }
            }
        }

    回调到NetworkManagementService.java中的onEvent方法并通知之前创建好的Observers

    复制代码
            @Override
            public boolean onEvent(int code, String raw, String[] cooked) {
                String errorMessage = String.format("Invalid event from daemon (%s)", raw);
                switch (code) {
                case NetdResponseCode.InterfaceChange:
                        /*
                         * a network interface change occured
                         * Format: "NNN Iface added <name>"
                         *         "NNN Iface removed <name>"
                         *         "NNN Iface changed <name> <up/down>"
                         *         "NNN Iface linkstatus <name> <up/down>"
                         */
                        if (cooked.length < 4 || !cooked[1].equals("Iface")) {
                            throw new IllegalStateException(errorMessage);
                        }
                        if (cooked[2].equals("added")) {
                            notifyInterfaceAdded(cooked[3]);
                            return true;
                        } else if (cooked[2].equals("removed")) {
                            notifyInterfaceRemoved(cooked[3]);
                            return true;
                        } else if (cooked[2].equals("changed") && cooked.length == 5) {
                            notifyInterfaceStatusChanged(cooked[3], cooked[4].equals("up"));
                            return true;
                        } else if (cooked[2].equals("linkstate") && cooked.length == 5) {
                            notifyInterfaceLinkStateChanged(cooked[3], cooked[4].equals("up"));
                            return true;
                        }
                        throw new IllegalStateException(errorMessage);
                        // break;
                case NetdResponseCode.BandwidthControl:
                        /*
                         * Bandwidth control needs some attention
                         * Format: "NNN limit alert <alertName> <ifaceName>"
                         */
                        if (cooked.length < 5 || !cooked[1].equals("limit")) {
                            throw new IllegalStateException(errorMessage);
                        }
                        if (cooked[2].equals("alert")) {
                            notifyLimitReached(cooked[3], cooked[4]);
                            return true;
                        }
                        throw new IllegalStateException(errorMessage);
                        // break;
                case NetdResponseCode.InterfaceClassActivity:
                        /*
                         * An network interface class state changed (active/idle)
                         * Format: "NNN IfaceClass <active/idle> <label>"
                         */
                        if (cooked.length < 4 || !cooked[1].equals("IfaceClass")) {
                            throw new IllegalStateException(errorMessage);
                        }
                        long timestampNanos = 0;
                        int processUid = -1;
                        if (cooked.length >= 5) {
                            try {
                                timestampNanos = Long.parseLong(cooked[4]);
                                if (cooked.length == 6) {
                                    processUid = Integer.parseInt(cooked[5]);
                                }
                            } catch(NumberFormatException ne) {}
                        } else {
                            timestampNanos = SystemClock.elapsedRealtimeNanos();
                        }
                        boolean isActive = cooked[2].equals("active");
                        notifyInterfaceClassActivity(Integer.parseInt(cooked[3]),
                                isActive ? DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH
                                : DataConnectionRealTimeInfo.DC_POWER_STATE_LOW,
                                timestampNanos, processUid, false);
                        return true;
                        // break;
                case NetdResponseCode.InterfaceAddressChange:
                        /*
                         * A network address change occurred
                         * Format: "NNN Address updated <addr> <iface> <flags> <scope>"
                         *         "NNN Address removed <addr> <iface> <flags> <scope>"
                         */
                        if (cooked.length < 7 || !cooked[1].equals("Address")) {
                            throw new IllegalStateException(errorMessage);
                        }
    
                        String iface = cooked[4];
                        LinkAddress address;
                        try {
                            int flags = Integer.parseInt(cooked[5]);
                            int scope = Integer.parseInt(cooked[6]);
                            address = new LinkAddress(cooked[3], flags, scope);
                        } catch(NumberFormatException e) {     // Non-numeric lifetime or scope.
                            throw new IllegalStateException(errorMessage, e);
                        } catch(IllegalArgumentException e) {  // Malformed/invalid IP address.
                            throw new IllegalStateException(errorMessage, e);
                        }
    
                        if (cooked[2].equals("updated")) {
                            notifyAddressUpdated(iface, address);
                        } else {
                            notifyAddressRemoved(iface, address);
                        }
                        return true;
                        // break;
                case NetdResponseCode.InterfaceDnsServerInfo:
                        /*
                         * Information about available DNS servers has been received.
                         * Format: "NNN DnsInfo servers <interface> <lifetime> <servers>"
                         */
                        long lifetime;  // Actually a 32-bit unsigned integer.
    
                        if (cooked.length == 6 &&
                            cooked[1].equals("DnsInfo") &&
                            cooked[2].equals("servers")) {
                            try {
                                lifetime = Long.parseLong(cooked[4]);
                            } catch (NumberFormatException e) {
                                throw new IllegalStateException(errorMessage);
                            }
                            String[] servers = cooked[5].split(",");
                            notifyInterfaceDnsServerInfo(cooked[3], lifetime, servers);
                        }
                        return true;
                        // break;
                case NetdResponseCode.RouteChange:
                        /*
                         * A route has been updated or removed.
                         * Format: "NNN Route <updated|removed> <dst> [via <gateway] [dev <iface>]"
                         */
                        if (!cooked[1].equals("Route") || cooked.length < 6) {
                            throw new IllegalStateException(errorMessage);
                        }
    
                        String via = null;
                        String dev = null;
                        boolean valid = true;
                        for (int i = 4; (i + 1) < cooked.length && valid; i += 2) {
                            if (cooked[i].equals("dev")) {
                                if (dev == null) {
                                    dev = cooked[i+1];
                                } else {
                                    valid = false;  // Duplicate interface.
                                }
                            } else if (cooked[i].equals("via")) {
                                if (via == null) {
                                    via = cooked[i+1];
                                } else {
                                    valid = false;  // Duplicate gateway.
                                }
                            } else {
                                valid = false;      // Unknown syntax.
                            }
                        }
                        if (valid) {
                            try {
                                // InetAddress.parseNumericAddress(null) inexplicably returns ::1.
                                InetAddress gateway = null;
                                if (via != null) gateway = InetAddress.parseNumericAddress(via);
                                RouteInfo route = new RouteInfo(new IpPrefix(cooked[3]), gateway, dev);
                                notifyRouteChange(cooked[2], route);
                                return true;
                            } catch (IllegalArgumentException e) {}
                        }
                        throw new IllegalStateException(errorMessage);
                        // break;
                case NetdResponseCode.StrictCleartext:
                    final int uid = Integer.parseInt(cooked[1]);
                    final byte[] firstPacket = HexDump.hexStringToByteArray(cooked[2]);
                    try {
                        ActivityManager.getService().notifyCleartextNetwork(uid, firstPacket);
                    } catch (RemoteException ignored) {
                    }
                    break;
                default: break;
                }
                return false;
            }
        }

三、 Netd测试工具ndc

ndc的原理其实就是通过socket连接上netd进行交互,这部分可以从源代码体现:

ndc.c

复制代码
int main(int argc, char **argv) {
    //argv[1]可以是socket name.
    if ((sock = socket_local_client(argv[1],
                                     ANDROID_SOCKET_NAMESPACE_RESERVED,
                                     SOCK_STREAM)) < 0) {
        //如果不传,那么默认就是name为"netd"的socket                   
        if ((sock = socket_local_client("netd",
                                         ANDROID_SOCKET_NAMESPACE_RESERVED,
                                         SOCK_STREAM)) < 0) {
            fprintf(stderr, "Error connecting (%s)\n", strerror(errno));
            exit(4);
        }
    }
    exit(do_cmd(sock, argc-cmdOffset, &(argv[cmdOffset])));
} 
复制代码
static int do_cmd(int sock, int argc, char **argv) {
   //命令参数最终通过socket发送给netd服务进程处理
   if (write(sock, final_cmd, strlen(final_cmd) + 1) < 0) {
        int res = errno;
        perror("write");
        free(final_cmd);
        return res;
    }
}

监听:

查看可用命令表:

复制代码
console:/ # ndc interface list
110 0 dummy0
110 0 eth0
110 0 ip6_vti0
110 0 ip6tnl0
110 0 ip_vti0
110 0 lo
200 0 Interface list completed

例如: $ adb shell ndc interface list

|-----------|--------------------------------------------------------------------------------------------------|
| interface | list |
| interface | readrxcounter| readtxcounter |
| interface | getthrottle<iface><"rx|tx"> |
| interface | setthrottle<iface><rx_kbps|tx_kbps> |
| interface | driver<iface><cmd><args> |
| interface | route<add|remove> <iface> <"default|secondary"><dst> <prefix> <gateway> |
| list_ttys | |
| ipfwd | status |
| ipfwd | enable|disable |
| tether | status |
| tether | start-reverse|stop-reverse |
| tether | stop< |
| tether | start<addr_1 addr_2 addr_3 addr_4 [addr_2n]> |
| tether | interface<add|remove|list> |
| tether | dnslist |
| tether | dnsset <addr_1> < addr_2> |
| nat | <enable|disable><iface><extface><addrcnt><nated-ipaddr/prelength> |
| pppd | attach<tty> <addr_local> <add_remote> <dns_1><dns_2> |
| pppd | detach<tty> |
| softap | startap|stopap |
| softap | fwreload<iface> <AP|P2P> |
| softap | clients |
| softap | status |
| softap | set<iface> <SSID> <wpa-psk|wpa2-psk|open> [<key><channel> <preamble><max SCB>] |
| resolver | setdefaultif<iface> |
| resolver | setifdns<iface><dns_1><dns_2> |
| resolver | flushdefaultif |
| resolver | flushif<iface> |
| bandwith | enable|disable |
| bandwith | removequota|rq |
| bandwith | getquota|gq |
| bandwith | getiquota|giq<iface> |
| bandwith | setquota|sq<bytes> <iface> |
| bandwith | removequota|rqs<iface> |
| bandwith | removeiiquota|riq<iface> |
| bandwith | setiquota|sq<interface><bytes> |
| bandwith | addnaughtyapps|ana<appUid> |
| bandwith | removenaughtyapps|rna<appUid> |
| bandwith | setgolbalalert|sga<bytes> |
| bandwith | debugsettetherglobalalert|dstga<iface0><iface1> |
| bandwith | setsharedalert|ssa<bytes> |
| bandwith | removesharedalert|rsa |
| bandwith | setinterfacealert|sia<iface><bytes> |
| bandwith | removeinterfacealert|ria<iface> |
| bandwith | gettetherstats|gts<iface0><iface1> |
| idletimer | enable|disable |
| idletimer | add|remove<iface><timeout><classLabel> |
| firewall | enable|disable|is_enabled |
| firewall | set_interface_rule<rmnet0><allow|deny> |
| firewall | set_egress_source_rule<ip_addr><allow|deny> |
| firewall | set_egress_dest_rule<ip_addr><port><allow|deny> |
| firewall | set_uid_rule<uid><allow|deny> |
| clatd | stop|status|start<iface> |

相关推荐
starstarzz1 分钟前
计算机网络实验四:Cisco交换机配置VLAN
网络·计算机网络·智能路由器·vlan·虚拟局域网
Biomamba生信基地3 分钟前
R语言基础| 功效分析
开发语言·python·r语言·医药
CodeClimb18 分钟前
【华为OD-E卷-木板 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
夜幕龙25 分钟前
iDP3复现代码数据预处理全流程(二)——vis_dataset.py
人工智能·python·机器人
做梦敲代码41 分钟前
达梦数据库-读写分离集群部署
数据库·达梦数据库
晚夜微雨问海棠呀1 小时前
长沙景区数据分析项目实现
开发语言·python·信息可视化
cdut_suye1 小时前
Linux工具使用指南:从apt管理、gcc编译到makefile构建与gdb调试
java·linux·运维·服务器·c++·人工智能·python
小蜗牛慢慢爬行1 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
hanbarger1 小时前
nosql,Redis,minio,elasticsearch
数据库·redis·nosql
dundunmm2 小时前
机器学习之scikit-learn(简称 sklearn)
python·算法·机器学习·scikit-learn·sklearn·分类算法