02-设计概述

上一篇:01-导言


        本章重点讨论 JNI 中的主要设计问题。本节中的大多数设计问题都与本地方法有关。调用 API 的设计将在第 5 章:调用 API 中介绍。

2.1 JNI 接口函数和指针

        本地代码通过调用 JNI 函数来访问 Java 虚拟机功能。JNI 函数可通过接口指针使用。接口指针是指向指针的指针该指针指向一个指针数组,数组中每个指针指向一个接口函数每个接口函数都位于数组内预定义的偏移量处。下图 "接口指针 "说明了接口指针的组织结构。

        JNI 接口的组织方式类似于 C++ 虚拟函数表或 COM 接口。使用接口表而不是硬连线函数项的好处是,JNI 名称空间与本地代码分离。虚拟机可以轻松提供多个版本的 JNI 函数表。例如,虚拟机可以支持两个 JNI 函数表:

        ①. 一种会对非法参数进行彻底检查,适合调试;

        ②. 而另一种方法只执行 JNI 规范要求的最少检查量,因此效率更高。

        JNI 接口指针只在当前线程中有效。因此,本地方法不得将接口指针从一个线程传递到另一个线程。实现 JNI 的虚拟机可以在 JNI 接口指针指向的区域中分配和存储线程本地数据。

        本地方法接收 JNI 接口指针作为参数。当虚拟机从同一个 Java 线程多次调用本地方法时,会保证将同一个接口指针传递给本地方法。不过,本地方法可以从不同的 Java 线程调用,因此可能会收到不同的 JNI 接口指针

2.2 编译、加载和链接本地方法

        由于 Java 虚拟机是多线程的,因此本地库也应使用多线程感知本地编译器进行编译和链接。例如,使用 Sun Studio 编译器编译的 C++ 代码应使用 -mt 标志。对于使用 GNU gcc 编译器编译的代码,应使用标志 -D_REENTRANT 或 -D_POSIX_C_SOURCE 。

        本地方法用 System.loadLibrary 方法加载。在下面的示例中,类初始化方法加载了一个特定平台的本地库,其中定义了本地方法 f :

