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 {@Overridepublic 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 broadcastsetNetworkDetailedState(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 countersgetWifiLinkLayerStats();}@Overridepublic 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;@Overridepublic void enter() {ApfFilter.ApfConfiguration apfConfig = new ApfFilter.ApfConfiguration();apfConfig.apfCapabilities = mConfiguration.mApfCapabilities;apfConfig.multicastFilter = mMulticastFiltering;// Get the Configuration for ApfFilter from ContextapfConfig.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 {@Overridepublic 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;@Overridepublic void enter() {super.enter();mController.sendMessage(CMD_PRE_DHCP_ACTION);}@Overridepublic 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();}@Overridepublic 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_IDENTIFIERINADDR_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:

        @Overridepublic 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/netdclass mainsocket netd stream 0660 root systemsocket dnsproxyd stream 0660 root inetsocket mdns stream 0660 root systemsocket fwmarkd stream 0660 root inetonrestart restart zygoteonrestart 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.javaif (!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 purposesmFgHandler = 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 datafinal 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 datafinal 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 instancesif (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

    复制代码

            @Overridepublic 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

 

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

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

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

相关文章

OJ练习第167题——单词接龙

单词接龙 力扣链接&#xff1a;127. 单词接龙 题目描述 字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> … -> sk&#xff1a; 每一对相邻的单词只差一个字母。 对于 1 < i < k 时&…

Vue2+Vue3笔记(尚硅谷张天禹老师)day03

声明:只是记录&#xff0c;初心是为了让页面更好看,会有错误,我并不是一个会记录的人&#xff0c;所以有点杂乱无章的感觉&#xff0c;我先花点时间把视频迅速过掉&#xff0c;再来整理这些杂乱无章的内容 计划是一天更新一点 待做的东西 完成博客记录把完成后的博客记录拆成单…

计算机网络的故事——简单的HTTP协议

简单的HTTP协议 文章目录 简单的HTTP协议一、简单的HTTP协议 一、简单的HTTP协议 HTTP是不保存状态的协议&#xff0c;为了实现保存状态功能于是引入了Cookie技术。 method: get:获取资源 post:传输实体主体 put:传输文件 head:获取报文首部&#xff0c;用于确认URI的有效性以…

把文件上传到Gitee的详细步骤

目录 第一步&#xff1a;创建一个空仓库 第二步&#xff1a;找到你想上传的文件所在的地址&#xff0c;打开命令窗口&#xff0c;git init 第三步&#xff1a;git add 想上传的文件 &#xff0c;git commit -m "给这次提交取个名字" 第四步&#xff1a;和咱们在第…

从0开始的ios自动化测试

最近由于工作内容调整&#xff0c;需要开始弄ios自动化了。网上信息有点杂乱&#xff0c;这边我就按我的实际情况&#xff0c;顺便记录下来&#xff0c;看是否能帮到有需要的人。 环境准备 安装tidevice pip3 install -U “tidevice[openssl]”它的作用是&#xff0c;帮你绕…

C++ 多态语法点

前置知识点 成员变量和成员函数分开存储&#xff0c;只有非静态成员变量才属于类的对象上。 静态成员变量和静态成员函数没有在类上存储。 非静态成员函数也不属于类的对象上 class Animal {public:virtual void speak(){cout<<"动物在说话"<<endl;}}v…

注解-宋红康

目录 一、注解&#xff08;Annotation&#xff09;概述二、常见的注解实例三、如何自定义注解四、JDK中的四个元注解五、Java8注解的新特性1、可重复注解2、类型注解 一、注解&#xff08;Annotation&#xff09;概述 二、常见的注解实例 三、如何自定义注解 自定义注解必须配…

虚拟化和容器

文章目录 1 介绍1.1 简介1.2 虚拟化工作原理1.3 两大核心组件&#xff1a;QEMU、KVMQEMUKVM 1.4 发展历史1.5 虚拟化类型1.6 云计算与虚拟化1.7 HypervisorHypervisor分为两大类 1.8 虚拟化 VS 容器 2 虚拟化应用dockerdocker 与虚拟机的区别 K8Swine 参考 1 介绍 1.1 简介 虚…

springBoot对接Apache POI 实现excel下载和上传

搭建springboot项目 此处可以参考 搭建最简单的SpringBoot项目_Steven-Russell的博客-CSDN博客 配置Apache POI 依赖 <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>5.2.2</version> </…

FastChat工作原理解析

在了解FastChat如何完成大模型部署前&#xff0c;先了解下Huggingface提供的Transformer库。Hugggingface提供的Transformer库 Hugging Face 的 Transformers 库是一个用于自然语言处理&#xff08;NLP&#xff09;任务的 Python 库&#xff0c;旨在简化和加速使用预训练语言模…

手写Spring:第5章-注入属性和依赖对象

文章目录 一、目标&#xff1a;注入属性和依赖对象二、设计&#xff1a;注入属性和依赖对象三、实现&#xff1a;注入属性和依赖对象3.0 引入依赖3.1 工程结构3.2 注入属性和依赖对象类图3.3 定义属性值和属性集合3.3.1 定义属性值3.3.2 定义属性集合 3.4 Bean定义补全3.5 Bean…

android 注解详解

1&#xff0c;注解的概念 注解现在广泛的应用于android的各个开源框架中&#xff0c;不理解注解&#xff0c;我们就无法更好的提升我们的架构能力。那么什么是注解呢&#xff1f;注解&#xff08;Annotation&#xff09;&#xff0c;是JDK5.0 引入的一种注释机制。 注解是元数…

go的iris框架进行本地资源映射到服务端

我这里使用的是HandleDirapi,有其他的请补充 package mainimport ("github.com/kataras/iris/v12" )type Hello struct{Status int json:"status"Message string json:"message" }func main(){app : iris.New()//第一个api:相当于首页app.Get(&q…

飞猪店铺小管家软件需求分析说明书

飞猪店铺小管家软件 项目背景&#xff1a; 在飞猪店铺运营过程中&#xff0c;客服人数不足导致客服团队忙不过来&#xff0c;容易出现订票信息错误&#xff0c;进而客户无法顺利参观景点&#xff0c;频繁投诉。这种情况不仅影响客户体验&#xff0c;还可能导致商家因赔付而承受…

出行类APP商业化路径解决方案

当下市场主流的商业化路径和方法相比于之前区别不大&#xff0c;开发者们都是在现有商业化体系下&#xff0c;制定更加详细、优质的策略&#xff0c;以期获得更高利益。 出行类App用户结构分析 年龄层次&#xff1a;出行类App用户的年龄分布比较广泛&#xff0c;主要集中在20…

苹果电脑版虚拟机推荐 VMware Fusion Pro for mac(vm虚拟机)

VMware Fusion Pro是一款功能强大的虚拟化软件&#xff0c;专为Mac用户设计。它允许用户在Mac上创建、运行和管理虚拟机&#xff0c;以便同时运行多个操作系统和应用程序。 以下是VMware Fusion Pro的一些主要特点和功能&#xff1a; 1. 多操作系统支持&#xff1a;VMware Fu…

MySQL--数据库基础

数据库分类 数据库大体可以分为 关系型数据库 和 非关系型数据库 常用数据类型 数值类型&#xff1a; 分为整型和浮点型&#xff1a; 字符串类型 日期类型

试图替代 Python 的下一代AI编程语言:Mojo

文章目录 为什么叫 Mojo &#xff1f;Python 家族的一员&#xff0c;MojoPython 的好处&#xff1a;Python 兼容性Python 的问题移动和服务器部署&#xff1a;Python 子集和其他类似 Python 的语言&#xff1a; Mojo 是一种创新的编程语言&#xff0c;结合了 Python 的可用性和…

组件以及组件间的通讯

组件 & 组件通讯 :::warning 注意 阅读本文章之前&#xff0c;你应该先要了解 ESM 模块化的 import export&#xff0c;如需要请查看 ESM 模块化。 ::: 上一篇有介绍到什么是组件化&#xff0c;就是把一个页面拆分成若干个小模块&#xff0c;然后重新组成一个页面。其中的…

BL110设备支持Modbus TCP协议接入

随着物联网技术的不断发展&#xff0c;越来越多的工业设备被连接到云平台上&#xff0c;以实现远程监控和管理。在这篇文章中&#xff0c;我们将介绍如何方便用户快速把多种工业设备接入几个主流的云平台&#xff0c;如华为云 IoT、AWS IoT、阿里云 IoT、ThingsBoard、金鸽云等…