Android ANR触发机制之Service ANR

news2024/11/17 3:46:34

一、前言

    在Service组件StartService()方式启动流程分析文章中,针对Context#startService()启动Service流程分析了源码,其实关于Service启动还有一个比较重要的点是Service启动的ANR,因为因为线上出现了上百例的"executing service " + service.shortName的异常。

二、Service-ANR原理

2.1 Service启动ANR原理简述

    Service的ANR触发原理,是在启动Service前使用Handler发送一个延时的Message(埋炸弹过程),然后在Service启动完成后remove掉这个Message(拆炸弹过程)。如果在指定的延迟时间内没有remove掉这个Message,那么就会触发ANR(没有在炸弹爆炸前拆掉就会爆炸),弹出AppNotResponding的弹窗。
Android-应用程序无响应
    其实这个机制跟Windows/MacOS的应用程序无响应,是类似的交互设计。
Windows-应用程序无响应
Mac-应用程序无响应

2.2 前台Service VS 后台Service的区别

2.2.1 前台Service

    前台Service是一种在通知栏中显示持续通知的服务,它通常用于执行用户明确知晓的任务,比如音乐播放器、定位服务等。前台Service在系统内部被视为用户正在主动使用的组件,因此它具有更高的优先级和较低的系统资源限制。在使用前台Service时,必须在通知栏中显示一个通知,以告知用户有一个正在运行的Service,并且通常还应该提供一些与该Service相关的有用信息。

2.2.3 后台Service

    后台Service是一种不会在通知栏中显示通知的服务。它用于执行一些不需要用户直接交互或注意的任务,例如数据同步、网络请求等。后台Service具有较低的系统优先级,系统可能会在资源紧张的情况下终止这些服务,以释放资源。

2.3 Service启动ANR源码执行过程

    ps: 还是基于Android SDK28源码分析
    基于文章:Service组件StartService()方式启动流程分析的总结,我们已经很清楚,通过startService的方式启动Service的源码过程。因此,本文直接从com.android.server.am.ActiveServices#bringUpServiceLocked方法的源码开始分析,如有不清楚前置的启动流程的同学,可以参考我之前的文章,然后打开AS对照看下这部分的代码。

2.3.1 ActiveServices#bringUpServiceLocked

    private String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg,
            boolean whileRestarting, boolean permissionsReviewRequired)
            throws TransactionTooLargeException {
        if (r.app != null && r.app.thread != null) {
            sendServiceArgsLocked(r, execInFg, false);
            return null;
        }
        realStartServiceLocked(r, app, execInFg);
    }

2.3.2 ActiveServices#realStartServiceLocked

private final void realStartServiceLocked(ServiceRecord r,
        ProcessRecord app, boolean execInFg) throws RemoteException {
    // ...
    // 会转调到scheduleServiceTimeoutLocked(r.app)方法进行埋炸弹操作
	bumpServiceExecutingLocked(r, execInFg, "create");
	try {
	app.thread.scheduleCreateService(r, r.serviceInfo,
        mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),
        app.repProcState);
    } catch (DeadObjectException e) {
    	mAm.appDiedLocked(app);
    	throw e;
    } finally {
        // serviceDoneExecutingLocked方法内会拆除炸弹
    	serviceDoneExecutingLocked(r, inDestroying, inDestroying);
    	// ...
    }
}

2.3.3 埋炸弹过程:ActiveServices#bumpServiceExecutingLocked

private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
	// ...
	scheduleServiceTimeoutLocked(r.app);
	// ...
}
  • com.android.server.am.ActiveServices#scheduleServiceTimeoutLocked
