Windows COM技术:COM介绍、代码演示。

目录

步骤一:理解COM技术

介绍COM的基础知识

1. COM的目的和特点

2. COM的关键概念

3. COM的实现

4. COM与DCOM、ActiveX

讨论COM的用途

1. 软件自动化

2. 插件和扩展

3. 跨语言开发

4. 分布式计算

5. 系统级组件

6. 网络浏览器插件

步骤二:设置开发环境

步骤三:编写COM组件

1. 定义COM接口

定义接口和CLSID

2. 实现接口

3. 注册组件

步骤四:使用COM组件

自动调用,需注册DLL

手动调用

步骤五:单元测试

单元测试策略

步骤六:更新DLL并兼容老的

1. 使用版本控制

2. 使用COM的接口继承

示例:加入新功能到COM DLL

出现的问题及其成因


步骤一:理解COM技术

介绍COM的基础知识

        件对象模型(COM)是一个由微软开发的软件架构,旨在促进不同软件组件之间的二进制交互。COM定义了一种方法,使得在各种编程语言中编写的组件可以相互通信,不仅在同一个程序内部,而且可以在不同的计算机上。以下是对COM基础知识的更详尽的解释:

1. COM的目的和特点

        COM是为了解决软件开发中的可重用性、灵活性和版本控制等问题而设计的。其主要特点包括:

  • 语言中立性:COM允许用任何支持COM的编程语言(如C++, Visual Basic, Delphi等)编写的组件彼此交互。
  • 二进制互操作性:COM组件以二进制标准进行通信,这意味着可以在不重新编译调用者代码的情况下替换组件。
  • 位置透明性:COM组件可以在本地机器上运行,也可以通过网络在远程机器上运行。

2. COM的关键概念

        理解COM的工作原理涉及几个核心概念:

  • 接口:COM使用接口与实现分离的方式,组件提供的功能通过一组严格定义的接口暴露。接口是一组函数的集合,类似于C++中的纯虚函数的类定义。
  • GUID和IID:每个COM接口和组件都有一个全局唯一标识符(GUID),接口的GUID也称为接口ID(IID)。这些标识符确保COM的注册和引用是唯一的。
  • COM库和注册表:COM组件在系统中的注册依靠COM库来管理,每个组件的信息(包括位置和可用的接口)都记录在Windows注册表中。
  • 引用计数:COM使用引用计数来管理内存和生命周期,组件负责跟踪有多少客户端正在使用它,并相应地管理其生命周期。

3. COM的实现

        在实际应用中,一个COM对象通常会通过以下步骤实现和使用:

  • 创建实例:客户端通过CoCreateInstance()等函数创建COM对象实例。
  • 接口查询:使用IUnknown接口的QueryInterface方法来查询支持的接口。IUnknown是所有COM接口的基接口,提供了对象生命周期管理和接口查询的基本方法。
  • 调用接口:一旦获取了接口的指针,客户端就可以调用其方法来执行操作。
  • 释放接口:完成操作后,客户端需要调用接口的Release方法来减少其引用计数,当引用计数达到0时,COM对象将自行销毁。

4. COM与DCOM、ActiveX

  • DCOM:分布式组件对象模型(DCOM)是COM的扩展,支持在网络上的不同计算机之间通信。
  • ActiveX:ActiveX控件是基于COM的一种特殊形式,用于在网页上嵌入和执行特定功能的组件。

        COM为开发者提供了一种强大的机制,用于创建可在多个程序之间共享的模块化组件。尽管现代软件开发中已经出现了许多新的技术和方法,但在需要高度稳定和兼容性的大型企业应用中,COM依然具有其独特的价值。通过确保组件可以被不同语言编写的应用程序使用,并且可以被安全地更新和替换,COM帮助软件系统实现了更好的维护性和

讨论COM的用途

        组件对象模型(COM)是一个为了提高软件模块化和可重用性而设计的技术。自从1990年代初期由微软引入以来,它已经在各种应用中证明了其价值。COM的设计允许开发者创建灵活、可重用的组件,这些组件可以被不同的应用程序在不同的环境中使用,而不需要了解组件的内部实现细节。以下是一些具体的COM用途:

1. 软件自动化

        COM广泛用于办公软件的自动化。通过COM接口,应用程序(如Microsoft Office套件)可以暴露其功能给外部脚本或程序,允许自动化复杂的任务。例如,一个企业可能使用VBA(Visual Basic for Applications)脚本与Excel交互,自动化报表的生成和数据分析。

