news 2026/10/6 8:32:06

基于Java的APP用户行为分析系统:Flume+Hive离线链路源码详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Java的APP用户行为分析系统:Flume+Hive离线链路源码详解

简介:基于Java的手机APP信息统计分析系统设计源码,是一套面向移动端开发者、数据分析工程师及后端架构人员的完整项目,主要解决APP用户行为数据的采集、传输、存储、离线分析与可视化展示问题,帮助团队掌握功能使用情况并持续优化产品体验。项目采用多模块Maven工程结构,包含日志采集上报客户端、Flume日志接入、Web收集与可视化后台、公共模块以及Hive离线分析等环节,从数据入口到报表输出形成完整闭环,适合作为大数据分析项目的工程范例。资源共57个文件,核心为24个Java类文件、14个XML配置、2个JSP页面与2个Properties配置,覆盖业务逻辑、框架配置与动态页面;另含GeoIP数据库、Markdown/Word说明文档及图片素材,便于理解IP归属地处理和项目部署结构。压缩包56.75MB,已有338人学习下载,适合需要研究APP日志分析全链路、参考源码进行二次开发或准备相关课程设计/毕业设计的开发者。

1. 基于 Java 的手机 APP 信息统计分析系统:一份能跑通全链路的源代码

这套源码把“用户打开 App 之后发生了什么”这件事,完整地拆成了六段:客户端模拟打点、Web 端收集日志、Flume 采集落盘、Hive 清洗建模、可视化报表展示。我用它复现用户行为分析系统时最大的感受是:它不像网上那些只有 CRUD 的管理系统,而是把日志从产生到变成报表的每一环都留在工程里,适合正在学大数据、准备 Java 面试或者做课程设计的人。源码里有 57 个文件,24 个 Java 类、14 个 XML 配置、2 个 JSP 页面,还有一份完整的项目文档和 MaxMind 的 GeoIP 库,拿到的就是整套离线分析链路。接下来我从模块边界讲起,一直拆到排错和批量造数的技巧。

2. 先看模块边界:五个 Maven 子工程的分工与数据流向

2.1 从工程名反推架构:谁在采集、谁在清洗、谁在展示

拿到源码先别急着跑,把pom.xml和src目录扫一遍,架构基本就清楚了。这套源码按“模块职责”拆了五个 Maven 子工程,我整理成一张表,对照着看源码会顺很多。

模块目录责任关键点
app_logs_client模拟手机 App 端,生成启动日志、页面访问日志产生日志的直接入口,可独立运行
app_logs_collect_web接收客户端上报的日志,写入本地磁盘Java Web 项目,处理 HTTP 请求
app_logs_flumeFlume 采集配置与打包监控日志文件,写入 HDFS
app_logs_common公共类:常量、日志实体、IP 解析工具被其他模块依赖,是复用核心
app_logs_hiveHive ETL:建表语句、清洗 SQL、指标计算离线分析的“加工车间”
app_logs_visualize_web可视化 Web 端,读取分析结果并渲染报表JSP/HTML 页面,面向业务展示

模块之间的数据流是一条直线:client模拟用户行为,把日志通过 HTTP 打到collect_web;collect_web把日志落成本地文件;flume监控这批文件,搬运到 HDFS;hive模块在 HDFS 数据之上建外部表、跑指标 SQL;最终visualize_web把指标查出来画成报表。理解这条链路比理解任何一个模块都重要,因为后面所有排错都是顺着这条线找断点的。

2.2 客户端打点:一条模拟日志是怎么被造出来的

app_logs_client是整个系统的源头。它做的事情很纯粹:模拟手机 App 启动、页面跳转、退出登录这些行为,然后把行为数据组装成一条 JSON 或 key-value 日志,发给收集端。常见的做法是拿 Java 的HttpURLConnection直接 POST,不引太重的东西。我复现时习惯先看它生成的日志长什么样,再决定 Hive 那边怎么建表。