void scheduleServiceTimeoutLocked(ProcessRecord proc) {
    if (proc.executingServices.size() == 0 || proc.thread == null) {
        return;
    }
    Message msg = mAm.mHandler.obtainMessage(
            ActivityManagerService.SERVICE_TIMEOUT_MSG);
    msg.obj = proc;
    mAm.mHandler.sendMessageDelayed(msg,
            proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
}
  • 这里需要知道,炸弹的爆炸时间在ActiveServices中定义了三个:
// 前台Service的超时时间是20s
static final int SERVICE_TIMEOUT = 20*1000;
// 后台Service的超时时间是200s,是前台超时时间的10倍
static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;
// How long the startForegroundService() grace period is to get around to
// calling startForeground() before we ANR + stop it.
static final int SERVICE_START_FOREGROUND_TIMEOUT = 10*1000;

2.3.4 拆炸弹过程:ActiveServices#serviceDoneExecutingLocked

private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying,
        boolean finishing) {
    // ...
	mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app); 
	// ...       
}

2.3.5 炸弹爆炸出发ANR弹窗过程

    如上文分析的,ANR弹窗其实就是一个sendMessageDelayed()方式发送的一个Message,想要了解ANR炸弹这么爆炸的,其实检索这个what值为ActivityManagerService.SERVICE_TIMEOUT_MSG的消息处理过程即可。
这个消息的Handler对应的handleMessage方法实现代码在AMS.java中。

  • com.android.server.am.ActivityManagerService.MainHandler#handleMessage
final class MainHandler extends Handler {
    public MainHandler(Looper looper) {
        super(looper, null, true);
    }
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            // ...
        	case SERVICE_TIMEOUT_MSG: {
                mServices.serviceTimeout((ProcessRecord)msg.obj);
            } break;
            case SERVICE_FOREGROUND_TIMEOUT_MSG: {
                mServices.serviceForegroundTimeout((ServiceRecord)msg.obj);
            } break;
            case SERVICE_FOREGROUND_CRASH_MSG: {
                mServices.serviceForegroundCrash(
                    (ProcessRecord) msg.obj, msg.getData().getCharSequence(SERVICE_RECORD_KEY));
            } break;
            // ...
        }
    }
  • com.android.server.am.ActiveServices#serviceTimeout