2. 插件和扩展

        COM使得软件开发者能够为他们的应用程序创建插件架构,其他开发者可以为这些应用程序开发添加功能的插件。这种插件通常以DLL的形式实现,并通过COM接口与主应用程序通信。例如,图形处理软件如Photoshop可以通过COM插件来扩展其图像处理能力。

3. 跨语言开发

        由于COM的语言无关性,使用不同编程语言开发的组件可以互相操作。这意味着一个用C++编写的组件可以被一个用Visual Basic或C#开发的应用程序使用,反之亦然。这种特性极大地增加了不同软件项目间代码的可重用性。

4. 分布式计算

        通过DCOM(分布式组件对象模型),COM技术扩展到网络。DCOM允许在不同计算机上运行的组件通过网络通信,支持构建分布式应用。这在处理大型企业级应用中尤其有用,例如金融服务领域的数据处理和实时交易系统。

5. 系统级组件

        COM还用于操作系统级别的功能扩展,如Shell扩展处理器、服务组件等。Windows操作系统本身就广泛使用COM为开发者提供可扩展的API,使得第三方开发者可以创建与Windows深度集成的软件解决方案。

6. 网络浏览器插件

ActiveX控件,一种基于COM的技术,曾经是实现浏览器功能扩展的主要方式,允许网页通过嵌入的ActiveX组件来提供富交互性应用,虽然现在由于安全考虑,其使用已经大幅减少。

        COM技术的设计初衷是促进软件组件的重用,降低开发成本,并加速开发过程。虽然现代软件开发中已经有了更现代的技术(如.NET Framework),但COM因其强大的跨语言和跨平台的互操作性,在很多现存的系统中仍然保持着其重要性。通过灵活使用COM,软件开发者可以创造出更加模块化、易于管理和维护的应用程序。

步骤二:设置开发环境

安装必要的工具:需要Visual Studio和Windows SDK。

Microsoft Visual Studio C++2017+Windows 11 SDK环境_microsoft visual c++ 2017-CSDN博客

配置项目:创建一个新的Win32项目,并设置为DLL类型,因为大多数COM组件都是以DLL形式发布。

Visual Studio 2022如何创建Win32项目_vs2022怎么创建win32项目-CSDN博客

步骤三:编写COM组件

1. 定义COM接口

定义接口和CLSID

        MathOperations.h

#include <windows.h>
#include <Unknwnbase.h>// Interface ID (IID) for IMathOperations
// {12345678-1234-1234-1234-123456789012}
static const IID IID_IMathOperations = 
{ 0x12345678, 0x1234, 0x1234, { 0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12 } };// Class ID (CLSID) for MathOperations
// {87654321-4321-4321-4321-210987654321}
static const CLSID CLSID_MathOperations = 
{ 0x87654321, 0x4321, 0x4321, { 0x43, 0x21, 0x21, 0x09, 0x87, 0x65, 0x43, 0x21 } };// Define the IMathOperations interface
class IMathOperations : public IUnknown
{
public:virtual HRESULT STDMETHODCALLTYPE Add(int a, int b, int* result) = 0;virtual HRESULT STDMETHODCALLTYPE Subtract(int a, int b, int* result) = 0;
};

2. 实现接口

        实现COM接口和类,并提供必要的方法和引用计数逻辑。

        MathOperations.cpp

#include "ComTest.h"class MathOperations : public IMathOperations
{volatile long refCount;
public:MathOperations() : refCount(1) {}// IUnknown methodsHRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override {if (riid == IID_IUnknown || riid == IID_IMathOperations) {*ppvObject = static_cast<IMathOperations*>(this);this->AddRef();return S_OK;}*ppvObject = NULL;return E_NOINTERFACE;}ULONG STDMETHODCALLTYPE AddRef() override {return InterlockedIncrement(&refCount);}ULONG STDMETHODCALLTYPE Release() override {ULONG res = InterlockedDecrement(&refCount);if (res == 0) delete this;return res;}// IMathOperations methodsHRESULT STDMETHODCALLTYPE Add(int a, int b, int* result) override {*result = a + b;return S_OK;}HRESULT STDMETHODCALLTYPE Subtract(int a, int b, int* result) override {*result = a - b;return S_OK;}
};extern "C" __declspec(dllexport) HRESULT CreateInstance(REFIID riid, void** ppv) {MathOperations* pMath = new MathOperations();HRESULT hr = pMath->QueryInterface(riid, ppv);pMath->Release();  // release initial referencereturn hr;
}