// 模拟一条启动日志并上报到 collect_web String url = "http://localhost:8080/app_logs_collect_web/log/start"; HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); conn.setDoOutput(true); String json = "{" + "\"appId\":\"app_001\"," + "\"deviceId\":\"864393028571234\"," + "\"appVersion\":\"2.1.0\"," + "\"osType\":\"android\"," + "\"network\":\"wifi\"," + "\"entry\":\"push\"," + "\"action\":\"startup\"" + "}"; conn.getOutputStream().write(json.getBytes("UTF-8")); int code = conn.getResponseCode(); System.out.println("上报结果 code = " + code);

这段代码的核心参数在 JSON 体里:deviceId是设备唯一标识,后续 Hive 算新增用户、活跃用户全靠它去重;entry表示用户从哪个入口进来,比如push是推送、icon是点击图标;action是具体动作。注意deviceId必须保持稳定,如果客户端每次启动都随机生成一个新 ID,后面所有用户指标都会虚高。我在第一次跑通后踩过这个坑,造数脚本里把deviceId写死成一个数组循环用,就正常了。

2.3 Flume 落 HDFS:TAILDIR 监控配置与参数拆解

collect_web把日志写到本地磁盘之后,Flume 负责搬运。这套源码里用的是 TAILDIR Source,它最大的好处是能记录读取位置:Flume 重启之后不会重复消费已经读过的日志。看app_logs_flume模块里的配置文件,核心是下面这段。

a1.sources = r1 a1.channels = c1 a1.sinks = k1 a1.sources.r1.type = TAILDIR a1.sources.r1.positionFile = /opt/flume/data/taildir_position.json a1.sources.r1.filegroups = f1 a1.sources.r1.filegroups.f1 = /opt/logs/app_logs/.*\.log a1.sources.r1.batchSize = 500 a1.sources.r1.channels = c1 a1.channels.c1.type = memory a1.channels.c1.capacity = 10000 a1.channels.c1.transactionCapacity = 2000 a1.sinks.k1.type = hdfs a1.sinks.k1.hdfs.path = /user/hive/warehouse/app_logs.db/ods_start_log/dt=%Y%m%d a1.sinks.k1.hdfs.filePrefix = app_log a1.sinks.k1.hdfs.fileType = DataStream a1.sinks.k1.hdfs.writeFormat = Text a1.sinks.k1.hdfs.rollInterval = 60 a1.sinks.k1.channels = c1

三个关键参数要调明白:positionFile保存读取进度,如果多套 Flume 复用同一个文件会互相抢锁;filegroups.f1是正则匹配日志目录,目录路径写错日志就永远进不来;hdfs.path里%Y%m%d是时间变量,Flume 会按当前时间自动生成日期目录,这个日期要和 Hive 分区字段对齐,对不齐的话 Hive 查出来全是空。rollInterval默认 60 秒滚动一次文件,测试时可以调成 10 秒方便看效果,生产环境不要设太短,否则 HDFS 上小文件会爆炸。

2.4 为什么这条链路适合新手复现

整套架构没有引入 Kafka、没有 Spark Streaming,纯离线流程,单机就能跑通。这对学习者非常友好:Flume 挂了你查 Flume 日志,Hive 查不到数据你查分区路径,每一步都是独立的黑盒子,可以逐个击破。对于只想看报表效果的人,也可以跳过 Flume,直接把日志文件丢到 HDFS 对应目录,然后跑 Hive SQL。源码的价值在于它把这些环节都串好了,你只需要替换成自己的路径和参数。

3. 把日志变成可分析的表:Hive ETL 与核心指标口径

3.1 启动日志与页面日志分表建模:建外部表时的两个细节

app_logs_hive模块里的 SQL 脚本,核心是把 Flume 落到 HDFS 的日志文件映射成 Hive 外部表。注意一定要用外部表,因为数据是 Flume 写的,Hive 只管读;如果建成内部表,一不小心DROP TABLE会把 HDFS 上的原始日志一起删掉,没有后悔药。

-- 启动日志外部表,按天分区 CREATE EXTERNAL TABLE ods_start_log ( app_id STRING, device_id STRING, app_version STRING, os_type STRING, network STRING, entry STRING, action STRING, log_time STRING ) PARTITIONED BY (dt STRING) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' STORED AS TEXTFILE LOCATION '/user/hive/warehouse/app_logs.db/ods_start_log';

