如何在Android上实现RTSP服务器

news2024/12/24 18:26:31

技术背景

在Android上实现RTSP服务器确实是一个不太常见的需求,因为Android平台主要是为客户端应用设计的。在一些内网场景下,我们更希望把安卓终端或开发板,作为一个IPC(网络摄像机)一样,对外提供个拉流的rtsp url,然后把摄像头麦克风甚至屏幕采集的数据,共享出去,轻量级RTSP的设计理念脱颖而出。

轻量级RTSP服务设计初衷,就是避免用户单独部署RTSP或者RTMP服务,实现本地的音视频数据(如摄像头、麦克风),编码后,汇聚到内置RTSP服务,对外提供可供拉流的RTSP URL,轻量级RTSP服务,适用于内网环境下,对并发要求不高的场景,支持H.264/H.265,支持RTSP鉴权、单播、组播模式,考虑到单个服务承载能力,我们支持同时创建多个RTSP服务,并支持获取当前RTSP服务会话连接数。

如何实现RTSP服务器

如果你找不到合适的库,或者需要更高级的功能,你可以考虑编写自己的RTSP服务器:

  • 了解RTSP协议:首先,你需要深入了解RTSP协议的工作原理,包括其消息格式、会话管理和流控制机制。
  • 网络编程:在Android上,你可以使用Java的Socket API来处理网络通信,也可以直接底层实现,然后对上提供jni的接口。你需要编写代码来监听端口、接收RTSP请求、解析请求并发送响应。
  • 媒体处理:RTSP服务器需要能够捕获、编码和传输媒体数据。你可以使用Android的Camera或Camera2的API来捕获视频,并使用FFmpeg或Android的MediaCodec API来实现音视频数据编码。
  • 多线程和并发:RTSP服务器需要处理多个并发客户端连接。你可以使用Java的线程或并发API来管理这些连接。

功能设计

一个完整的RTSP服务,需要设计的功能如下:

  •  ​[视频格式]H.264/H.265(Android H.265硬编码);
  •  [音频格式]G.711 A律、AAC;
  • 协议:RTSP;
  •  [音量调节]Android平台采集端支持实时音量调节;
  •  [H.264硬编码]支持H.264特定机型硬编码;
  •  [H.265硬编码]支持H.265特定机型硬编码;
  • [音视频]支持纯音频/纯视频/音视频;
  • [摄像头]支持采集过程中,前后摄像头实时切换;
  • 支持帧率、关键帧间隔(GOP)、码率(bit-rate)设置;
  • [实时水印]支持动态文字水印、png水印;
  • [实时快照]支持实时快照;
  • [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
  • [外部编码前视频数据对接]支持YUV数据对接;
  • [外部编码前音频数据对接]支持PCM对接;
  • [外部编码后视频数据对接]支持外部H.264、H.265数据对接;
  • [外部编码后音频数据对接]外部AAC数据对接;
  • [扩展录像功能]支持和录像SDK组合使用,录像相关功能。​
  • 支持RTSP端口设置;
  • 支持RTSP鉴权用户名、密码设置;
  • 支持获取当前RTSP服务会话连接数;
  • 支持Android 5.1及以上版本。

接口设计

Android内置轻量级RTSP服务SDK接口详解

调用描述

接口

接口描述

SmartRTSPServerSDK

初始化RTSP Server

InitRtspServer

Init rtsp server(和UnInitRtspServer配对使用,即便是启动多个RTSP服务,也只需调用一次InitRtspServer,请确保在OpenRtspServer之前调用)

创建一个rtsp server

OpenRtspServer

创建一个rtsp server,返回rtsp server句柄

设置端口

SetRtspServerPort

设置rtsp server 监听端口, 在StartRtspServer之前必须要设置端口

设置鉴权用户名、密码

SetRtspServerUserNamePassword

设置rtsp server 鉴权用户名和密码, 这个可以不设置,只有需要鉴权的再设置

获取rtsp server当前会话数

GetRtspServerClientSessionNumbers

获取rtsp server当前的客户会话数, 这个接口必须在StartRtspServer之后再调用

启动rtsp server

StartRtspServer

启动rtsp server

停止rtsp server

StopRtspServer

停止rtsp server

关闭rtsp server

CloseRtspServer

关闭rtsp server

UnInit rtsp server

UnInitRtspServer

UnInit rtsp server(和InitRtspServer配对使用,即便是启动多个RTSP服务,也只需调用一次UnInitRtspServer)

SmartRTSPServerSDK供Publisher调用的接口

设置rtsp的流名称

SetRtspStreamName

设置rtsp的流名称

给要发布的rtsp流设置rtsp server

AddRtspStreamServer

给要发布的rtsp流设置rtsp server, 一个流可以发布到多个rtsp server上,rtsp server的创建启动请参考OpenRtspServer和StartRtspServer接口

清除设置的rtsp server

ClearRtspStreamServer

清除设置的rtsp server

启动rtsp流

StartRtspStream

启动rtsp流

停止rtsp流

StopRtspStream

停止rtsp流

调用逻辑

以大牛直播SDK的Android平台Camera2对接为例,先初始化RTSP Server,启动RTSP服务,然后发布RTSP流即可,如果需要停止RTSP服务,那么先停止RTSP流,再停止RTSP服务即可:

/*
 * MainActivity.java
 * Author: daniusdk.com
 * WeChat: xinsheng120
 */
@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);
	
	...

	context_ = this.getApplicationContext();
	
	libPublisher = new SmartPublisherJniV2();

	libPublisher.InitRtspServer(context_);      //和UnInitRtspServer配对使用,即便是启动多个RTSP服务,也只需调用一次InitRtspServer,请确保在OpenRtspServer之前调用
}

