【Android 内存优化】KOOM 快手开源框架线上内存监控方案-源码剖析

文章目录

  • 前言
  • OOMMonitorInitTask.INSTANCE.init
  • OOMMonitor.INSTANCE.startLoop
  • super.startLoop
    • call() == LoopState.Terminate
  • dumpAndAnalysis
  • dump
  • startAnalysisService
  • 回到startLoop方法
  • 总结

前言

这篇文章主要剖析KOOM的Java层源码设计逻辑。

使用篇请看上一篇:
【Android KOOM】KOOM java leak使用全解析

OOMMonitorInitTask.INSTANCE.init

OOMMonitorInitTask.INSTANCE.init(JavaLeakTestActivity.this.getApplication());

这里进行初始化,来看看init里面做了什么:

object OOMMonitorInitTask : InitTask {override fun init(application: Application) {val config = OOMMonitorConfig.Builder().setThreadThreshold(50) //50 only for test! Please use default value!.setFdThreshold(300) // 300 only for test! Please use default value!.setHeapThreshold(0.9f) // 0.9f for test! Please use default value!.setVssSizeThreshold(1_000_000) // 1_000_000 for test! Please use default value!.setMaxOverThresholdCount(1) // 1 for test! Please use default value!.setAnalysisMaxTimesPerVersion(3) // Consider use default value!.setAnalysisPeriodPerVersion(15 * 24 * 60 * 60 * 1000) // Consider use default value!.setLoopInterval(5_000) // 5_000 for test! Please use default value!.setEnableHprofDumpAnalysis(true).setHprofUploader(object : OOMHprofUploader {override fun upload(file: File, type: OOMHprofUploader.HprofType) {MonitorLog.e("OOMMonitor", "todo, upload hprof ${file.name} if necessary")}}).setReportUploader(object : OOMReportUploader {override fun upload(file: File, content: String) {MonitorLog.i("OOMMonitor", content)MonitorLog.e("OOMMonitor", "todo, upload report ${file.name} if necessary")}}).build()MonitorManager.addMonitorConfig(config)}
}

可以看到里面做了各种参数的配置,包括上传hprof和报告的上传回调。
使用了构建者模式来进行参数设置,接着通过MonitorManager.addMonitorConfig(config) 添加到MonitorManager中,可见MonitorManager这个类就是监控器管理用的。

interface InitTask {fun init(application: Application)
}

定义了一个接口,用来初始化内存监控任务。参数是需要传递Application,但是这里没有看到有使用到。

OOMMonitor.INSTANCE.startLoop

        OOMMonitor.INSTANCE.startLoop(true, false,5_000L);

上面配置好咯参数和回调,这里就是开始循环。下面来看看里面做了什么。


object OOMMonitor : LoopMonitor<OOMMonitorConfig>(), LifecycleEventObserver {
@Volatileprivate var mIsLoopStarted = false
...override fun startLoop(clearQueue: Boolean, postAtFront: Boolean, delayMillis: Long) {throwIfNotInitialized { return }if (!isMainProcess()) {return}MonitorLog.i(TAG, "startLoop()")if (mIsLoopStarted) {return}mIsLoopStarted = truesuper.startLoop(clearQueue, postAtFront, delayMillis)getLoopHandler().postDelayed({ async { processOldHprofFile() } }, delayMillis)}...}

判断下,假如非主线程,立刻返回。这里可以看出来,调用的地方必须是主线程,不然它就不会执行。
来看下mIsLoopStarted,它被Volatile修饰。Volatile的作用是可以把对应的变量刷新到Cpu缓存中,保证了多线程环境变量的可见性。假如有其他线程修改了这个变量,那么其他线程可以立刻知道。
而这里判断假如loop已经开始,那么也return掉。这些属于健壮性代码。

super.startLoop

看下super.startLoop:

  open fun startLoop(clearQueue: Boolean = true,postAtFront: Boolean = false,delayMillis: Long = 0L) {if (clearQueue) getLoopHandler().removeCallbacks(mLoopRunnable)if (postAtFront) {getLoopHandler().postAtFrontOfQueue(mLoopRunnable)} else {getLoopHandler().postDelayed(mLoopRunnable, delayMillis)}mIsLoopStopped = false}

这里可看到围绕着mLoopRunnable来做功夫。首先看看是否需要清理之前的mLoopRunnable,接着根据参数,决定把runable post到消息队列的哪种情况中,这个稍后研究。这里先看看哪里传入的Handler。

通过跳转,找到了这里:

package com.kwai.koom.base.loopimport android.os.Handler
import android.os.HandlerThread
import android.os.Process.THREAD_PRIORITY_BACKGROUNDinternal object LoopThread : HandlerThread("LoopThread", THREAD_PRIORITY_BACKGROUND) {init {start()}internal val LOOP_HANDLER = Handler(LoopThread.looper)
}

这里是一个HandlerThread,至于HandlerThread。并且LoopThread它在初始化就执行start方法来启动线程。

接着看mLoopRunnable

protected open fun getLoopInterval(): Long {return DEFAULT_LOOP_INTERVAL}companion object {private const val DEFAULT_LOOP_INTERVAL = 1000L}private val mLoopRunnable = object : Runnable {override fun run() {if (call() == LoopState.Terminate) {return}if (mIsLoopStopped) {return}getLoopHandler().removeCallbacks(this)getLoopHandler().postDelayed(this, getLoopInterval())}}

这里就是拿到handler,执行postDelayed,间隔设置为1秒。

call() == LoopState.Terminate

这行代码是关键,假如LoopState.Terminate,是结束状态的话,那就执行call方法。
在这里插入图片描述
看下OOMMonitor的实现:

  override fun call(): LoopState {if (!sdkVersionMatch()) {return LoopState.Terminate}if (mHasDumped) {return LoopState.Terminate}return trackOOM()}

假如dump完成,就返回terminate状态。继续看trackOOM方法:

 private fun trackOOM(): LoopState {SystemInfo.refresh()mTrackReasons.clear()for (oomTracker in mOOMTrackers) {if (oomTracker.track()) {mTrackReasons.add(oomTracker.reason())}}if (mTrackReasons.isNotEmpty() && monitorConfig.enableHprofDumpAnalysis) {if (isExceedAnalysisPeriod() || isExceedAnalysisTimes()) {MonitorLog.e(TAG, "Triggered, but exceed analysis times or period!")} else {async {MonitorLog.i(TAG, "mTrackReasons:${mTrackReasons}")dumpAndAnalysis()}}return LoopState.Terminate}return LoopState.Continue}

看下refresh方法:

var procStatus = ProcStatus()var lastProcStatus = ProcStatus()var memInfo = MemInfo()var lastMemInfo = MemInfo()var javaHeap = JavaHeap()var lastJavaHeap = JavaHeap()fun refresh() {lastJavaHeap = javaHeaplastMemInfo = memInfolastProcStatus = procStatusjavaHeap = JavaHeap()procStatus = ProcStatus()memInfo = MemInfo()javaHeap.max = Runtime.getRuntime().maxMemory()javaHeap.total = Runtime.getRuntime().totalMemory()javaHeap.free = Runtime.getRuntime().freeMemory()javaHeap.used = javaHeap.total - javaHeap.freejavaHeap.rate = 1.0f * javaHeap.used / javaHeap.maxFile("/proc/self/status").forEachLineQuietly { line ->if (procStatus.vssInKb != 0 && procStatus.rssInKb != 0&& procStatus.thread != 0) return@forEachLineQuietlywhen {line.startsWith("VmSize") -> {procStatus.vssInKb = VSS_REGEX.matchValue(line)}line.startsWith("VmRSS") -> {procStatus.rssInKb = RSS_REGEX.matchValue(line)}line.startsWith("Threads") -> {procStatus.thread = THREADS_REGEX.matchValue(line)}}}File("/proc/meminfo").forEachLineQuietly { line ->when {line.startsWith("MemTotal") -> {memInfo.totalInKb = MEM_TOTAL_REGEX.matchValue(line)}line.startsWith("MemFree") -> {memInfo.freeInKb = MEM_FREE_REGEX.matchValue(line)}line.startsWith("MemAvailable") -> {memInfo.availableInKb = MEM_AVA_REGEX.matchValue(line)}line.startsWith("CmaTotal") -> {memInfo.cmaTotal = MEM_CMA_REGEX.matchValue(line)}line.startsWith("ION_heap") -> {memInfo.IONHeap = MEM_ION_REGEX.matchValue(line)}}}memInfo.rate = 1.0f * memInfo.availableInKb / memInfo.totalInKbMonitorLog.i(TAG, "----OOM Monitor Memory----")MonitorLog.i(TAG,"[java] max:${javaHeap.max} used ratio:${(javaHeap.rate * 100).toInt()}%")MonitorLog.i(TAG,"[proc] VmSize:${procStatus.vssInKb}kB VmRss:${procStatus.rssInKb}kB " + "Threads:${procStatus.thread}")MonitorLog.i(TAG,"[meminfo] MemTotal:${memInfo.totalInKb}kB MemFree:${memInfo.freeInKb}kB " + "MemAvailable:${memInfo.availableInKb}kB")MonitorLog.i(TAG,"avaliable ratio:${(memInfo.rate * 100).toInt()}% CmaTotal:${memInfo.cmaTotal}kB ION_heap:${memInfo.IONHeap}kB")}

SystemInfo类里面有很多Java堆,内存信息,进程状态相关的类。这里面可以看出,这个类就是用来把一些监控到的数据刷新和写入文件里面的。当然,还有log输出。

再看mOOMTrackers,分别是各个跟踪器