两个容易翻车的点。第一,FIELDS TERMINATED BY '\t'要和客户端实际输出的分隔符一致,客户端如果是逗号分隔,这里不改成','的话,整行数据会被当成一个字段塞进第一列。第二,分区字段dt不要写在表结构字段列表里,它通过PARTITIONED BY单独声明,数据文件里也没有这一列,分区值来自目录名。我见过有人把dt写进字段列表,加载数据时那一列永远是 NULL,还查不出原因。建完表后执行MSCK REPAIR TABLE ods_start_log;让 Hive 自动识别 HDFS 上的分区目录,这一步容易漏。

3.2 核心指标口径:新增用户、活跃用户、启动次数怎么算

有了基础表,剩下的就是指标计算。这套系统里的核心指标有三个:新增用户数、活跃用户数和启动次数。先说口径,再看 SQL,因为口径不一致,同一个数字两个人能算出两个结果。

-- 当日活跃用户数:当天去重后的设备数 SELECT dt, COUNT(DISTINCT device_id) AS active_users FROM ods_start_log WHERE dt = '2024-01-01' GROUP BY dt; -- 当日新增用户数:当天首次出现的设备数 SELECT COUNT(*) AS new_users FROM ( SELECT device_id FROM ods_start_log WHERE dt = '2024-01-01' GROUP BY device_id HAVING MIN(dt) = '2024-01-01' ) t;

活跃用户数的口径是“当天有多少设备产生过启动行为”,直接用COUNT(DISTINCT device_id)。新增用户的逻辑是“这个设备之前从没出现过”,所以先用MIN(dt)找每个设备首次出现日期,再过滤出首次出现日等于当天的设备。这里有个隐性条件:日志表里的数据必须覆盖历史全量,如果只加载了当天数据,那MIN(dt)算出来的全是当天,新增数会等于活跃数,指标直接失真。我在测试环境验证指标时,会故意往表里灌前一天的数据,确认新增数降下来后再看当天报表。

启动次数稍微特殊,每一条启动日志代表一次启动会话,所以COUNT(1)就是启动次数。如果日志里还带页面停留时长字段,平均使用时长就是SUM(duration) / COUNT(DISTINCT device_id),单位通常是秒。这三个指标配合起来,能回答“产品改版后用户到底回没回来”这一类问题。

3.3 用 mmdb 把 IP 翻译成地域:GeoIP 解析的接入方式

源码里那个.mmdb文件是 MaxMind 的 GeoIP 数据库,用来把访问 IP 解析成省份、城市。它配合 Hive 的 UDF 或者直接写 Java 工具类使用。app_logs_common模块里应该有对应的解析封装,核心调用方式如下。

// 用 maxmind-db 读取 mmdb 文件,按 IP 查询地域信息 Reader reader = new Reader(new File("/opt/data/GeoLite2-City.mmdb")); Map<String, Object> result = reader.get(InetAddress.getByName("218.30.118.68")); Map<String, Object> provinceMap = (Map<String, Object>) result.get("subdivisions"); String province = provinceMap.get("names").get("zh-CN").toString(); System.out.println("解析结果: " + province); reader.close();

这段代码里Reader.get()返回的是一个嵌套 Map,subdivisions下的names有多语言版本,取zh-CN才能拿到中文省份名。注意 mmdb 文件是只读的二进制库,不需要导入数据库,直接放文件系统用代码读就行。我踩过的坑是文件路径写死成src/main/resources/下的相对路径,打包后 classpath 变了就找不到文件,后来统一改成绝对路径,或者放进配置项用${geoip.path}注入,问题才消失。另外 mmdb 文件有版本更新周期,IP 段一直在变,代码写完后记得定期替换文件,否则新地区的用户会解析成空。

4. 可视化与联调:从 JSP 报表到接口参数映射

4.1 可视化端读的是哪张表:报表字段和结果表怎么对上

app_logs_visualize_web是分析结果的出口。它不做计算,只把 Hive 算好的结果表查出来渲染成页面。常见的做法是用 SpringMVC 或者原生 Servlet 查 MySQL,然后把 JSON 返回给前端 JSP/HTML。源码里只有 2 个 JSP 和 1 个 HTML,说明报表页面是轻量级的,重点是字段映射。我第一次接可视化时最晕的就是:页面上叫“活跃用户”,代码里查的字段可能叫active_users,而且可能来自两张不同的结果表。