启动、停止RTSP服务:

//启动/停止RTSP服务
class ButtonRtspServiceListener implements View.OnClickListener {
	public void onClick(View v) {
		if (isRTSPServiceRunning) {
			stopRtspService();

			btnRtspService.setText("启动RTSP服务");
			btnRtspPublisher.setEnabled(false);

			isRTSPServiceRunning = false;
			return;
		}

		Log.i(TAG, "onClick start rtsp service..");

		rtsp_handle_ = libPublisher.OpenRtspServer(0);

		if (rtsp_handle_ == 0) {
			Log.e(TAG, "创建rtsp server实例失败! 请检查SDK有效性");
		} else {
			int port = 8554;
			if (libPublisher.SetRtspServerPort(rtsp_handle_, port) != 0) {
				libPublisher.CloseRtspServer(rtsp_handle_);
				rtsp_handle_ = 0;
				Log.e(TAG, "创建rtsp server端口失败! 请检查端口是否重复或者端口不在范围内!");
			}

			if (libPublisher.StartRtspServer(rtsp_handle_, 0) == 0) {
				Log.i(TAG, "启动rtsp server 成功!");
			} else {
				libPublisher.CloseRtspServer(rtsp_handle_);
				rtsp_handle_ = 0;
				Log.e(TAG, "启动rtsp server失败! 请检查设置的端口是否被占用!");
			}

			btnRtspService.setText("停止RTSP服务");
			btnRtspPublisher.setEnabled(true);

			isRTSPServiceRunning = true;
		}
	}
}

stopRtspService()实现如下:

//停止RTSP服务
private void stopRtspService() {
	if(!isRTSPServiceRunning)
	{
		return;
	}
	if (libPublisher != null && rtsp_handle_ != 0) {
		libPublisher.StopRtspServer(rtsp_handle_);
		libPublisher.CloseRtspServer(rtsp_handle_);
		rtsp_handle_ = 0;
	}
}

发布、停止RTSP流:

//发布/停止RTSP流
class ButtonRtspPublisherListener implements View.OnClickListener {
	public void onClick(View v) {
		if (stream_publisher_.is_rtsp_publishing()) {
			stopRtspPublisher();

			btnRtspPublisher.setText("发布RTSP流");
			btnGetRtspSessionNumbers.setEnabled(false);
			btnRtspService.setEnabled(true);
			return;
		}

		Log.i(TAG, "onClick start rtsp publisher..");

		InitAndSetConfig();

		String rtsp_stream_name = "stream1";
		stream_publisher_.SetRtspStreamName(rtsp_stream_name);
		stream_publisher_.ClearRtspStreamServer();

		stream_publisher_.AddRtspStreamServer(rtsp_handle_);

		if (!stream_publisher_.StartRtspStream()) {
			stream_publisher_.try_release();
			Log.e(TAG, "调用发布rtsp流接口失败!");
			return;
		}

		startAudioRecorder();
		startLayerPostThread();

		btnRtspPublisher.setText("停止RTSP流");
		btnGetRtspSessionNumbers.setEnabled(true);
		btnRtspService.setEnabled(false);
	}
}

stopRtspPublisher()实现如下:

//停止发布RTSP流
private void stopRtspPublisher() {
	stream_publisher_.StopRtspStream();
	stream_publisher_.try_release();

	if (!stream_publisher_.is_publishing())
		stopAudioRecorder();
}

其中,InitAndSetConfig()实现如下,通过调研SmartPublisherOpen()接口,生成推送实例句柄。

/*
 * MainActivity.java
 * Author: daniusdk.com
 */
private void InitAndSetConfig() {
	if (null == libPublisher)
		return;

	if (!stream_publisher_.empty())
		return;

	Log.i(TAG, "InitAndSetConfig video width: " + video_width_ + ", height" + video_height_ + " imageRotationDegree:" + cameraImageRotationDegree_);

	int audio_opt = 1;
	long handle = libPublisher.SmartPublisherOpen(context_, audio_opt, 3,  video_width_, video_height_);
	if (0==handle) {
		Log.e(TAG, "sdk open failed!");
		return;
	}

	Log.i(TAG, "publisherHandle=" + handle);

	int fps = 25;
	int gop = fps * 3;

	initialize_publisher(libPublisher, handle, video_width_, video_height_, fps, gop);

	stream_publisher_.set(libPublisher, handle);
}

对应的initialize_publisher()实现如下,设置软硬编码、帧率、关键帧间隔等。