3. 注册组件

        注册COM组件通常涉及向Windows注册表添加条目,以便系统可以找到并实例化COM对象。这可以通过手动添加注册表键值或使用注册表函数在安装过程中自动完成。

        也可以直接调用dll,无需注册。

步骤四:使用COM组件

        为了调用上文定义的 COM DLL,我们需要编写一个客户端程序,该程序使用 COM 组件进行通信。以下是如何在 C++ 中编写调用 MathOperations COM 组件的代码。此代码演示了如何初始化 COM,创建组件实例,调用接口方法,以及最后如何清理。

自动调用,需注册DLL

#include <iostream>
#include <windows.h>
#include "MathOperations.h"  // 包含COM接口定义int main() {HRESULT hr = CoInitialize(NULL);  // 初始化COM库if (FAILED(hr)) {std::cout << "Failed to initialize COM library." << std::endl;return -1;}IMathOperations* pMathOps = nullptr;  // 指向接口的指针// 创建组件实例hr = CoCreateInstance(CLSID_MathOperations,   // 组件的CLSIDNULL,                   // 没有外部聚合CLSCTX_INPROC_SERVER,   // DLL运行在相同的进程IID_IMathOperations,    // 请求的接口IID(void**)&pMathOps);     // 指针存放位置if (SUCCEEDED(hr)) {int result = 0;// 调用Add方法hr = pMathOps->Add(5, 3, &result);if (SUCCEEDED(hr)) {std::cout << "Addition Result: " << result << std::endl;}// 调用Subtract方法hr = pMathOps->Subtract(5, 3, &result);if (SUCCEEDED(hr)) {std::cout << "Subtraction Result: " << result << std::endl;}// 释放接口pMathOps->Release();} else {std::cout << "Failed to create component instance." << std::endl;}CoUninitialize();  // 清理COMreturn 0;
}

关键步骤解释

  1. 初始化 COM

    • 使用 CoInitializeCoInitializeEx 初始化 COM 库,这是使用 COM 组件前必需的步骤。
  2. 创建 COM 对象实例

    • 使用 CoCreateInstance 函数来创建 COM 组件的实例。这个函数需要 CLSID 来找到正确的组件,还需要 IID 来获取指定的接口。
  3. 调用接口方法

    • 一旦获取了接口指针,就可以调用定义的方法。此示例中调用了 AddSubtract 方法。
  4. 释放接口

    • 完成操作后,需要调用接口的 Release 方法来减少引用计数。当引用计数达到零时,COM 对象会被销毁。
  5. 清理 COM

    • 在程序结束前,使用 CoUninitialize 清理 COM 环境。

        这段代码假设 COM 组件已经正确注册在系统上,且客户端和服务器共享接口定义(头文件)。这是在同一台机器上或已经通过某种方式共享了头文件的情况。在实际部署中,通常需要将接口的定义(通常是 IDL 文件或编译后的类型库)与客户端开发者共享。

手动调用

        如果您想要手动加载 DLL 而不是使用 CoCreateInstance() 来自动加载,可以采用显式加载的方式。这通常涉及使用 Windows API 如 LoadLibraryGetProcAddress 来动态加载 DLL 并获取函数指针。这种方式对于 COM 组件来说稍微复杂,但可行,尤其是在某些特定环境中,如当你没有注册 COM 组件到系统注册表时。 