页面指标数据来源表关键字段
今日新增用户dws_new_user_daynew_users
今日活跃用户dws_active_user_dayactive_users
启动次数dws_start_count_daystart_count
平均使用时长dws_avg_duration_dayavg_duration
地域分布dws_area_distributionprovince、user_count

联调时先确认后端接口返回的 JSON 字段名和页面ajax里的取值一致。我遇到最频繁的问题是:后端返回province,页面里写的是areaName,结果图表渲染出来全是空,控制台也不报错,看着像数据丢了,其实是字段名对不上。

4.2 让报表页面动起来:数据访问与渲染的最小闭环

可视化端连 Hive 或 MySQL 的配置都在visualize_web模块里,数据库驱动和连接串要看清楚。如果是直连 Hive,连接串通常是jdbc:hive2://localhost:10000/app_logs,需要先启动 HiveServer2;如果结果已经同步到 MySQL,那连接串就是普通的jdbc:mysql://。我用一个简单的接口代码来说明这个闭环。

@WebServlet("/api/summary") public class SummaryServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String sql = "SELECT new_users, active_users, start_count " + "FROM dws_summary_day WHERE dt = '" + req.getParameter("dt") + "'"; List<Map<String, Object>> rows = JdbcUtil.query(sql); resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write(JSON.toJSONString(rows)); } }

dws_summary_day是预处理好的汇总表,把当天三个核心指标算进一行,查询时按dt参数取某一天。这里有两个细节:dt参数一定要校验,否则拼 SQL 会存在注入风险;响应头里必须显式写charset=UTF-8,不然浏览器默认按 ISO-8859-1 解码,中文全部变成乱码。页面端拿到 JSON 后,用 ECharts 的setOption把数据填进去。第一次联调时先不要急着看图表,直接在浏览器地址栏访问/api/summary?dt=2024-01-01,看到 JSON 再渲染页面,这样能快速区分是接口问题还是前端问题。

4.3 可视化联调的验证顺序

跑可视化端必须遵循一个固定的验证顺序:先确认 Hive 结果表有数据,再确认接口返回 JSON,最后才看页面图表。如果结果表是空的,接口写得再好页面也是白屏。我一般用一条简单 SQL 快速验证:SELECT * FROM dws_summary_day WHERE dt = '2024-01-01' LIMIT 10;有数据再往下走。页面加载出来但图表无数据时,打开浏览器开发者工具看 Network 面板,检查接口状态码和响应体,百分之九十的问题在这一步就定位了。

5. 复现与排错:我把这套源码跑通前后的五件事

5.1 环境版本基线:JDK 8 + Hadoop 2.7 + Hive 2.3 + Flume 1.9

复现这套源码前,环境版本必须先定下来。我的建议是 JDK 1.8、Hadoop 2.7.x、Hive 2.3.x、Flume 1.9.x。这个组合在源码场景下最稳,原因有三:JDK 8 兼容所有 Maven 依赖,不会有 module-info 报错;Hive 2.3 的 HiveServer2 对 JDBC 驱动要求宽松,老驱动也能连上;Flume 1.9 的 TAILDIR 特性稳定。不要一上来就上 Hadoop 3.x 配 Hive 3.x,虽然能跑,但会遇到 YARN 资源调度差异、Hive 视图查询报错等一堆跟业务无关的环境问题。第一次复现没必要追求新版本,跑通链路才是目标。

5.2 五个高频踩坑记录

坑一:collect_web 启动后日志文件不落盘现象:接口返回 200,但指定目录下没有.log文件生成。 原因:日志输出目录不存在,或者 Linux 下进程没有该目录的写权限。 解决:先执行mkdir -p /opt/logs/app_logs并chmod 777目录,再看日志。我在根目录创建目录时经常漏掉权限,Java 进程写不进去又不会报错,只是静默失败,排查了很久才发现是权限问题。