private boolean initialize_publisher(SmartPublisherJniV2 lib_publisher, long handle, int width, int height, int fps, int gop) {
	if (null == lib_publisher) {
		Log.e(TAG, "initialize_publisher lib_publisher is null");
		return false;
	}

	if (0 == handle) {
		Log.e(TAG, "initialize_publisher handle is 0");
		return false;
	}

	if (videoEncodeType == 1) {
		int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, true);
		Log.i(TAG, "h264HWKbps: " + kbps);
		int isSupportH264HWEncoder = lib_publisher.SetSmartPublisherVideoHWEncoder(handle, kbps);
		if (isSupportH264HWEncoder == 0) {
			lib_publisher.SetNativeMediaNDK(handle, 0);
			lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
			lib_publisher.SetVideoHWEncoderQuality(handle, 39);
			lib_publisher.SetAVCHWEncoderProfile(handle, 0x08); // 0x01: Baseline, 0x02: Main, 0x08: High

			// lib_publisher.SetAVCHWEncoderLevel(handle, 0x200); // Level 3.1
			// lib_publisher.SetAVCHWEncoderLevel(handle, 0x400); // Level 3.2
			// lib_publisher.SetAVCHWEncoderLevel(handle, 0x800); // Level 4
			lib_publisher.SetAVCHWEncoderLevel(handle, 0x1000); // Level 4.1 多数情况下,这个够用了
			//lib_publisher.SetAVCHWEncoderLevel(handle, 0x2000); // Level 4.2

			// lib_publisher.SetVideoHWEncoderMaxBitrate(handle, ((long)h264HWKbps)*1300);

			Log.i(TAG, "Great, it supports h.264 hardware encoder!");
		}
	} else if (videoEncodeType == 2) {
		int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, false);
		Log.i(TAG, "hevcHWKbps: " + kbps);
		int isSupportHevcHWEncoder = lib_publisher.SetSmartPublisherVideoHevcHWEncoder(handle, kbps);
		if (isSupportHevcHWEncoder == 0) {
			lib_publisher.SetNativeMediaNDK(handle, 0);
			lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
			lib_publisher.SetVideoHWEncoderQuality(handle, 39);

			// libPublisher.SetVideoHWEncoderMaxBitrate(handle, ((long)hevcHWKbps)*1200);

			Log.i(TAG, "Great, it supports hevc hardware encoder!");
		}
	}

	boolean is_sw_vbr_mode = true;
	//H.264 software encoder
	if (is_sw_vbr_mode) {
		int is_enable_vbr = 1;
		int video_quality = LibPublisherWrapper.estimate_video_software_quality(width, height, true);
		int vbr_max_kbps = LibPublisherWrapper.estimate_video_vbr_max_kbps(width, height, fps);
		lib_publisher.SmartPublisherSetSwVBRMode(handle, is_enable_vbr, video_quality, vbr_max_kbps);
	}

	if (is_pcma_) {
		lib_publisher.SmartPublisherSetAudioCodecType(handle, 3);
	} else {
		lib_publisher.SmartPublisherSetAudioCodecType(handle, 1);
	}

	lib_publisher.SetSmartPublisherEventCallbackV2(handle, new EventHandlerPublisherV2().set(handler_, record_executor_));

	lib_publisher.SmartPublisherSetSWVideoEncoderProfile(handle, 3);

	lib_publisher.SmartPublisherSetSWVideoEncoderSpeed(handle, 2);

	lib_publisher.SmartPublisherSetGopInterval(handle, gop);

	lib_publisher.SmartPublisherSetFPS(handle, fps);

	// lib_publisher.SmartPublisherSetSWVideoBitRate(handle, 600, 1200);

	boolean is_noise_suppression = true;
	lib_publisher.SmartPublisherSetNoiseSuppression(handle, is_noise_suppression ? 1 : 0);

	boolean is_agc = false;
	lib_publisher.SmartPublisherSetAGC(handle, is_agc ? 1 : 0);

	int echo_cancel_delay = 0;
	lib_publisher.SmartPublisherSetEchoCancellation(handle, 1, echo_cancel_delay);

	return true;
}

发布RTSP流成功后,会回调上来可供拉流的RTSP URL:

private static class EventHandlerPublisherV2 implements NTSmartEventCallbackV2 {
	@Override
	public void onNTSmartEventCallbackV2(long handle, int id, long param1, long param2, String param3, String param4, Object param5) {

		switch (id) {
			...
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL:
				publisher_event = "RTSP服务URL: " + param3;
				break;
		}
	}
}

获取RTSP Session会话数:

//获取RTSP会话数
class ButtonGetRtspSessionNumbersListener implements View.OnClickListener {
	public void onClick(View v) {
		if (libPublisher != null && rtsp_handle_ != 0) {
			int session_numbers = libPublisher.GetRtspServerClientSessionNumbers(rtsp_handle_);

			Log.i(TAG, "GetRtspSessionNumbers: " + session_numbers);

			PopRtspSessionNumberDialog(session_numbers);
		}
	}
}

//当前RTSP会话数弹出框
private void PopRtspSessionNumberDialog(int session_numbers) {
	final EditText inputUrlTxt = new EditText(this);
	inputUrlTxt.setFocusable(true);
	inputUrlTxt.setEnabled(false);

	String session_numbers_tag = "RTSP服务当前客户会话数: " + session_numbers;
	inputUrlTxt.setText(session_numbers_tag);

	AlertDialog.Builder builderUrl = new AlertDialog.Builder(this);
	builderUrl
			.setTitle("内置RTSP服务")
			.setView(inputUrlTxt).setNegativeButton("确定", null);
	builderUrl.show();
}

onDestroy() 的时候,调研UnInitRtspServer()即可:

@Override
protected void onDestroy() {
	Log.i(TAG, "activity destory!");

	stopAudioRecorder();

	stopRtspPublisher();
	stopRtspService();
	isRTSPServiceRunning = false;

	stream_publisher_.release();

	if (libPublisher != null)
		libPublisher.UnInitRtspServer();      //如已启用内置服务功能(InitRtspServer),调用UnInitRtspServer, 注意,即便是启动多个RTSP服务,也只需调用UnInitRtspServer一次

	stopLayerPostThread();

	if (camera2Helper != null) {
		camera2Helper.release();
	}

	super.onDestroy();
}

总结

Android上实现RTSP服务器是一个极具挑战的任务,功能设计这块,除了需要支持接编码前音视频数据外,还需要支持对接编码后音视频数据,并实现本地录像、快照等功能组合使用。需要注意的是,就像海康、大华的摄像头一样,对外的并发,一般限于4-8个,Android设备的性能一般来说,可能不足以处理高负载的RTSP服务器,但是小并发模式下,能稳定的运行,就达到设计初衷了,感兴趣的开发者,可以单独跟单独探讨。

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

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

相关文章

rhat Linux虚拟机桥接网络配置

本文物理机安装Windows10系统,虚拟机安装redhat7.7。 0、基本概念 VMware中的桥接模式(Bridged Mode)是一种虚拟机网络配置方式,它允许虚拟机直接连接到宿主机的物理网络,使得虚拟机能够像物理机一样访问外部网络。 在…

高颜值,实力派丨捷顺科技旗舰级人脸识别门禁、速通摆闸新品发布

9月12日,捷顺科技成功举办以“全新进化,定义非凡”为主题的2024智慧人行线上新品发布会,正式推出两款全新一代的智慧人行领域旗舰级产品——人脸识别门禁Y20、速通摆闸3910A。 作为国内最早进入出入口控制管理行业的企业之一,捷顺…

深度学习-生成式检索-论文速读-2024-09-14

深度学习-生成式检索-论文速读-2024-09-14 前言: 生成式检索(Generative Retrieval, GR)是一种结合了生成模型和检索系统的人工智能技术方法。这种方法在处理信息检索任务时,不仅依赖于已有数据的检索,还能生成新的、…

unity的学习