#include <iostream>
#include <windows.h>
#include "MathOperations.h"  // 包含COM接口定义// 定义函数指针类型
typedef HRESULT (*PFN_CREATE_INSTANCE)(REFIID riid, void** ppv);int main() {HMODULE hDll = LoadLibrary(TEXT("MathOperations.dll"));  // 动态加载DLLif (hDll == NULL) {std::cout << "Failed to load DLL." << std::endl;return -1;}// 获取函数指针PFN_CREATE_INSTANCE pfnCreateInstance = (PFN_CREATE_INSTANCE)GetProcAddress(hDll, "CreateInstance");if (pfnCreateInstance == NULL) {std::cout << "Failed to get function address." << std::endl;FreeLibrary(hDll);return -1;}HRESULT hr = CoInitialize(NULL);  // 初始化COM库if (FAILED(hr)) {std::cout << "Failed to initialize COM library." << std::endl;FreeLibrary(hDll);return -1;}IMathOperations* pMathOps = nullptr;  // 指向接口的指针// 创建组件实例hr = pfnCreateInstance(IID_IMathOperations, (void**)&pMathOps);if (SUCCEEDED(hr)) {int result = 0;// 调用Add方法hr = pMathOps->Add(5, 3, &result);if (SUCCEEDED(hr)) {std::cout << "Addition Result: " << result << std::endl;}// 调用Subtract方法hr = pMathOps->Subtract(5, 3, &result);if (SUCCEEDED(hr)) {std::cout << "Subtraction Result: " << result << std::endl;}// 释放接口pMathOps->Release();} else {std::cout << "Failed to create component instance." << std::endl;}CoUninitialize();  // 清理COMFreeLibrary(hDll); // 释放DLLreturn 0;
}

关键改动说明:

  1. 加载 DLL

    • 使用 LoadLibrary 加载 DLL 文件。这需要 DLL 文件的路径,路径可以是绝对或相对的。
  2. 获取函数地址

    • 使用 GetProcAddress 获取 DLL 中导出函数的地址。这里的 CreateInstance 是假设你的 DLL 中导出了一个创建 COM 对象的函数。
  3. 手动创建实例

    • 使用获取到的函数指针 pfnCreateInstance 来创建 COM 对象的实例。
  4. 释放资源

    • 使用 FreeLibrary 释放加载的 DLL。这是确保资源正确管理的重要步骤。

步骤五:单元测试

        讨论如何为COM组件编写和执行单元测试,确保其功能正确。

单元测试策略

  1. 测试环境设置

    • 确保COM环境已正确初始化。
    • 加载COM组件,以便在测试期间使用。
  2. 测试案例设计

    • 对每个方法执行正常值测试。
    • 对每个方法进行边界值测试。
    • 对每个方法进行错误处理测试,例如输入无效参数。
  3. 资源清理

    • 测试完成后,正确释放所有资源。