坑二:Flume TAILDIR 不消费新产生的日志现象:日志文件持续增长,HDFS 上没有新文件出现。 原因:filegroups.f1的正则没匹配上文件路径,或者positionFile里记录了错误的文件 ID。 解决:先手动往日志目录写一条数据,然后看 Flume 日志里有没有Opening file的打印。没有就检查正则;有但不出数,删掉taildir_position.json重启 Flume 再试。

坑三:Hive 表能查出数据但分区时间是空的现象:WHERE dt = '2024-01-01'查不到,全表扫描有数据。 原因:Flume 写入 HDFS 的路径是按服务器本地时间生成的日期目录,本地时区或日期格式和 SQL 里写的不一致。比如 Flume 生成的是20240101,SQL 里写的是2024-01-01,永远对不上。 解决:统一日期格式,HDFS 目录用%Y%m%d生成,Hive 分区字段就存20240101,查询条件也用同格式。这个坑属于典型的“两边各写各的”。

坑四:IP 解析结果全部为空现象:报表地域分布一栏是空,其他指标都正常。 原因:mmdb 文件路径在打包后失效,或者拿到的字段名不对,subdivisions这个键在某些 IP 段下不存在。 解决:先单独写个 Java main 方法测试 IP 解析,用固定的公网 IP,比如218.30.118.68,能出结果再封装 UDF 或者工具类。字段读取前先判空:if (provinceMap != null) {...}。

坑五:页面 JSON 中文乱码现象:接口返回的中文变成???或乱字符,图表标题没法看。 原因:响应头没设置字符集,Tomcat 默认用 ISO-8859-1 编码输出。 解决:过滤器里统一加response.setContentType("application/json;charset=UTF-8"),同时确认 JSP 头部也声明了 UTF-8。这属于老生常谈,但几乎每次复现都会遇到一次。

5.3 启动顺序与验证命令

整个系统启动是有顺序的,顺序错了链路就断。我的标准顺序是:HDFS 和 YARN 先起,然后 HiveServer2,再 Flume Agent,最后启动collect_web和visualize_web。每次启动后按下面的检查点过一遍。

# 1. 检查 HDFS 上 Flume 是否写入了新目录 hdfs dfs -ls /user/hive/warehouse/app_logs.db/ods_start_log/ # 2. 检查 Hive 分区是否识别 hive -e "SHOW PARTITIONS app_logs.ods_start_log;" # 3. 手动触发分区修复 hive -e "MSCK REPAIR TABLE app_logs.ods_start_log;" # 4. 直连 HiveServer2 测试查询 beeline -u "jdbc:hive2://localhost:10000/app_logs" \ -e "SELECT dt, COUNT(DISTINCT device_id) FROM ods_start_log GROUP BY dt;"

这套检查命令能帮你在 30 秒内确认链路断在哪:第一步看 Flume 有没有扛起活来,第二步看 Hive 认不认新分区,第四步直接验证查询能否出数。把这几条命令存在一个 shell 脚本里,每次启动后跑一遍,比到处翻日志高效得多。

6. 一个提效技巧:用模拟客户端批量造数,把验证周期从小时压到分钟

跑通一次全链路最慢的不是写代码,是造数据。手工改配置、一次次点击页面产生日志,几十分钟过去 Hive 里还是那几条数据,指标根本看不出趋势。我的做法是写一个批量造数的脚本,直击collect_web的接口,一次性灌入几百台设备的日志,验证周期立刻缩短到分钟级。

# 批量造数脚本:模拟 100 台设备,每台产生 3 次启动日志 BASE_URL="http://localhost:8080/app_logs_collect_web/log/start" for i in $(seq 1 100); do DEVICE_ID="8643930000$(printf '%06d' $i)" for j in 1 2 3; do curl -s -X POST "$BASE_URL" \ -H "Content-Type: application/json" \ -d "{\"appId\":\"app_001\",\"deviceId\":\"$DEVICE_ID\",\"appVersion\":\"2.1.0\",\"osType\":\"android\",\"network\":\"wifi\",\"entry\":\"push\",\"action\":\"startup\"}" \ > /dev/null sleep 1 done done echo "批量造数完成"