因为需要构建一个三维物理环境,所以学习了unity,半天就可以,非常简单清晰 1.安装 去官网下载unity hub . 然后需要下载editor,但注意已经有了vs2022就不要再下一次了,下的时候会全放c盘,再安装.c盘都装不下了. 如果美游vs2022,就先自己把vs2022安装好,再安装unity hub.(其实不…

Vue自定义指令以及项目中封装过的自定义指令

自定义指令 Vue 自定义指令是 Vue.js 框架中一个非常强大的功能,它允许你注册一些全局或局部的自定义 DOM 操作指令,以便在模板中复用。自定义指令通过 Vue.directive() 方法进行全局注册,或者在组件的 directives 选项中局部注册。 自定义…

YOLOV8输出预测框的坐标信息

结果:(前提是对应类别的yolov8模型已经训练好) 具体实现: 在ultralytics\utils\plotting.py里面 CtrlF搜索box_label 再次照片的最后一行输入: # 左上角cv2.putText(self.im, f"({p1[0]}, {p1[1]})", (p1…

计算机毕业设计选题推荐-共享图书管理系统-小程序/App

✨作者主页:IT研究室✨ 个人简介:曾从事计算机专业培训教学,擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

【北京迅为】《STM32MP157开发板使用手册》- 第三十一章Cortex-M4窗口看门狗实验

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器,既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构,主频650M、1G内存、8G存储,核心板采用工业级板对板连接器,高可靠,牢固耐…

国外SIM卡有信号无法连接,如何解决?

🏆本文收录于《CSDN问答解惑-专业版》专栏,主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收…

[Java]SpringBoot能力进阶

配置优先级 SpringBoot中支持三种格式的配置文件 优先级: application.properties文件 > application.yml文件 > application.yaml文件 SpringBoot除了通过配置文件配置属性, 还支持Java系统属性和命令行参数的方式 进行属性配置 1.在IDEA中配置java系统属性和命令行参…

19、Python如何读写excel文件

在数据处理和分析时,经常使用Excel,而手动操作Excel可能非常繁琐和耗时。Python提供了许多强大的库和工具,可以方便地操作Excel文件。 在Python标准库中是不支持Excel读写的,我们要安装第三方库来实现。 在Python中操作Excel有多…

设计一个高质量的API接口:提升应用性能的关键步骤

在当今的软件开发世界中,API(应用程序编程接口)接口扮演着至关重要的角色。一个设计精良的API不仅能够提高开发效率,还能提升用户体验,并确保系统的可扩展性和安全性。本文将探讨如何设计一个高质量的API接口&#xff…

Golang | Leetcode Golang题解之第412题Fizz Buzz

题目&#xff1a; 题解&#xff1a; func fizzBuzz(n int) (ans []string) {for i : 1; i < n; i {sb : &strings.Builder{}if i%3 0 {sb.WriteString("Fizz")}if i%5 0 {sb.WriteString("Buzz")}if sb.Len() 0 {sb.WriteString(strconv.Itoa(i…

Davinci 大数据可视化分析

Davinci 大数据可视化分析 一、Davinci 架构设计1.1 Davinci定义1.2 Davinci 应用场景 二、Davinci 安装部署2.1 部署规划2.2 前置环境准备2.3 Davinci部署2.3.1 物料准备2.3.2 安装配置 2.4 环境变量配置2.5 初始化数据库2.5.1 创建数据库及用户 2.5.2 建表2.6 初始化配置 三、…

开源免费的NAS系统-TrueNAS CORE搭建和使用(保姆级教程)

TrueNAS CORE说明 TrueNASCORE(以前称为 FreeNAS)是世界上最受欢迎的存储操作系统,因为它使您能够构建自己的专业级存储系统,用于各种数据密集型应用程序,而无需任何软件成本。只需将其安装到硬件或虚拟机上,即可体验开源存储的真正存储自由。 TrueNAS提供了CORE和Ente…

如何用python构建简单神经网络?

今天&#xff0c;我们将运用Python来搭建一个简单的神经网络模型&#xff01;并深入浅出地探索其背后的运作原理。 多输入 提及神经网络时&#xff0c;我们可以将其视为一个高度灵活的函数来理解和应用。 为了做出准确的预测&#xff0c;我们需要构建能够同时组合多个输入的…

吸浮毛宠物空气净化器推荐,希喂、小米、有哈宠物空气净化器测评

养猫需谨慎&#xff0c;不然就要做猫奴一辈子啦&#xff01;上次堂妹来我家住几天&#xff0c;刚开始还担心和猫处不来&#xff0c;不敢去摸它&#xff0c;走的时候已经约好下次来看它的时间&#xff0c;笑死我了。毕竟猫咪这么可爱&#xff0c;很少有人可以抵抗它的魅力。 这不…

Zabbix 2024 中国峰会在上海举办,Databuff 携最业界领先研发成果重磅亮相

9月13日&#xff0c;Zabbix中国峰会在上海圆满结束。会上共进行了18场技术演讲&#xff0c;共有超过500人参加了本次盛会。 大会开始&#xff0c;Zabbix创始人Alexei Vladishev介绍了Zabbix7.0新功能以及未来的发展蓝图。 随后&#xff0c;乘云数字CEO向成钢分享了《如何打造开…

JVM 调优篇7 调优案例4- 线程溢出

一 线程溢出 1.1 报错信息 每个 Java 线程都需要占用一定的内存空间&#xff0c;当 JVM 向底层操作系统请求创建一个新的 native 线程时&#xff0c;如果没有足够的资源分配就会报此类错误。报错信息&#xff1a;java.lang.outofmemoryError:unable to create new Native Thr…

集群软件在linux上的安装

前置准备 为了保证各个服务器之间的正常通信&#xff0c;要完成集群化环境的前置准备&#xff0c;包括创建多台虚拟机&#xff0c;配置主机名映射&#xff0c;SSH免密登录等等。 配置多个虚拟机 配置多台Linux虚拟机 安装集群化软件&#xff0c;首要条件就是要有多台Linux服务…