#include "pch.h"
#include "CppUnitTest.h"
#include "../MathOperations/MathOperations.h"  // 包含MathOperations接口定义using namespace Microsoft::VisualStudio::CppUnitTestFramework;namespace MathOperationsTests
{TEST_CLASS(MathOperationsTests){public:IMathOperations* pMathOps = nullptr;// 测试初始化TEST_METHOD_INITIALIZE(Setup){CoInitialize(NULL);  // 初始化COMHRESULT hr = CoCreateInstance(CLSID_MathOperations, NULL, CLSCTX_INPROC_SERVER,IID_IMathOperations, (void**)&pMathOps);Assert::IsTrue(SUCCEEDED(hr));}// 测试清理TEST_METHOD_CLEANUP(Teardown){if (pMathOps){pMathOps->Release();pMathOps = nullptr;}CoUninitialize();  // 清理COM}// 测试Add方法TEST_METHOD(TestAdd){int result = 0;HRESULT hr = pMathOps->Add(10, 20, &result);Assert::AreEqual(S_OK, hr);Assert::AreEqual(30, result);}// 测试Subtract方法TEST_METHOD(TestSubtract){int result = 0;HRESULT hr = pMathOps->Subtract(30, 10, &result);Assert::AreEqual(S_OK, hr);Assert::AreEqual(20, result);}};
}

步骤六:更新DLL并兼容老的

1. 使用版本控制

        在DLL中,通常通过接口的版本控制来保持向后兼容性。为此,你可以:

  • 保留旧接口不变:确保原有的接口不发生变化,以保证依赖于这些接口的现有应用程序可以继续无缝工作。
  • 新增接口:为新功能创建新的接口。这可以通过继承原有接口并添加新方法来实现,或者定义一个完全独立的新接口。

2. 使用COM的接口继承

对于COM组件,接口继承是保持老版本兼容的常见方法。具体步骤如下:

  • 定义新接口:基于现有的接口(如 IMathOperations),你可以定义一个新接口(如 IMathOperations2),在其中加入新的方法。
  • 实现新接口:在COM类中实现这个新接口,同时保留对旧接口的支持。

示例:加入新功能到COM DLL

        假设原来的DLL提供了加法和减法功能,现在需要添加一个乘法功能。

#include <windows.h>
#include <Unknwnbase.h>// Interface ID (IID) for IMathOperations
// {12345678-1234-1234-1234-123456789012}
static const IID IID_IMathOperations =
{ 0x12345678, 0x1234, 0x1234, { 0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12 } };// Class ID (CLSID) for MathOperations
// {87654321-4321-4321-4321-210987654321}
static const CLSID CLSID_MathOperations =
{ 0x87654321, 0x4321, 0x4321, { 0x43, 0x21, 0x21, 0x09, 0x87, 0x65, 0x43, 0x21 } };// Define the IMathOperations interface
class IMathOperations : public IUnknown
{
public:virtual HRESULT STDMETHODCALLTYPE Add(int a, int b, int* result) = 0;virtual HRESULT STDMETHODCALLTYPE Subtract(int a, int b, int* result) = 0;
};// Interface ID (IID) for IMathOperations2
// {98765432-4321-4321-4321-123456789012}
static const IID IID_IMathOperations2 =
{ 0x98765432, 0x4321, 0x4321, { 0x43, 0x21, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12 } };class IMathOperations2 : public IMathOperations
{
public:virtual HRESULT STDMETHODCALLTYPE Multiply(int a, int b, int* result) = 0;
};

 LoadDLL:这里注意:

        IMathOperations2* pMathOps = nullptr; 这个指针不能生命错了,需要是新更新的类,更新的类里有继承老的类,所以这个IMathOperations2*调用老方法也没问题。

#include <iostream>
#include <windows.h>
#include "../COMTest/ComTest.h"  // 包含COM接口定义// 定义函数指针类型
typedef HRESULT(*PFN_CREATE_INSTANCE)(REFIID riid, void** ppv);int main() {HMODULE hDll = LoadLibrary(TEXT("COMTest.dll"));  // 动态加载DLLif (hDll == NULL) {std::cout << "Failed to load DLL." << std::endl;return -1;}// 获取函数指针PFN_CREATE_INSTANCE pfnCreateInstance = (PFN_CREATE_INSTANCE)GetProcAddress(hDll, "CreateInstance");if (pfnCreateInstance == NULL) {std::cout << "Failed to get function address." << std::endl;FreeLibrary(hDll);return -1;}HRESULT hr = CoInitialize(NULL);  // 初始化COM库if (FAILED(hr)) {std::cout << "Failed to initialize COM library." << std::endl;FreeLibrary(hDll);return -1;}IMathOperations2* pMathOps = nullptr;  // 指向接口的指针// 创建组件实例hr = pfnCreateInstance(IID_IMathOperations2, (void**)&pMathOps);if (SUCCEEDED(hr)) {int result = 0;// 调用Add方法hr = pMathOps->Add(5, 3, &result);if (SUCCEEDED(hr)) {std::cout << "Addition Result: " << result << std::endl;}// 调用Subtract方法hr = pMathOps->Subtract(5, 3, &result);if (SUCCEEDED(hr)) {std::cout << "Subtraction Result: " << result << std::endl;}hr = pMathOps->Multiply(5, 3, &result);if (SUCCEEDED(hr)) {std::cout << "Subtraction Result: " << result << std::endl;}// 释放接口pMathOps->Release();}else {std::cout << "Failed to create component instance." << std::endl;}CoUninitialize();  // 清理COMFreeLibrary(hDll); // 释放DLLsystem("pause");return 0;
}

出现的问题及其成因

  1. GetProcAddress返回空指针:

    • 原因可能包括函数名称修饰(name mangling)错误、DLL未正确加载或者指定的函数未正确导出。解决方法包括使用extern "C"来避免C++的名称修饰,并确保使用__declspec(dllexport)正确导出函数。
  2. COM接口的IID定义和使用:

    • 在C++中定义新接口时需要提供全新的IID。这是因为每个COM接口必须有一个全局唯一的标识符。未正确处理这一点可能导致接口不可识别或引用错误。
  3. 代码示例中缺少导出声明:

    • 最初的示例中未包括__declspec(dllexport),这导致了动态链接库中的函数不能被外部访问。这反映了在COM开发过程中对DLL导出规则的关注不足。
  4. 保持向后兼容性的方法:

    • 展示了如何通过继承和扩展现有COM接口来添加新功能,而不会影响依赖旧接口的现有应用程序。这是软件维护中非常重要的策略,以避免引入破坏性变更。

         COM编出来的dll可维护性极高。

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

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

相关文章

【嵌入式Linux】STM32P1开发环境搭建

要进行嵌入式Linux开发&#xff0c;需要在Windows、Linux和嵌入式Linux3个系统之间来回跑&#xff0c;需要使用多个软件工具。经过了4小时的安装&#xff08;包括下载时间&#xff09;&#xff0c;我怕以后会忘记&#xff0c;本着互利互助的原则&#xff0c;我打算把这些步骤详…

线性代数基础3 行列式

行列式 行列式其实在机器学习中用的并不多&#xff0c;一个矩阵必须是方阵&#xff0c;才能计算它的行列式 行列式是把矩阵变成一个标量 import numpy as np A np.array([[1,3],[2,5]]) display(A) print(矩阵A的行列式是&#xff1a;\n,np.linalg.det(A))array([[1, 3],[2, …

Tomcat核心组件深度解析

Server组件 Service组件 连接器Connector组件 容器Container组件

Vue3——组件基础

组件基础 1. 组件定义与使用 1.1 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>组件基础&l…

linux中如何挂载yum云仓库进行软件的安装

1.首先在根目录下建立文件&#xff0c;用来挂载镜像文件 [rootclient ~]# mkdir /rhel9 2.挂载镜像文件&#xff1a; [rootclient ~]# mount /dev/cdrom /rhel9 3.切换到 /etc/yum.repos.d 下的目录并查看 &#xff0c;创建 rhel9.repo文件&#xff0c;并编辑云仓库域名&am…

ubuntu在xshell中使用快捷方式操作命令,减少命令行的数入量

第一步 第二步 然后无脑确定 第三步 在xshell的显示方式 方式一 这样就会在每个窗格中进行显示 方式二 效果显示–> 这种窗格的显示是全局的 然后你双击这个process就会自动把命令打在命令行上&#xff0c;减少你的输入量

操作教程丨MaxKB+Ollama:快速构建基于大语言模型的本地知识库问答系统

2024年4月12日&#xff0c;1Panel开源项目组正式对外介绍了其官方出品的开源子项目——MaxKB&#xff08;github.com/1Panel-dev/MaxKB&#xff09;。MaxKB是一款基于LLM&#xff08;Large Language Model&#xff09;大语言模型的知识库问答系统。MaxKB的产品命名内涵为“Max …

JUC面试——⭐⭐Java中的四种引用类型/Threadlocal

四种引用类型 Java 中对象的引用分为四种级别&#xff0c;这四种级别由高到低依次为&#xff1a;强引用、软引用、弱引用和虚引用。 基础知识 强引用&#xff1a;普通使用的引用 强引用是造成 Java 内存泄漏的主要原因之一 软引用&#xff1a; GC内存不够时回收 适用于&…

4.18学习总结

多线程补充 等待唤醒机制 现在有两条线程在运行&#xff0c;其中一条线程可以创造一个特殊的数据供另一条线程使用&#xff0c;但这个数据的创建也有要求&#xff1a;在同一时间只允许有一个这样的特殊数据&#xff0c;那么我们要怎样去完成呢&#xff1f;如果用普通的多线程…

路由器热备份

HSRP HSRP&#xff08;Hot Standby Routing Protocol&#xff09;热备份路由选择协议 HSRP是思科私有的协议&#xff0c;HSRP起到一个双网关热备份的一个目的&#xff0c;不考虑线路问题针对设备而言&#xff0c;一个设备挂了还有另外一台设备&#xff0c;所以双网关也叫双机…

IntelliJ IDEA运行发布传统Java Web Application项目

接 重温8年前项目部署 要求&#xff0c;如何改用IntelliJ IDEA运行发布传统 Java Web Application项目呢&#xff0c;简述步骤如下&#xff1a; 一、下载源码 源码&#xff1a;https://github.com/wysheng/kindergarten 下载后的本地项目路径&#xff1a;/Users/songjianyon…

C语言Linux vim shell命令

1. actionmotion dG删到文件尾 ggdG先到开头再删除到末尾 d^到达行首 d$到行尾 2. num action 2dd删除两行 t"向后寻找"找到&#xff0c;找到前面一个位置 f"向后寻找"找到&#xff0c;直接找到本来的位置 diw删除单词并保持在视图状态&#xff…