这个脚本里DEVICE_ID是核心:用seq生成 100 个固定编号,保证设备 ID 稳定且唯一,这样 Hive 里算活跃用户、新增用户才能得到符合预期的数字。每条请求之间sleep 1是为了模拟真实节奏,同时避免并发太高把 Tomcat 打挂。跑完脚本后不要立刻查 Hive,Flume 的 TAILDIR 是轮询读取的,给它 1~2 分钟把日志搬完,再执行前面第 5.3 节的检查命令。

用这个方式,我可以在十分钟内验证:日志是否落盘、Flume 是否分区写入、Hive 指标是否符合预期、可视化是否渲染正常。从那以后我每次改完 Hive 口径,都强制用脚本重灌一批数据、跑一遍四步检查,再去看报表。数据链路这种东西,靠肉眼盯日志永远盯不干净,只有批量造数才能逼出问题。希望这个习惯能帮到你少走点弯路。

最后说一个最实用的边界认知:这套源码是离线链路的完整参考,不是在线实时系统,日志从产生到报表可见有分钟级延迟。如果你想做的业务要求秒级响应,那需要引入 Kafka 和 Spark Streaming,那是另一个技术方向了。先把离线链路跑通透,再往实时方向扩展,路会顺很多。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 8:31:18

降AI率保姆级流程:从检测原理到人工重塑的实操指南

说实话&#xff0c;我第一次把一篇自己认真改过的稿子丢进AIGC检测工具里&#xff0c;看到报告上写着“疑似AI生成比例&#xff1a;87%”的时候&#xff0c;人是对着屏幕沉默了三分钟的。那篇稿子确实用AI打了底稿&#xff0c;但我前后改了两个多小时&#xff0c;加了自己的真实…

作者头像 李华
网站建设 2026/10/6 8:31:16

SSM+MySQL酒店客房预定系统:从SSM整合到并发事务的实战指南

简介&#xff1a;基于SSMMySQL的酒店客房预定管理系统是一份面向计算机专业毕业设计、课程设计场景的完整项目资源包&#xff0c;覆盖个人中心、用户管理、客房类型、客房预定、开房记录、退房结账等核心业务模块&#xff0c;前后端技术栈与权限角色设计完整&#xff0c;适合需…

作者头像 李华
网站建设 2026/10/6 8:31:14

SpringBoot+Vue+MySQL全栈实战:膳食营养健康管理系统解析

作为开发者和博主&#xff0c;我平时收到最多的私信类型之一就是“有没有适合练手或者做毕业设计的全栈项目”。市面上大部分开源项目要么太老&#xff0c;要么文档不全&#xff0c;要么跑起来一堆坑。最近我花时间把一套基于SpringBoot Vue MySQL的膳食营养健康网站信息管理…

作者头像 李华
网站建设 2026/10/6 8:30:57

SpringBoot3+Vue3旅游景点推荐系统:手写协同过滤全栈实战

最近总有读者和学弟学妹来找我要一套能做毕业设计、又能真正跑起来学东西的项目&#xff0c;我手头刚好整理完一个非常适合零基础入门、又能覆盖全栈核心知识点的作品&#xff1a;基于 SpringBoot3 Vue3 的在线旅游景点推荐系统。后端用 SpringBoot3 搭 REST 接口&#xff0c;…

作者头像 李华
网站建设 2026/10/6 8:30:39

校园网三层架构设计与ENSP配置实战:从VLAN到VRRP/OSPF

说实话&#xff0c;校园网这个题目我做了不止一遍。第一版是为了练手&#xff0c;直接在ENSP里拖了几台设备&#xff0c;想当然把网关全都堆在核心交换机上&#xff0c;结果核心压力大、VLAN间访问要走一大圈&#xff0c;后面一加无线和访客隔离就乱套了。后来按真正园区网的交…

作者头像 李华
网站建设 2026/10/6 8:30:24

IP地址与TCP协议:计算机网络入门核心解析

有一次我给部门新来的同事讲网络基础&#xff0c;他问了句特别经典的话&#xff1a;我打开浏览器输入一个网址&#xff0c;敲下回车&#xff0c;中间到底发生了什么&#xff1f;我说这个问题要是展开讲&#xff0c;基本就是整本《计算机网络》教材。但如果你只是想解决实际工作…

作者头像 李华