void serviceTimeout(ProcessRecord proc) {
    String anrMessage = null;
    synchronized(mAm) {
        if (proc.executingServices.size() == 0 || proc.thread == null) {
            return;
        }
        final long now = SystemClock.uptimeMillis();
        final long maxTime =  now -
                (proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
        ServiceRecord timeout = null;
        long nextTime = 0;
        for (int i=proc.executingServices.size()-1; i>=0; i--) {
            ServiceRecord sr = proc.executingServices.valueAt(i);
            if (sr.executingStart < maxTime) {
                timeout = sr;
                break;
            }
            if (sr.executingStart > nextTime) {
                nextTime = sr.executingStart;
            }
        }
        if (timeout != null && mAm.mLruProcesses.contains(proc)) {
            Slog.w(TAG, "Timeout executing service: " + timeout);
            StringWriter sw = new StringWriter();
            PrintWriter pw = new FastPrintWriter(sw, false, 1024);
            pw.println(timeout);
            timeout.dump(pw, "    ");
            pw.close();
            mLastAnrDump = sw.toString();
            mAm.mHandler.removeCallbacks(mLastAnrDumpClearer);
            mAm.mHandler.postDelayed(mLastAnrDumpClearer, LAST_ANR_LIFETIME_DURATION_MSECS);
            // 这一句对于我们分析问题比较关键,如果有Service的ANR,
            // 就会在log中有这样的前缀打印:executing service Service.shortName
            anrMessage = "executing service " + timeout.shortName;
        } else {
            Message msg = mAm.mHandler.obtainMessage(
                    ActivityManagerService.SERVICE_TIMEOUT_MSG);
            msg.obj = proc;
            mAm.mHandler.sendMessageAtTime(msg, proc.execServicesFg
                    ? (nextTime+SERVICE_TIMEOUT) : (nextTime + SERVICE_BACKGROUND_TIMEOUT));
        }
    }
    // 真正触发ANR弹窗的位置
    if (anrMessage != null) {
        mAm.mAppErrors.appNotResponding(proc, null, null, false, anrMessage);
    }
}

这里记住anrMessage的格式是:executing service Service.shortName,代表的是Service的启动超时。

  • com.android.server.am.AppErrors#appNotResponding
final void appNotResponding(ProcessRecord app, ActivityRecord activity, ActivityRecord parent, boolean aboveSystem, final String annotation) {
	if (mService.mController != null) {
    try {
        // 0 == continue, -1 = kill process immediately
        // !!关键:mService.mController的实现类是:
       // com.android.server.am.ActivityManagerShellCommand.MyActivityController
        int res = mService.mController.appEarlyNotResponding(
                app.processName, app.pid, annotation);
        if (res < 0 && app.pid != MY_PID) {
            app.kill("anr", true);
        }
    } catch (RemoteException e) {
        mService.mController = null;
        Watchdog.getInstance().setActivityController(null);
    }
    long anrTime = SystemClock.uptimeMillis();
    if (ActivityManagerService.MONITOR_CPU_USAGE) {
       mService.updateCpuStatsNow();
    }
    // Unless configured otherwise, swallow ANRs in background processes 
    // & kill the process.
    // 读取开发者选项中的“显示后台ANR”开关
    boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(),
    Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;
    boolean isSilentANR;
    // Don't dump other PIDs if it's a background ANR
    isSilentANR = !showBackground && !isInterestingForBackgroundTraces(app);
    // Log the ANR to the main log.
	StringBuilder info = new StringBuilder();
	info.setLength(0);
	// 这里可以看出写入到日志文件中的格式是:ANR in processName,
	// 比如:我们应用包名是com.techmix.myapp,
	// 那可在搜索时,直接输入ANR in com.techminx.myapp搜索,可更高效定位到ANR的trace位置
	info.append("ANR in ").append(app.processName);
	if (activity != null && activity.shortComponentName != null) {
    	info.append(" (").append(activity.shortComponentName).append(")");
	}
    info.append("\n");
    info.append("PID: ").append(app.pid).append("\n");
    if (annotation != null) {
      // 这里的reason还是serviceTimeout中定义的Service启动的anrMessage字符串:
      // "executing service Service.shortName",没有多余的更明细的分类了,具体是哪一步ANR了。
      info.append("Reason: ").append(annotation).append("\n");
    }
    if (parent != null && parent != activity) {
      info.append("Parent: ").append(parent.shortComponentName).append("\n");
    }
    ProcessCpuTracker processCpuTracker = new ProcessCpuTracker(true);
    // For background ANRs, don't pass the ProcessCpuTracker to
    // avoid spending 1/2 second collecting stats to rank lastPids.
    File tracesFile = ActivityManagerService.dumpStackTraces(true, firstPids,
    (isSilentANR) ? null : processCpuTracker, 
    (isSilentANR) ? null : lastPids, nativePids);
    
    // 写入cpu占用信息到anr log中
    String cpuInfo = null;
    if (ActivityManagerService.MONITOR_CPU_USAGE) {
       mService.updateCpuStatsNow();
       synchronized (mService.mProcessCpuTracker) {
           cpuInfo = mService.mProcessCpuTracker.printCurrentState(anrTime);
       }
       info.append(processCpuTracker.printCurrentLoad());
       info.append(cpuInfo);
    }
    info.append(processCpuTracker.printCurrentState(anrTime));
    // ANR log写入到dropbox文件夹中,annotation变量就是
    // com.android.server.am.ActiveServices#serviceTimeout中传入的anrMessage变量
    mService.addErrorToDropBox("anr", app, app.processName, activity, parent, annotation,
                cpuInfo, tracesFile, null);
    synchronized (mService) {
    	// 静默ANR的定义?
    	if (isSilentANR) {
           app.kill("bg anr", true);
           return;
        }
    	// 通过Handler发送ANR弹窗的dialog,这里直接跟进
    	// ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG这个消息的handleMessage处理逻辑即可
    	Message msg = Message.obtain();
    	msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
    	msg.obj = new AppNotRespondingDialog.Data(app, activity, aboveSystem);
    	// 注意这里的mService是AMS
    	mService.mUiHandler.sendMessage(msg);
    }
}
  • 静默ANR的定义?
  • 弹出ANR弹窗的逻辑代码:com.android.server.am.ActivityManagerService.UiHandler#handleMessage
case SHOW_NOT_RESPONDING_UI_MSG: {
	// 还是转调到AppErrors中的方法去实现了,所以这里只是用AMS中的UiHandler切换了一下线程而已
	// 最开始的startService方法,从应用主线程
	// ContextImpl#startService->AMS#startService(),后者其实是执行在binder线程池的线程里
	// 面的,是子线程。所以这里通过消息的方式切到主线程
    mAppErrors.handleShowAnrUi(msg);
    ensureBootCompleted();
   } break;
  • com.android.server.am.AppErrors#handleShowAnrUi
	// 跟ANR相关的变量直接存储在了ProcessRecord.java类中,每个进程单独维护一个
	boolean notResponding;      // does the app have a not responding dialog?
	Dialog anrDialog;           // dialog being displayed due to app not resp.

    void handleShowAnrUi(Message msg) {
        Dialog dialogToShow = null;
        synchronized (mService) {
            AppNotRespondingDialog.Data data = (AppNotRespondingDialog.Data) msg.obj;
            final ProcessRecord proc = data.proc;
            if (proc == null) {
                Slog.e(TAG, "handleShowAnrUi: proc is null");
                return;
            }
            if (proc.anrDialog != null) {
                Slog.e(TAG, "App already has anr dialog: " + proc);
                MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_APP_ANR,
                        AppNotRespondingDialog.ALREADY_SHOWING);
                return;
            }
			// 这个ANR的广播,应用进程如果注册了能接收到吗?
            Intent intent = new Intent("android.intent.action.ANR");
            if (!mService.mProcessesReady) {
                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
                        | Intent.FLAG_RECEIVER_FOREGROUND);
            }
            mService.broadcastIntentLocked(null, null, intent,
                    null, null, 0, null, null, null, AppOpsManager.OP_NONE,
                    null, false, false, MY_PID, Process.SYSTEM_UID, 0 /* TODO: Verify */);

            boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(),
                    Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;
            if (mService.canShowErrorDialogs() || showBackground) {
                dialogToShow = new AppNotRespondingDialog(mService, mContext, data);
                proc.anrDialog = dialogToShow;
            } else {
                MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_APP_ANR,
                        AppNotRespondingDialog.CANT_SHOW);
                // 如果ANR弹窗是关闭状态下,直接kill当前应用进程了
                // 跟ANR弹窗中点关闭应用一样,多是调用AMS#killAppAtUsersRequest方法
                // 关闭当前进程
                mService.killAppAtUsersRequest(proc, null);
            }
        }
        // If we've created a crash dialog, show it without the lock held
        if (dialogToShow != null) {
            dialogToShow.show();
        }
    }
  • com.android.server.am.AppErrors#killAppAtUserRequestLocked
  void killAppAtUserRequestLocked(ProcessRecord app, Dialog fromDialog) {
        app.crashing = false;
        app.crashingReport = null;
        app.notResponding = false;
        app.notRespondingReport = null;
        if (app.anrDialog == fromDialog) {
            app.anrDialog = null;
        }
        if (app.waitDialog == fromDialog) {
            app.waitDialog = null;
        }
        // 这里的MY_PID是定义在AMS中的:static final int MY_PID = myPid();
        // 所以这里只要是有效的应用pid,都是能进入if逻辑分支中的
        if (app.pid > 0 && app.pid != MY_PID) {
            handleAppCrashLocked(app, "user-terminated" /*reason*/,
                    null /*shortMsg*/, null /*longMsg*/, null /*stackTrace*/, null /*data*/);
            app.kill("user request after error", true);
        }
    }