package p.q.r;class A {native double f(int i, String s);static {// // /*** defined in "libcore/ojluni/src/main/java/java/lang/System.java"* public static void loadLibrary(String libname);* 加载由libname参数指定的本机库。* libname参数不能包含任何特定于平台的前缀、文件扩展名或路径。* 如果一个名为libname的本地库与虚拟机静态链接,则调用该库导出的JNI_OnLoad()函数。* 否则,libname参数将从系统库位置加载,并以依赖于实现的方式映射到本机库映像。*/System.loadLibrary("p_q_r_A");}
}

        System.loadLibrary 的参数是程序员任意选择的库名。系统会采用一种标准的、但与特定平台有关的方法,将库名称转换为本地库名称。例如,Linux 系统会将库名 p_q_r_A 转换为 libp_q_r_A.so ,而 Windows 系统会将库名 p_q_r_A 转换为 p_q_r_A.dll

        程序员可以使用一个库来存储任意数量的类所需的所有本地方法,只要这些类是用同一个类加载器加载的虚拟机内部会为每个类加载器维护一个已加载的本地库列表。供应商在选择本地库名称时,应尽量减少名称冲突的机会。

        对动态链接和静态链接库的支持,以及它们各自的生命周期管理 "加载 "和 "卸载 "功能钩子,在库和版本管理的调用 API 部分有详细介绍。

2.2.1 解析本地方法名称

        JNI 定义了从 Java 中声明的 native 方法名称到本地库中本地方法名称的 1:1 映射。虚拟机使用该映射将 native 方法的 Java 调用动态链接到本地库中的相应实现。

        该映射通过连接从 native 方法声明中派生出来的以下组件来生成本地方法名称:

                ①. Java_前缀;

                ②. 给出了声明 native 方法的类的二进制内部名称转义该名称的结果

                ③.  ("_") 下划线

                ④. 转义方法名

                ⑤. 如果 native 方法声明是重载的:两个下划线("__"),后跟方法声明的转义参数描述符(JVMS 4.3.3)。

        转义时,每个字母数字 ASCII 字符 ( A-Za-z0-9 ) 都保持不变,并用相应的转义序列替换下表中的每个 UTF-16 代码单元。如果要转义的名称包含一对代用字符,则高代用代码单元和低代用代码单元将分别转义。转义的结果是一个仅由 ASCII 字符 A-Za-z0-9 和下划线组成的字符串。

        出于两个原因,转义是必要的。首先,为了确保 Java 源代码中可能包含 Unicode 字符的类名和方法名能转换成 C 源代码中有效的函数名。其次,确保 native 方法的参数描述符(使用"; "和"["字符对参数类型进行编码)可以在 C 函数名中编码

        当 Java 程序调用一个 native 方法时,虚拟机会搜索本地库,首先查找本地方法名称的简短版本,即不包含转义参数签名的名称如果找不到短名称的本地方法,虚拟机就会查找长版本的本地方法名称,即包含转义参数签名的名称

        先查找简短的名称,可以更方便地在本地库中声明实现。例如,给出 Java 中的 native 方法:

package p.q.r;
class A {native double f(int i, String s);
}

        相应的 C 函数可以命名为 Java_p_q_r_A_f ,而不是 Java_p_q_r_A_f__ILjava_lang_String_2 。

        只有当一个类中的两个或多个 native 方法具有相同的名称时,才有必要在本地库中声明具有长名称的实现。例如,Java 中有以下1 个方法:

package p.q.r;
class A {native double f(int i, String s);native double f(int i, Object s);
}

        相应的 C 语言函数必须命名为 Java_p_q_r_A_f__ILjava_lang_String_2 和 Java_p_q_r_A_f__ILjava_lang_Object_2 ,因为这两个方法是重载的。

        如果 Java 中的 native 方法只被非 native 方法重载,则无需在本地库中使用长名称。在下面的示例中, native 方法 g 不必使用长名称链接,因为另一个方法 g 不是 native 方法,因此不存在于本地库中。

package p.q.r;
class B {int g(int i);native int g(double d);
}

        请注意,转义序列可以安全地以 _0 、 _1 等开头,因为 Java 源代码中的类名和方法名从不以数字开头。但是,在非 Java 源代码生成的类文件中,情况并非如此。为了保持与本地方法名称的 1:1 映射,虚拟机会对生成的名称进行如下检查。如果从方法声明(类或方法名称或参数类型)中转义任何前导字符串的过程导致前导字符串中的 " 0 "、" 1 "、" 2 "或 " 3 "字符在结果中紧跟下划线后或在转义字符串的开头(在完全组装后的名称中,这些字符将紧跟下划线)出现,且未发生变化,则称转义过程 "失败"。在这种情况下,将不执行本地库搜索,并且在尝试链接 native 方法调用时将抛出 UnsatisfiedLinkError 。可以扩展目前的简单映射方案以涵盖这种情况,但复杂性成本将超过任何好处。

        本地方法和接口 API 都遵循特定平台上的标准库调用约定。例如,UNIX 系统使用 C 调用约定,而 Win32 系统使用 __stdcall。

        本机方法也可以使用 RegisterNatives 功能进行显式链接需要注意的是, RegisterNatives 函数可以通过更改给定本地 Java 方法要执行的本地代码来改变 JVM 的记录行为(包括加密算法、正确性、安全性、类型安全性)。因此,请谨慎使用使用 RegisterNatives 函数的本地库应用程序。

2.2.2 本地方法参数

        JNI 接口指针是本地方法的第一个参数。JNI 接口指针的类型是 JNIEnv。第二个参数根据本地方法是静态还是非静态而有所不同。非静态本地方法的第二个参数是对象的引用静态本地方法的第二个参数是对其 Java 类的引用

        其余参数与常规 Java 方法参数相对应。本地方法调用通过返回值将结果传回调用例程。第 3 章:JNI 类型和数据结构,介绍了 Java 和 C 语言类型之间的映射。

        下面的代码示例说明了如何使用 C 语言函数来实现本地方法 f 。本地方法 f 的声明如下:

package p.q.r;class A {native double f(int i, String s);// ...
}

        长名称为 Java_p_q_r_A_f_ILjava_lang_String_2 的 C 语言函数实现了本地方法 f :

jdouble Java_p_q_r_A_f__ILjava_lang_String_2 (JNIEnv *env,        /* interface pointer */jobject obj,        /* "this" pointer */jint i,             /* argument #1 */jstring s)          /* argument #2 */
{/* Obtain a C-copy of the Java string */const char *str = (*env)->GetStringUTFChars(env, s, 0);/* process the string */.../* Now we are done with str */(*env)->ReleaseStringUTFChars(env, s, str);return ...
}

        请注意,我们总是使用接口指针 env 来操作 Java 对象。如下面的代码示例所示,使用 C++ 可以编写稍微简洁的代码:

extern "C" /* specify the C calling convention */jdouble Java_p_q_r_A_f__ILjava_lang_String_2 (JNIEnv *env,        /* interface pointer */jobject obj,        /* "this" pointer */jint i,             /* argument #1 */jstring s)          /* argument #2 */{const char *str = env->GetStringUTFChars(s, 0);// ...env->ReleaseStringUTFChars(s, str);// return ...
}

        在 C++ 中,额外的间接层次和接口指针参数从源代码中消失了。在 C++ 中,JNI 函数被定义为内联成员函数,可扩展为 C 语言的对应函数

2.3 引用 Java 对象

        原始类型(如整数、字符等)可在 Java 和本地代码之间复制而任意 Java 对象则通过引用传递虚拟机必须跟踪所有已传递给本地代码的对象,以免垃圾回收器释放这些对象。反过来,本地代码也必须有办法通知虚拟机它不再需要这些对象。此外,垃圾回收器必须能够移动本地代码引用的对象。

2.3.1 全局和本地引用

        JNI 将本地代码使用的对象引用分为两类:本地引用和全局引用。本地引用在本地方法调用期间有效,并在本地方法返回后自动释放。全局引用在显式释放之前一直有效

        对象以本地引用的形式传递给本地方法。JNI 函数返回的所有 Java 对象都是局部引用。JNI 允许程序员从局部引用创建全局引用。期望返回 Java 对象的 JNI 函数既接受全局引用,也接受局部引用本地方法可将本地引用或全局引用作为结果返回给 VM

        在大多数情况下,程序员应依靠虚拟机在本地方法返回后释放所有本地引用。不过,有时程序员应该显式释放本地引用。例如,请考虑以下情况:

                ①. 本地方法访问大型 Java 对象,从而创建 Java 对象的本地引用。然后,本地方法在返回给调用者之前会执行额外的计算。即使在剩余的计算中不再使用该对象,该大型 Java 对象的本地引用也会阻止该对象被垃圾回收

                ②. 本地方法会创建大量的本地引用,但并非所有引用都会同时使用。由于虚拟机需要一定的空间来跟踪本地引用,创建过多的本地引用可能会导致系统内存不足。例如,一个本地方法在一个大型对象数组中循环,以本地引用的形式检索元素,每次迭代对一个元素进行操作。每次迭代后,程序员就不再需要数组元素的本地引用了。

        JNI 允许程序员在本地方法的任意位置手动删除本地引用。为确保程序员能手动释放本地引用,JNI 函数不允许创建额外的本地引用,但作为结果返回的引用除外。

        本地引用只在创建引用的线程中有效。本地代码不得将本地引用从一个线程传递到另一个线程

2.3.2 本地引用的实现

        为了实现本地引用,Java 虚拟机会为每次从 Java 到本地方法的控制转换创建一个注册表。注册表将不可移动的本地引用映射到 Java 对象,并防止对象被垃圾回收。传递给本地方法的所有 Java 对象(包括作为 JNI 函数调用结果返回的对象)都会自动添加到注册表中。本地方法返回后,注册表将被删除,从而允许对其所有条目进行垃圾回收。

        实现注册表的方法有很多种,例如:使用表、链表或哈希表。虽然可以使用引用计数来避免注册表中的重复条目,但 JNI 实现没有义务检测和删除重复条目

        请注意,本地引用不能通过保守地扫描本地堆栈来忠实地实现。本地代码可能会将本地引用存储到全局或堆数据结构中。

2.4 访问 Java 对象

        JNI 为全局和局部引用提供了丰富的访问函数集。这意味着,无论虚拟机如何在内部表示 Java 对象,都能使用相同的本地方法实现。这也是 JNI 可以被各种虚拟机实现所支持的重要原因。

        通过不透明引用使用访问函数的开销要高于直接访问 C 数据结构的开销。我们相信,在大多数情况下,Java 程序员会使用本地方法来执行一些非繁琐的任务,而这些任务会掩盖该接口的开销。

2.4.1 访问原始数组

        对于包含许多原始数据类型(如整数数组和字符串)的大型 Java 对象来说,这种开销是不可接受的。(考虑一下用于执行向量和矩阵计算的本地方法)。通过函数调用遍历 Java 数组并检索每个元素的效率非常低。

        一种解决方案引入了 "锁定 "的概念,这样本地方法就可以要求虚拟机锁定数组的内容。然后,本地方法会收到指向元素的直接指针。不过,这种方法有两个影响:

                ①. 垃圾回收器必须支持“锁住”功能。

                ②. 虚拟机必须在内存中连续布局基元数组。虽然这对大多数基元数组来说是最自然的实现方式,但布尔数组可以以打包或未打包的方式实现。因此,依赖于布尔数组精确布局的本地代码将无法移植。

        我们采取了一种折中的方法来克服上述两个问题。

        首先,我们提供了一组函数,用于在 Java 数组段和本地内存缓冲区之间复制原始数组元素。如果本地方法只需访问大型数组中的少量元素,则使用这些函数。

        其次,程序员可以使用另一组函数来检索数组元素的固定版本。请记住,这些函数可能需要 Java 虚拟机执行存储分配和复制。这些函数实际上是否复制数组取决于 VM 的实现,具体如下:

                ①. 如果垃圾回收器支持"锁定",且数组布局与本地方法预期的相同,则无需复制。

                ②. 否则,数组将被复制到一个不可移动的内存块(例如,在 C 堆中),并执行必要的格式转换。系统将返回指向拷贝的指针

        最后,接口提供了一些函数,用于通知虚拟机本地代码不再需要访问数组元素。调用这些函数时,系统要么取消数组的锁定,要么将原始数组与其不可移动的副本进行核对,并释放副本

        我们的方法具有灵活性。垃圾回收器算法可以针对每个给定的数组分别做出复制或“锁定”的决定。例如,垃圾回收器可以复制小对象,但“锁定”较大的对象。

        JNI 实现必须确保在多个线程中运行的本地方法可以同时访问同一个数组。例如,JNI 可以为每个被钉住的数组保留一个内部计数器,这样一个线程就不会解除被另一个线程钉住的数组。请注意,JNI 不需要为本地方法的独占访问锁定原始数组不同线程同时更新 Java 数组会导致非确定性结果

2.4.2 访问字段和方法

        JNI 允许本地代码访问 Java 对象的字段和调用 Java 对象的方法。JNI 通过符号名称和类型签名来识别方法和字段。根据字段或方法的名称和签名,分两步计算出查找字段或方法的成本。例如,要调用类 cls 中的方法 f ,本地代码首先要获取一个方法 ID,如下所示:

jmethodID mid = env->GetMethodID(cls, "f", "(ILjava/lang/String;)D");

        这样,本地代码就可以重复使用该方法 ID,而无需花费方法查找的成本,如下所示:

jdouble result = env->CallDoubleMethod(obj, mid, 10, str);

        字段或方法 ID 不会阻止虚拟机卸销毁 ID 所派生的类。类销毁后,方法或字段 ID 将失效,并且不能传递给任何使用该 ID 的函数。因此,本地代码如果打算长期使用某个方法或字段 ID,必须确保:

                ①. 保持对底层类的实时引用,或

                ②. 重新计算方法或字段 ID;

        JNI 对内部如何实现字段和方法 ID 没有施加任何限制。

2.4.2.1 调用对调用者敏感的方法

        少数 Java 方法具有一种特殊属性,称为调用者敏感性。对调用者敏感的方法可以根据直接调用者的身份做出不同的行为。例如,AccessibleObject::canAccess 需要知道调用者才能确定是否可访问。

        当本地代码调用此类方法时,调用栈上可能没有任何 Java 调用者。程序员有责任了解从本地代码调用的 Java 方法是否对调用者敏感,以及如果没有 Java 调用者,这些方法将如何响应。如果有必要,程序员可以提供 Java 代码供本地代码调用,然后本地代码再调用原始 Java 方法

2.5 报告编程错误

        JNI 不会检查编程错误,如:传递 NULL 指针或非法参数类型。非法参数类型包括:使用普通 Java 对象而非 Java 类对象等。JNI 不检查这些编程错误的原因如下:

                ①. 强制 JNI 函数检查所有可能的错误条件会降低正常(正确)本地方法的性能。

                ②. 在许多情况下,没有足够的运行时类型信息来执行这种检查。

        多数 C 库函数都不会防范编程错误。例如, printf() 函数在接收到无效地址时通常会导致运行时错误,而不是返回错误代码。强制 C 库函数检查所有可能的错误条件很可能会导致重复检查:在用户代码中检查一次,然后在库中再检查一次

        程序员不得向 JNI 函数传递非法指针或错误类型的参数。否则可能导致任意后果,包括系统状态损坏或虚拟机崩溃。

也就是说:传递这种空指针异常(nullptr)的检查,由用户负责;

2.6 Java异常

        JNI 允许本地方法引发任意 Java 异常。本地代码也可以处理未处理的 Java 异常。未处理的 Java 异常会传播回虚拟机

2.6.1 异常和错误代码

        某些 JNI 函数使用 Java 异常机制来报告错误条件。在大多数情况下,JNI 函数通过返回错误代码和抛出 Java 异常来报告错误条件。错误代码通常是超出正常返回值范围的特殊返回值(如 NULL)。因此,程序员可以:

                ①. 快速检查最后一次 JNI 调用的返回值,以确定是否发生错误,以及

                ②. 调用 ExceptionOccurred() 函数来获取异常对象,该对象包含对错误条件的更详细描述。

        在两种情况下,程序员需要检查异常,而无法首先检查错误代码:

                ①. 调用 Java 方法的 JNI 函数会返回 Java 方法的结果。程序员必须调用 ExceptionOccurred() 来检查 Java 方法执行过程中可能出现的异常。

                ②. 某些 JNI 数组访问函数不会返回错误代码,但可能会抛出 ArrayIndexOutOfBoundsException ArrayStoreException

        在所有其他情况下,非错误返回值保证没有抛出异常。

2.6.2 异步异常

        一个线程可以通过调用 Thread.stop() 方法在另一个线程中引发异步异常,该方法自 Java 2 SDK 1.2 版起已被弃用。强烈建议程序员不要使用 Thread.stop() ,因为它通常会导致应用程序状态不确定

        此外,JVM 可能会在当前线程中产生异常,但这些异常并不是 JNI API 调用的直接结果,而是因为 JVM 内部的各种错误,例如: 像 StackOverflowError OutOfMemoryError VirtualMachineError 。这些异常也被称为异步异常。

        异步异常不会立即影响当前线程中本地代码的执行,直到出现以下情况:

                ①. 本地代码调用了一个可能引发同步异常的 JNI 函数,或

                ②. 本地代码使用 ExceptionOccurred() 来明确检查同步和异步异常。

        请注意,只有那些可能引发同步异常的 JNI 函数才会检查异步异常。

        本地方法应在必要的地方插入 ExceptionOccurred() 检查,例如:在没有其他异常检查的任何长时间运行代码中(可能包括紧密循环)。这样可以确保当前线程在合理的时间内响应异步异常。不过,由于异步异常的特性,在调用前进行异常检查并不能保证在检查和调用之间不会引发异步异常。

2.6.3 异常处理

        在本地代码中有两种处理异常的方法:

                ①. 本机方法可以选择立即返回,从而在启动本机方法调用的 Java 代码中抛出异常。

                ②. 本地代码可以通过调用 ExceptionClear() 清除异常,然后执行自己的异常处理代码。

        出现异常后,本地代码必须先清除异常,然后才能调用其他 JNI 函数。当出现待处理异常时,可以安全调用的 JNI 函数有:

ExceptionOccurred()
ExceptionDescribe()
ExceptionClear()
ExceptionCheck()
ReleaseStringChars()
ReleaseStringUTFChars()
ReleaseStringCritical()
Release<Type>ArrayElements()
ReleasePrimitiveArrayCritical()
DeleteLocalRef()
DeleteGlobalRef()
DeleteWeakGlobalRef()
MonitorExit()
PushLocalFrame()
PopLocalFrame()
DetachCurrentThread()

下一篇: 03-JNI 类型和数据结构

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

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

相关文章

如何实现桌面美化

一.隐藏桌面图标 1. 在商店里下载TranslucentTB 二.设置底层图标 1.下载Nexus 打开官网&#xff1a; Winstep Nexus Dock and Nexus Ultimate - The Advanced Docking System for Windows 三.设置插件 1.打开致美化官网 致美化 - 最专业的视觉美化交流平台 (zhutix.com) 2.注…

MySQL进阶:全局锁、表级锁、行级锁总结

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;MySQL进阶&#xff1a;MySQL事务、并发事务问题及隔离级别 &#x1f4da;订阅专栏&#xff1a;MySQL进阶 希望文章对你们有所帮助…

swagger在java中的基本使用

自动生成接口文档&#xff0c;和在线接口测试的框架。 导入依赖 <!-- knife4j对swagger进行一个封装--><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><versi…

LeetCode 刷题 [C++] 第121题.买卖股票的最佳时机

题目描述 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔交易中获取的…

Linux笔记--系统相关指令

一、系统资源观察 1. df 指令&#xff08;显示文件系统磁盘使用情况统计&#xff09; disk free&#xff0c;显示文件系统磁盘使用情况统计 #以高可读性的形式显示磁盘使用情况统计 df -h (挂载:利用一个目录当成进入点&#xff0c;将磁盘分区槽的数据放置到该目录下&…

Sqli-labs靶场第14关详解[Sqli-labs-less-14]

Sqli-labs-Less-14 #手工注入 post传参了 根据题目看&#xff0c;像一个登录页面&#xff0c;尝试使用布尔型盲注测试能否登录网站 1. Username输入a" 测试是否会有报错&#xff0c;burp抓包 报错&#xff1a;syntax to use near "a"" and password&q…

排序——手撕快排

本节复习快速排序&#xff0c; 快排我们要讲三个版本&#xff1a;一种是霍尔大佬的原版版本&#xff0c; 也就是快速排序的原版。 一种挖坑法。还有一种前后指针法。 首先我们应该知道&#xff0c;三个版本针对的是单趟进行排序的方法不同。 而多趟使用的是递归或者非递归模拟…

Linux 任务进程命令练习

1、通过ps命令的两种选项形式查看进程信息 2、通过top命令查看进程 3、通过pgrep命令查看sshd服务的进程号 4、查看系统进程树 5、使dd if/dev/zero of/root/file bs1M count8190 命令操作在前台运行 6、将第5题命令操作调入到后台并暂停 7、使dd if/dev/zero of/root/file2 bs…

v71.字符串计算

1.字符串 输入和输出 其中scanf("%s",string);读入数据的时候是很微妙的 输入的是Hello world!,输出结果是Hello#。 scanf函数只会读取一段单词&#xff08;字母紧靠一起&#xff09;&#xff0c;遇到回车、空格或者tab就会停止。但是scanf函数的读入是不安全的&am…

艾尔登法环备份存档方法

1.PC端使用WinR输入%AppData%\EldenRing 2.如图创建文件夹“我这取名叫备份存档”&#xff0c;将其中的三个文件复制到新建的文件夹中 3.理论上只需要备份替换ER0000.sl2文件即可

推荐6款SSH远程连接工具

1、Xshell 介绍&#xff1a; xshell是一个非常强大的安全终端模拟软件&#xff0c;它支持SSH1, SSH2, 以及Windows平台的TELNET 协议。Xshell可以在Windows界面下用来访问远端不同系统下的服务器&#xff0c;从而比较好的达到远程控制终端的目的。 业界最强大的SSH客户机 官…

新加坡大带宽服务器概览

随着全球互联网的迅猛发展&#xff0c;服务器作为支撑网络应用的重要基础设施&#xff0c;扮演着越来越重要的角色。新加坡&#xff0c;作为亚洲四小龙之一&#xff0c;其服务器市场也备受关注。特别是新加坡的大带宽服务器&#xff0c;更是受到了众多企业和个人的青睐。那么&a…