  private val mOOMTrackers = mutableListOf(HeapOOMTracker(), ThreadOOMTracker(), FdOOMTracker(),PhysicalMemoryOOMTracker(), FastHugeMemoryOOMTracker())

他们抽象父类是:

abstract class OOMTracker : Monitor<OOMMonitorConfig>() {/*** @return true 表示追踪到oom、 false 表示没有追踪到oom*/abstract fun track(): Boolean/*** 重置track状态*/abstract fun reset()/*** @return 追踪到的oom的标识*/abstract fun reason(): String
}

至于具体怎么track,由于篇幅和内容方向问题,这篇文章先不进一步分析。留到后面的文章继续。

回到trackOOM方法:

   mTrackReasons.clear()for (oomTracker in mOOMTrackers) {if (oomTracker.track()) {mTrackReasons.add(oomTracker.reason())}}if (mTrackReasons.isNotEmpty() && monitorConfig.enableHprofDumpAnalysis) {if (isExceedAnalysisPeriod() || isExceedAnalysisTimes()) {MonitorLog.e(TAG, "Triggered, but exceed analysis times or period!")} else {async {MonitorLog.i(TAG, "mTrackReasons:${mTrackReasons}")dumpAndAnalysis()}}

假如track到了原因,它就添加mTrackReasons。
假如分析超过时间和次数,就打印error。其它正常情况就打印mTrackReasons,执行dumpAndAnalysis,然后返回LoopState.Terminate状态。

下面重点看看dumpAndAnalysis方法:

dumpAndAnalysis

 private fun dumpAndAnalysis() {MonitorLog.i(TAG, "dumpAndAnalysis");runCatching {if (!OOMFileManager.isSpaceEnough()) {MonitorLog.e(TAG, "available space not enough", true)return@runCatching}if (mHasDumped) {return}mHasDumped = trueval date = Date()val jsonFile = OOMFileManager.createJsonAnalysisFile(date)val hprofFile = OOMFileManager.createHprofAnalysisFile(date).apply {createNewFile()setWritable(true)setReadable(true)}MonitorLog.i(TAG, "hprof analysis dir:$hprofAnalysisDir")ForkJvmHeapDumper.getInstance().run {dump(hprofFile.absolutePath)}MonitorLog.i(TAG, "end hprof dump", true)Thread.sleep(1000) // make sure file synced to disk.MonitorLog.i(TAG, "start hprof analysis")startAnalysisService(hprofFile, jsonFile, mTrackReasons.joinToString())}.onFailure {it.printStackTrace()MonitorLog.i(TAG, "onJvmThreshold Exception " + it.message, true)}}

这里面正式把track到的数据写入到文件中,包括json文件和hprof文件。重点看dump方法:

dump

@Overridepublic synchronized boolean dump(String path) {MonitorLog.i(TAG, "dump " + path);if (!sdkVersionMatch()) {throw new UnsupportedOperationException("dump failed caused by sdk version not supported!");}init();if (!mLoadSuccess) {MonitorLog.e(TAG, "dump failed caused by so not loaded!");return false;}boolean dumpRes = false;try {MonitorLog.i(TAG, "before suspend and fork.");int pid = suspendAndFork();if (pid == 0) {// Child processDebug.dumpHprofData(path);exitProcess();} else if (pid > 0) {// Parent processdumpRes = resumeAndWait(pid);MonitorLog.i(TAG, "dump " + dumpRes + ", notify from pid " + pid);}} catch (IOException e) {MonitorLog.e(TAG, "dump failed caused by " + e);e.printStackTrace();}return dumpRes;}

init方法:

  private void init () {if (mLoadSuccess) {return;}if (loadSoQuietly("koom-fast-dump")) {mLoadSuccess = true;nativeInit();}}

这里加载一个so库,可以看到还有这些native方法:

/*** Init before do dump.*/private native void nativeInit();/*** Suspend the whole ART, and then fork a process for dumping hprof.** @return return value of fork*/private native int suspendAndFork();/*** Resume the whole ART, and then wait child process to notify.** @param pid pid of child process.*/private native boolean resumeAndWait(int pid);/*** Exit current process.*/private native void exitProcess();

接着执行suspendAndFork,也是native方法。拿到进程pid之后,fork当前进程。然后dump hprof文件。
在这里插入图片描述
至于为什么需要fork一个进程出来dump,可以通过上面截图看出来原因,dump hprof 数据的时候会触发GC,而GC会出发STW,这无疑会造成APP卡顿。这也是LeakCanary不能做成线上内存监控的主要原因,而KOOM解决了这个问题。

子进程dump工作做完之后,接着exitProcess退出。

假如pid > 0,resumeAndWait,就恢复整个ART虚拟机,然后等待子线程唤醒。

这里逻辑我说的有点不清晰,由于看不到so的代码,无法确认。有知道的大佬可以指点一下,感激。

startAnalysisService

前面fork子进程后,执行了 Thread.sleep(1000) // make sure file synced to disk.
接着看是分析堆转信息工作:

private fun startAnalysisService(hprofFile: File,jsonFile: File,reason: String) {if (hprofFile.length() == 0L) {hprofFile.delete()MonitorLog.i(TAG, "hprof file size 0", true)return}if (!getApplication().isForeground) {MonitorLog.e(TAG, "try startAnalysisService, but not foreground")mForegroundPendingRunnables.add(Runnable {startAnalysisService(hprofFile,jsonFile,reason)})return}OOMPreferenceManager.increaseAnalysisTimes()val extraData = AnalysisExtraData().apply {this.reason = reasonthis.currentPage = getApplication().currentActivity?.localClassName.orEmpty()this.usageSeconds = "${(SystemClock.elapsedRealtime() - mMonitorInitTime) / 1000}"}HeapAnalysisService.startAnalysisService(getApplication(),hprofFile.canonicalPath,jsonFile.canonicalPath,extraData,object : AnalysisReceiver.ResultCallBack {override fun onError() {MonitorLog.e(TAG, "heap analysis error, do file delete", true)hprofFile.delete()jsonFile.delete()}override fun onSuccess() {MonitorLog.i(TAG, "heap analysis success, do upload", true)val content = jsonFile.readText()MonitorLogger.addExceptionEvent(content, Logger.ExceptionType.OOM_STACKS)monitorConfig.reportUploader?.upload(jsonFile, content)monitorConfig.hprofUploader?.upload(hprofFile, OOMHprofUploader.HprofType.ORIGIN)}})}

这里就是进行针对一些dump数据进行解析、整理等工作,假如需要上传到服务器,这里也预留了接口供开发者使用,非常贴心。

到这里KOOM框架的Java层核心代码逻辑基本过完了。

回到startLoop方法

回到startLoop方法中super.startLoop 方法,下一行代码是:

    getLoopHandler().postDelayed({ async { processOldHprofFile() } }, delayMillis)

前面分析知道,getLoopHandler拿到的是HandlerThread,这里延时post一个runable消息给它。这里使用协程来执行。

重点需要关注的是processOldHprofFile。


object OOMMonitor : LoopMonitor<OOMMonitorConfig>(), LifecycleEventObserver {private const val TAG = "OOMMonitor"...private fun processOldHprofFile() {MonitorLog.i(TAG, "processHprofFile")if (mHasProcessOldHprof) {return}mHasProcessOldHprof = true;reAnalysisHprof()manualDumpHprof()}...private fun reAnalysisHprof() {for (file in hprofAnalysisDir.listFiles().orEmpty()) {if (!file.exists()) continueif (!file.name.startsWith(MonitorBuildConfig.VERSION_NAME)) {MonitorLog.i(TAG, "delete other version files ${file.name}")file.delete()continue}if (file.canonicalPath.endsWith(".hprof")) {val jsonFile = File(file.canonicalPath.replace(".hprof", ".json"))if (!jsonFile.exists()) {MonitorLog.i(TAG, "create json file and then start service")jsonFile.createNewFile()startAnalysisService(file, jsonFile, "reanalysis")} else {MonitorLog.i(TAG,if (jsonFile.length() == 0L) "last analysis isn't succeed, delete file"else "delete old files", true)jsonFile.delete()file.delete()}}}}private fun manualDumpHprof() {for (hprofFile in manualDumpDir.listFiles().orEmpty()) {MonitorLog.i(TAG, "manualDumpHprof upload:${hprofFile.absolutePath}")monitorConfig.hprofUploader?.upload(hprofFile, OOMHprofUploader.HprofType.STRIPPED)}}}

里面就是操作dump出来的文件,判断当前的版本,假如是旧的,删掉重写等逻辑。

总结

截止到这里,我们开始监控的这两行代码分析完毕:

  /** Init OOMMonitor*/OOMMonitorInitTask.INSTANCE.init(JavaLeakTestActivity.this.getApplication());OOMMonitor.INSTANCE.startLoop(true, false,5_000L);

很简单的两行代码,里面包含了如此之多的业务逻辑和精彩的设计。

很多时候,我们使用越是简单的开源框架,越是能证明作者的厉害之处。他们把繁杂的逻辑内聚到了框架里面,让使用者能用简单一两行代码实现复杂的逻辑业务。

KOOM作为一个线上内存监控框架,有很多优秀的设计。这篇文章也只是在外层分析了一些表面的技术逻辑,至于更深入的内容,后续会继续更新。

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

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

相关文章

基于el-tree实现懒加载穿梭条

一、关键代码 <template><div><!-- 左侧待选列表 --><div class"left-box"><p>待选列表</p><el-input placeholder"输入关键词过滤" v-model"leftFilterText" clearable/><el-treeref"tree…

Windows系统安装MongoDB并结合内网穿透实现公网访问本地数据库

文章目录 前言1. 安装数据库2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射2.3 测试随机公网地址远程连接 3. 配置固定TCP端口地址3.1 保留一个固定的公网TCP端口地址3.2 配置固定公网TCP端口地址3.3 测试固定地址公网远程访问 前言 MongoDB是一个基于分布式文件存储的数…

颜色检测python项目

注意&#xff1a;本文引用自专业人工智能社区Venus AI 更多AI知识请参考原站 &#xff08;[www.aideeplearning.cn]&#xff09; 什么是颜色检测&#xff1f; 颜色检测是检测任何颜色名称的过程。很简单不是吗&#xff1f;嗯&#xff0c;对于人类来说&#xff0c;这是一项极…

荔枝派zero驱动开发06:GPIO操作(platform框架)

参考&#xff1a; 正点原子Linux第五十四章 platform设备驱动实验 一张图掌握 Linux platform 平台设备驱动框架 上一篇&#xff1a;荔枝派zero驱动开发05&#xff1a;GPIO操作&#xff08;使用GPIO子系统&#xff09; 下一篇&#xff1a;更新中… 概述 platform是一种分层思…

static的用法和作用

从三个方面来讲述static的用法和作用&#xff0c;分别是静态局部变量和静态函数以及静态成员 1. 在一个函数中定义静态局部变量的时候&#xff0c;该静态局部变量的生命周期将贯穿整个程序的运行期&#xff0c;而不会像局部变量一样函数运行结束的时候就被销毁。 #include &…

华为北向网管NCE开发教程(1)闭坑选接口协议

华为北向网管NCE开发教程&#xff08;1&#xff09;闭坑选接口协议 华为北向网管NCE开发教程&#xff08;2&#xff09;REST接口开发 华为北向网管NCE开发教程&#xff08;3&#xff09;CORBA协议开发 华为北向网管NCE开发教程&#xff08;4&#xff09;&#xff08;源代码接口…

网络地址转换协议NAT

网络地址转换协议NAT NAT的定义 NAT&#xff08;Network Address Translation&#xff0c;网络地址转换&#xff09;是1994年提出的。当在专用网内部的一些主机本来已经分配到了本地IP地址&#xff08;即仅在本专用网内使用的专用地址&#xff09;&#xff0c;但现在又想和因…

如何打造定制化企业内训系统?企培源码开发实战教学

在当今竞争激烈的商业环境中&#xff0c;企业内训系统的建立和定制化已成为提高企业竞争力和员工素质的关键。本文将探讨如何打造定制化企业内训系统&#xff0c;并介绍企培源码开发的实战教学。 第一步&#xff1a;需求分析与规划 建立定制化的企业内训系统之前&#xff0c…

echarts绘制雷达图

<template><div><div>【云端报警风险】</div><div ref"target" class"w-full h-full" stylewidth&#xff1a;200px;height:300px></div></div> </template><script setup> import { ref, onMounte…

【漏洞复现】Salia PLCC cPH2 远程命令执行漏洞(CVE-2023-46359)

0x01 漏洞概述 Salia PLCC cPH2 v1.87.0 及更早版本中存在一个操作系统命令注入漏洞&#xff0c;该漏洞可能允许未经身份验证的远程攻击者通过传递给连接检查功能的特制参数在系统上执行任意命令。 0x02 测绘语句 fofa&#xff1a;"Salia PLCC" 0x03 漏洞复现 ​…

为什么GPU对于人工智能如此重要?

GPU在人工智能中相当于稀土金属&#xff0c;甚至黄金&#xff0c;它们在当今生成式人工智能时代中的作用不可或缺。那么&#xff0c;为什么GPU在人工智能发展中如此重要呢&#xff1f; GPU概述 什么是GPU 图形处理器&#xff08;GPU&#xff09;是一种通常用于进行快速数学计…

学习Java的第六天

目录 一、变量 1、变量的定义 2、变量的声明格式 3、变量的注意事项 4、变量的作用域 二、常量 三、命名规范 Java 语言支持如下运算符&#xff1a; 1、算术运算符 解析图&#xff1a; 示例&#xff1a; 2、赋值运算符 解析图&#xff1a; 示例&#xff1a; 3、关…

Midjourney绘图欣赏系列(十)

Midjourney介绍 Midjourney 是生成式人工智能的一个很好的例子&#xff0c;它根据文本提示创建图像。它与 Dall-E 和 Stable Diffusion 一起成为最流行的 AI 艺术创作工具之一。与竞争对手不同&#xff0c;Midjourney 是自筹资金且闭源的&#xff0c;因此确切了解其幕后内容尚不…

pip 和conda 更换镜像源介绍

1、前言 很多深度学习的项目免不了安装库文件、配置环境等等&#xff0c;如果利用官方提供的连接&#xff0c;网速很慢&#xff0c;而且很容易download掉。 所以配置好了虚拟环境&#xff0c;将pip换源属实重要 常见的国内镜像源有清华、中科大、阿里等等... 这里建议用中科…

使用Amazon Bedrock托管的Claude3 学习中国历史

最近被Amazon Bedrock托管的Claude3 刷屏了&#xff0c;那么先简单介绍下什么是Claude 3。 Claude 3是Anthropic 推出了下一代 Claude模型&#xff0c;针对不同用例进行优化的三种先进模型&#xff1a;Claude 3 Haiku、Claude 3 Sonnet 和 Claude 3 Opus&#xff0c;使用户能够…

Django入门 整体流程跑通

Django学习笔记 一、Django整体流程跑通 1.1安装 pip install django //安装 import django //在python环境中导入django django.get_version() //获取版本号&#xff0c;如果能获取到&#xff0c;说明安装成功Django目录结构 Python310-Scripts\django-admi…

nginx代理参数proxy_pass

proxy_pass参数用于配置反向代理&#xff0c;指定客户端请求被转发到后端服务器&#xff0c;后端地址可以是域名、ip端口URI 代理后端报错提示本地找不到CSS文件、JavaScript文件或图片 例如&#xff1a; nginx &#xff1a;10.1.74.109 后端服务&#xff1a;http://10.1.74.…

华为北向网管NCE开发教程(3)CORBA协议开发

华为北向网管NCE开发教程&#xff08;1&#xff09;闭坑选接口协议 华为北向网管NCE开发教程&#xff08;2&#xff09;REST接口开发 华为北向网管NCE开发教程&#xff08;3&#xff09;CORBA协议开发 华为北向网管NCE开发教程&#xff08;4&#xff09;&#xff08;源代码接口…

嵌入式学习第二十六天!(网络传输:TCP编程、HTTP协议)

TCP通信&#xff1a; 1. TCP发端&#xff1a; socket -> connect -> send -> recv -> close 2. TCP收端&#xff1a; socket -> bind -> listen -> accept -> recv -> send -> close 3. TCP需要用到的函数&#xff1a; 1. co…

【脚本开发】脚本的启动与暂停

文章目录 需求简单实现测试代码 更新&#xff1a;添加两个方法 需求 基于pynput库开发一个脚本。 要能够用按键控制它启动&#xff0c;暂停。 简单实现 key参数&#xff0c;代表了用什么键控制。 state属性&#xff0c;代表了当前的开关状态。 listener属性&#xff0c;是…