app.kill()调用的是ProcessRecord#kill(),最终转调到AMS#killProcessGroup()方法了。这里AMS方式kill掉进程的,在Android的logcat中其实都能搜索到,Activity Manager killing的字样的。

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

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

相关文章

FFmpeg aresample_swr_opts的解析

ffmpeg option的解析 aresample_swr_opts是AVFilterGraph中的option。 static const AVOption filtergraph_options[] {{ "thread_type", "Allowed thread types", OFFSET(thread_type), AV_OPT_TYPE_FLAGS,{ .i64 AVFILTER_THREAD_SLICE }, 0, INT_MA…

mybatisPlus高级篇

文章目录 主键生成策略介绍AUTO策略INPUT策略ASSIGN_ID策略ASSIGN_UUID策略NONE策略 MybatisPlus分页分页插件自定义分页插件 ActiveRecord模式SimpleQuery工具类SimpleQuery介绍listmapGroup 主键生成策略介绍 主键&#xff1a;在数据库中&#xff0c;主键通常用于快速查找和…

【MySQL】视图(十)

&#x1f697;MySQL学习第十站~ &#x1f6a9;本文已收录至专栏&#xff1a;MySQL通关路 ❤️文末附全文思维导图&#xff0c;感谢各位点赞收藏支持~ 一.引入 视图&#xff08;View&#xff09;是一种虚拟存在的表。视图中的数据并不在数据库中实际存在&#xff0c;行和列数据…

python json保留汉字原始形式,而不是Unicode编码(Unicode码)(加ensure_ascii=False参数)

文章目录 问题解决办法测试 问题 如图&#xff0c;保存汉字的时候变成unicode码了。。。 代码是这样的&#xff1a; 解决办法 在Python中&#xff0c;可以使用json模块的ensure_ascii参数来控制是否将汉字转换为类似\u5730\u9707的Unicode编码。默认情况下&#xff0c;ensure…

会议OA项目之权限管理个人中心(修改个人信息,选择本地图片进行头像修改)

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于OA项目的相关操作吧 数据表及分析 表数据 表分析 所谓的权限管理就是不同的人管理不同的事&#xff0c;拥有着管理不同事情的不同权力。那么第一张表--权限表&…

网络知识整理

网络知识整理 网络拓扑网关默认网关 数据传输拓扑结构层面协议层面 网络拓扑 网关 连接两个不同的网络的设备都可以叫网关设备&#xff0c;网关的作用就是实现两个网络之间进行通讯与控制。 网关设备可以是交换机(三层及以上才能跨网络) 、路由器、启用了路由协议的服务器、代…

P3818 小A和uim之大逃离 II

题目 思路 一眼bfs 好像需要记录的东西有点多啊&#xff0c;那就交给数组吧 s t i j 0 / 1 st_{ij0/1} stij0/1​表示用/没用特殊步走到(i,j)的步数&#xff0c;然后套bfs模板即可 代码 #include<bits/stdc.h> using namespace std; const int N1005; int n,m,d,r,st…

c++学习(c++11)[24]

c11 列表初始化 #include"iostream" using namepace std;int main() {int x1 1;int x2 { 2 };int x3 { 2 };vector<int> v1 {1,2,3,4,5,6};vector<int> v1 {1,2,3,4,5,6};list<int> lt1 {1,2,3,4,5,6};list<int> lt1 {1,2,3,4,5,6};au…

红黑树与平衡二叉树

文章目录 前言一、平衡二叉树二、红黑树区别 前言 数据库的底层用到了多种树结构&#xff0c;这里简单记录一下红黑树与平衡二叉树。 一、平衡二叉树 满足二叉树。任何节点的两个子树的高度最大差为1。如果对平衡二叉树进行删除和新增&#xff0c;那么会破坏平衡&#xff0c;…

Jmix 如何将外部数据直接显示在界面?

企业级应用中&#xff0c;通常一个业务系统并不是孤立存在的&#xff0c;而是需要与企业、部门或者是外部的已有系统进行集成。一般而言&#xff0c;系统集成的数据和接口交互方式通常有以下几种&#xff1a; 文件传输&#xff1a;通过文件传输的方式将数据传递给其他系统&…

C++设计模式笔记

设计模式 如何解决复杂性&#xff1f; 分解 核心思想&#xff1a;分而治之&#xff0c;将大问题分解为多个小问题&#xff0c;将复杂问题分解为多个简单的问题。 抽象 核心思想&#xff1a;从高层次角度讲&#xff0c;人们处理复杂性有一个通用的技术&#xff0c;及抽象。…

现在运动耳机什么牌子的好用、最好的运动耳机推荐

对于注重身体健康的小伙伴来说&#xff0c;每周必然都少不了有规律的运动&#xff0c;而运动的时候耳边没有音乐的陪伴总是稍显枯燥无味&#xff0c;很难让人提起干劲来。有些小伙伴觉得运动的时候戴着耳机&#xff0c;稍微跳动几下耳机就开始松动&#xff0c;随时都要分心提防…

【LeetCode】124.二叉树中的最大路径和

题目 二叉树中的 路径 被定义为一条节点序列&#xff0c;序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点&#xff0c;且不一定经过根节点。 路径和 是路径中各节点值的总和。 给你一个二叉树的根节点 root &…

SQLserver 查询数据库表结构和说明简介信息

DECLARE tableName NVARCHAR(MAX ) SET tableName‘TK_Cargoowner’;–表名!!! SELECT CASE WHEN col.colorder 1 THEN obj.name ELSE ‘’ END AS 表名, col.colorder AS 序号 , col.name AS 列名 , ISNULL(ep.[value], ‘’) AS 列说明 , t.name AS 数据类型 , col.length A…

第十章:queue类

系列文章目录 文章目录 系列文章目录前言queue的介绍queue的使用成员函数使用queue 总结 前言 queue是容器适配器&#xff0c;底层封装了STL容器。 queue的介绍 queue的文档介绍 队列是一种容器适配器&#xff0c;专门用于在FIFO上下文(先进先出)中操作&#xff0c;其中从容器…

微信小程序实现日历功能、日历转换插件、calendar

文章目录 演示htmlJavaScript 演示 效果图 微信小程序实现交互 html <view wx:if"{{calendarArr.length}}"><view class"height_786 df_fdc_aic"><view class"grid_c7_104"><view class"font_weight_800 text_align…

多分类问题-Softmax Classifier分类器

概率分布&#xff1a;属于每一个类别的概率总和为0&#xff0c;且都>0&#xff0c;n组类别需要n-1个参数就能算出结果 数据预处理 loss函数 crossentropyloss()函数 CrossEntropyLoss <> LogSoftmax NLLLoss。也就是说使用CrossEntropyLoss最后一层(线性层)是不需要做…

Pytorch深度学习-----神经网络的卷积操作

系列文章目录 PyTorch深度学习——Anaconda和PyTorch安装 Pytorch深度学习-----数据模块Dataset类 Pytorch深度学习------TensorBoard的使用 Pytorch深度学习------Torchvision中Transforms的使用&#xff08;ToTensor&#xff0c;Normalize&#xff0c;Resize &#xff0c;Co…

软件外包开发测试管理工具

测试是软件工程中非常重要的一个环节&#xff0c;在上线前必须需要经过严格的测试才能确保上线后软件系统长时间运行。有大量的软件开发和测试管理工具&#xff0c;每一个工具都有自己的特点&#xff0c;今天和大家分享一些常见的工具&#xff0c;希望对大家有所帮助。北京木奇…

STM32 LWIP UDP 一对一 一对多发送

STM32 LWIP UDP通信 前言设置 IP 地址UDP函数配置实验结果单播发送&#xff0c;一对一发送广播发送&#xff0c;一对多发送 可能遇到的问题总结 前言 之前没有接触过网络的通信&#xff0c;工作需要 UDP 接收和发送通信&#xff0c;在网上没有找到一对一、一对多的相关例程&am…