65.9K
CodeProject 正在变化。 阅读更多。
Home

在后台使用 Windows 服务和 .NET Remoting 管理服务器远程启动/关机

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (5投票s)

2008 年 1 月 6 日

CPOL

9分钟阅读

viewsIcon

41857

downloadIcon

1987

通过多个客户端自动远程控制服务器的启动和关闭,以确保服务器仅在客户端活动时运行。使用 Wake On Lan、Windows 服务和 .NET Remoting。

目录

引言

在家里,我使用服务器进行存储和媒体流传输,但设备 24/7 的功耗让我很烦恼。Wake On Lan 有点帮助,但我经常忘记关闭设备。我也尝试过电源管理,但效果不佳。

我需要为一个软件项目研究 .NET Remoting 架构,所以我开始了 LDM 这个小演示项目。目标是让服务器仅在客户端活动时运行,而无需用户交互——它必须通过“妻子测试” :-)

架构

每个客户端上都安装了 Windows 服务,在启动时唤醒服务器,然后发送心跳信号,直到客户端关闭。一个服务器应用程序也作为 Windows 服务运行,监听所有客户端心跳,并在特定时间内没有心跳时关闭服务器。

由于客户端服务甚至在用户登录客户端机器之前就启动(并唤醒服务器),因此服务器可以很快地使用。

为了轻松处理这一切,还有两个应用程序:一个客户端服务的用户界面可视化心跳并允许一些手动交互;一个单独的配置应用程序配置运行在机器上的客户端服务,如果已连接,还可以远程配置服务器。

应用程序

  • ServerRemoteControl:运行在服务器机器上的 Windows 服务。
  • ServerRemoteClientService:运行在客户端机器上的 Windows 服务。
  • ServerRemoteClientForm:ServerRemoteClientService 的用户界面。
  • ServerRemoteConfig:客户端和服务器应用程序的配置工具。

主题

  • Remoting:不同应用程序之间的所有通信都使用可远程调用的对象解决。这既是客户端/服务器通信,也是 Windows 服务/UI 通信。
  • Windows 服务:客户端和服务器应用程序作为 Windows 服务运行,无需登录机器即可运行它们。
  • 应用程序主机 / 应用程序边界:使用 Remoting 技术在不同应用程序主机之间进行通信。
  • Wake On LAN:客户端应用程序发送一个 WOL “魔术包”来唤醒服务器。感谢 maxburov 的代码示例
  • 注册表访问:配置参数和窗口位置存储在 Windows 注册表(HKEY_LOCAL_MACHINE\SOFTWARE\Torkelware\ServerRemote)中,Windows 服务在 NetworkService 账户下运行也能访问。
  • 多线程 Windows 窗体:由于有单独的应用程序充当用户界面,我实现了一种基于消息的应用程序逻辑和用户界面之间的通信。
  • 任务栏图标:客户端服务的用户界面最小化到系统托盘,仅在需要时才打开窗口。

应用程序示意图

ServerRemote.jpg

类 / 方法

MessageService

消息服务是一个单例对象,提供可远程调用的方法,用于向其他应用程序发送消息和修改本地配置。可以通过 Remoting 订阅相应的事件来接收消息。甚至托管该对象的应用程序也通过 Remoting 访问其方法。

通过调用静态方法 CreateMessageServiceServer()MessageService 创建通信通道,将自身注册为单例对象,并返回通过 Remoting 使用 Activator.GetObject() 创建的实例。

public static MessageService CreateMessageServiceServer(MessageProtocol MsgProtocol, 
       int ServerPort, 
       string ServerUri, 
       string ApplicationName) {
       MessageService Result;

    // channels registrieren:
    BinaryServerFormatterSinkProvider ServChSinkProvider = 
        new BinaryServerFormatterSinkProvider();
    ServChSinkProvider.TypeFilterLevel = TypeFilterLevel.Full;
    BinaryClientFormatterSinkProvider ClientChSinkProvider = 
        new BinaryClientFormatterSinkProvider();
    if (MsgProtocol==MessageProtocol.http) {
        HttpServerChannel ServCh = new HttpServerChannel(
            "MessageServiceServerChannel", ServerPort, ServChSinkProvider);
        ChannelServices.RegisterChannel(ServCh);
        HttpClientChannel channel = new HttpClientChannel(
            "MessageServiceClientChannel", ClientChSinkProvider);
        ChannelServices.RegisterChannel(channel);
    } else if ((MsgProtocol==MessageProtocol.tcp)) {
        TcpServerChannel ServCh = new TcpServerChannel(
            "MessageServiceServerChannel", ServerPort, ServChSinkProvider);
        ChannelServices.RegisterChannel(ServCh);
        TcpClientChannel channel = new TcpClientChannel(
            "MessageServiceClientChannel", ClientChSinkProvider);
        ChannelServices.RegisterChannel(channel);
    } else {
        throw new ApplicationException(
            "Netzwerkprotokoll nicht implementiert: " + 
            MsgProtocol.ToString());
    }

    // Register remoting object:
    Debug.WriteLine("Registriere Remote-Objekt: " + 
          MsgProtocol.ToString() + ":" + 
          ServerPort + "/" + ServerUri);
    WellKnownServiceTypeEntry myservice = new WellKnownServiceTypeEntry(
        typeof(MessageService),
        ServerUri,
        WellKnownObjectMode.Singleton);
    RemotingConfiguration.RegisterWellKnownServiceType(myservice);

    // Server bezieht das selbst gehostete messageobjekt via remoting:
    string HostUrl = string.Format(@"{0}://{1}:{2}/{3}",
        MsgProtocol.ToString(),"localhost",ServerPort.ToString(), ServerUri);
    Debug.WriteLine("Verbinde Server zu MessageObject: " + HostUrl);
    Result = (MessageService)Activator.GetObject(
        typeof(MessageService),
        HostUrl);

    // testen, ob es sich bei dem messageobjekt um ein remotingobjekt handelt:
    if (RemotingServices.IsTransparentProxy(Result)) {
        Result.ApplicationName = ApplicationName;
        Result.ApplicationHost = System.Environment.MachineName;
    } else {
        throw new ApplicationException(
            "Fehler beim erstellen des Remoteobjekts.");
    }
    return Result;
}

方法 PublishCommand()MessageService.TextMessageEventHandler 的所有订阅者发送类型为“Command”的文本消息

public void PublishCommand(string Command, string SenderName) {
    TextMessageEventArgs e = new TextMessageEventArgs(Command);
    e.MessageType = TextMessageType.Command;
    e.SenderName = SenderName;
    this.MsgArrSync.Add(e);
    if (TextMessageEventHandler != null) {
        TextMessageEventHandler(this,e);
    }
}

RemotelyDelegatableObject

为了能够从另一个应用程序,或者可能从不同的机器订阅 MessageService 事件,使用一个抽象类来派生回调类。通过让发送方和接收方基于相同的(抽象)类,我们消除了两个应用程序使用完全相同版本回调类的需要。

public abstract class RemotelyDelegatableObject : MarshalByRefObject {
    public void TextMessageReceiver (object sender, TextMessageEventArgs e) {
        InternalTextMessageReceiver (sender, e) ;        
    }
    protected abstract void InternalTextMessageReceiver (
        object sender, TextMessageEventArgs e) ;
}

TextMessageEventArgs

可以发送两种类型的消息:“Message”和“Command”,定义在 public enum TextMessageType {Message, Command} 中。这些消息由“TextMessageEventArgs”类封装,该类标记为 [Serializable],并简单地包含一些带有适当公共属性的私有变量来访问它们。

ServerRemoteControl

服务器应用程序托管一个 MessageService 并订阅传入文本消息触发的事件。一个单独的线程运行一个倒计时,如果倒计时归零,则关闭服务器。倒计时可以通过通过 MessageService 传入的“心跳”命令重置为其初始值,其初始值可以通过“updateconfiguration”命令更改,例如。此外,它还提供了附加监视应用程序以监视倒计时和传入心跳的功能,就像 ServerRemoteConfig 所做的那样。

StartServerApp() 方法创建单例 MessageService 远程对象并订阅消息事件。

private void StartServerApp() {
    // Remoteobjekte und Channels registrieren:
    MyMessageService = MessageService.CreateMessageServiceServer(
        MessageProtocol.http, 
        this.ServerPort,
        "RemoteControl",
        "Remote Control Server Application");
    ServerCallback = new MyServerCallbackClass (this) ;
    MyMessageService.TextMessageEventHandler += 
        new TextMessageHandler(ServerCallback.TextMessageReceiver);
}

PerfLoop() 方法在单独的线程中运行,执行倒计时,如果达到零,则关闭服务器。

private void PerfLoop() {
    while (true) {
        lock (this) {
            if (this.CountDown--==0) {
                AppendToTextBox(System.DateTime.Now.ToString() + ": Shutdown!");
                PerformServerShutdown();
            }
        }
        UIMessage("SetControlValue", "lblCountDown" + 
                  ":" + CountDown.ToString());
        PublishMonitorCommand(
            "CountDown:" + CountDown.ToString(), Environment.MachineName);
        Thread.Sleep(1000);
    }
}

PerformServerShutdown() 只是创建一个新进程并执行配置的关闭命令。

public void PerformServerShutdown() {
    Process myProcess = new Process();
    myProcess.StartInfo.FileName = this.ShutDownCommand;
    myProcess.StartInfo.Arguments = this.ShutDownParams;
    myProcess.Start();
} 

Server_ReceiveTextMessage() 方法执行命令,或者在连接用户界面时简单地显示文本消息。

public void Server_ReceiveTextMessage(object sender, TextMessageEventArgs e) {
    if (e.MessageType==TextMessageType.Command) {
        string[] MsgArr = e.Message.Split(":"[0]);
        if (MsgArr[0].ToLower().Equals("heartbeat")) {
            lock (this) {
                this.CountDown = this.CountDownStartValue;
            }
            UIMessage("ShowHeartBeat", e.SenderName);
            UIMessage("SetControlValue", 
                      "lblCountDown:" + this.CountDown.ToString());
            PublishMonitorCommand("HeartBeat", e.SenderName);
        // ... scan for other commands...
        }
    } else {
        Debug.WriteLine("Server: " + 
            e.MessageDate.ToString("yyMMdd-HH:mm:ss") + ": " + e.Message);
        AppendToTextBox(e.MessageDate.ToString("yyMMdd-HH:mm:ss") + " " + 
                        e.MessageType.ToString() + " von " + 
                        e.SenderName + ": " + e.Message);
    }
}

MyServerCallbackClass

如前所述,对于 RemotelyDelegatableObject,在 ServerRemoteControl.StartServerApp() 中使用的回调类派生自抽象类 RemotelyDelegatableObjectInternalTextMessageReceiver() 的实现现在包含此应用程序特有的代码。

class MyServerCallbackClass : RemotelyDelegatableObject {
    private RemoteControlServer _RCServer;

    private MyServerCallbackClass() {}

    public MyServerCallbackClass (RemoteControlServer RCServer) {
        this._RCServer = RCServer;
    }
    
    protected override void InternalTextMessageReceiver (
        object sender, TextMessageEventArgs e) {
        this._RCServer.Server_ReceiveTextMessage(sender, e);
    }

    public override object InitializeLifetimeService() {
        return null;
    }
}

ServerRemoteConfig

配置应用程序提供对本地机器注册表中配置参数的直接访问。配置好参数后,它能够连接到服务器,可视化传入的心跳及其倒计时状态,以及一个用于修改服务器配置(关机命令,倒计时起始值)的表单。为此,它还托管一个 MessageService 对象用于监视值。

在启动时调用 CreateServerMonitorMessageService(),它创建一个 MessageService 用于监视值,并使用派生自 RemotelyDelegatableObjectMyMonitorCallbackClass 订阅其消息事件,如前所述。

private void CreateServerMonitorMessageService() {
    string PortStr = MyConfig.GetValue(
        ConfigKeyNames.Client_ServerPort.ToString());
    int port = int.Parse(PortStr); 
    MyMonitorMessageService = MessageService.CreateMessageServiceServer(
        MessageProtocol.http, 
        port,
        "RemoteServerMonitor",
        "Remote Control Server Monitor");

    MonitorCallback = new MyMonitorCallbackClass (this);
    MyMonitorMessageService.TextMessageEventHandler += 
        new TextMessageHandler(MonitorCallback.TextMessageReceiver);
}

按下“连接”按钮后,将调用 btnConnectToServer_Click(),它创建一个代理对象来调用服务器的 MessageService 并从服务器收集一些配置数据。然后,调用 StartServerMonitor() 会使服务器向此应用程序发送监视值。

private void btnConnectToServer_Click(object sender, System.EventArgs e) {
    string Host = MyConfig.GetValue(ConfigKeyNames.Client_ServerHostName.ToString());
    string Port = MyConfig.GetValue(ConfigKeyNames.Client_ServerPort.ToString());
    string HostUrl = string.Format(
        @"{0}://{1}:{2}/{3}",
        MessageProtocol.http, 
        Host,
        Port, 
        "RemoteControl");
    Debug.WriteLine("Client verbindet zu Server: " + HostUrl);
    try {
        MyServerMessageService = (MessageService)Activator.GetObject(
            typeof(MessageService), 
            HostUrl);
        MyServerMessageService.PublishMessage("Config Client Verbunden", 
            System.Environment.MachineName);
        btnConnectToServer.Enabled = false;
        lblServerConnectionState.Text = "Verbunden mit " + Host + ":" + Port;
    } catch (System.Net.WebException) {
        btnConnectToServer.Enabled = true;
        lblServerConnectionState.Text = "Verbindung zu " + Host + ":" + 
            Port + " konnte nicht hergestellt werden.";
        return;
    }
    this.ServerConnected = true;
    this.tbServerCountDownSeconds.Text = MyServerMessageService.GetConfigValue(
        ConfigKeyNames.Server_CountDownSeconds.ToString());
    this.tbServerCountDownSeconds.Enabled = true;
    this.tbServerShutDownCommand.Text = MyServerMessageService.GetConfigValue(
        ConfigKeyNames.Server_ShutDownCommand.ToString());
    this.tbServerShutDownCommand.Enabled = true;
    this.tbServerShutDownParams.Text = MyServerMessageService.GetConfigValue(
        ConfigKeyNames.Server_ShutDownParams.ToString());
    this.tbServerShutDownParams.Enabled = true;

    StartServerMonitor();
}

StartServerMonitor() 方法向服务器发送一个“AttachMonitor”命令,包括机器名和端口,以便将监视值发送到该位置。

 private void StartServerMonitor() {
    string PortStr = MyConfig.GetValue(ConfigKeyNames.Client_ServerPort.ToString());
    int port = int.Parse(PortStr);
    MyServerMessageService.PublishCommand("AttachMonitor:" + 
        port.ToString(), Environment.MachineName);
}

ServerRemoteClientService

客户端应用程序托管一个单例 RemoteClient 对象,该对象提供唤醒服务器和向服务器发送心跳消息的所有功能。

InitRemoteClient() 方法中,创建客户端和服务器通道,RemoteClient 被注册为单例对象,并使用带有本地 URI 的 Activator.GetObject() 创建一个用于对 RemoteClient 进行远程调用的代理。

private void InitRemoteClient() {

    string ErrorPos = "";
    try {
        ErrorPos = "Config-objekt erstellen.";

        Config MyConfig = new Config(false);
        ErrorPos = "Config-objekt erstellt, registry auslesen.";
        string ClientServerPort_str = MyConfig.GetValue(
            ConfigKeyNames.Client_ClientServerPort.ToString());
        int ClientServerPort;
        try {
            ClientServerPort = int.Parse(ClientServerPort_str);
        } catch {
            throw new ApplicationException("Registryschlüssel [" + 
                ConfigKeyNames.Client_ClientServerPort.ToString() + 
                "] hat das falsche Format.");
        }
        string ClientServerUri = "RemoteControlClient";

        ErrorPos = "Clientchannel registrieren.";

        // client channel:
        BinaryClientFormatterSinkProvider ClientChSinkProvider = 
            new BinaryClientFormatterSinkProvider();
        HttpClientChannel channel = new HttpClientChannel(
            "SRCS_ClientChannel", ClientChSinkProvider);
        ChannelServices.RegisterChannel(channel);

        ErrorPos = "Serverchannel registrieren.";

        // server channel:
        BinaryServerFormatterSinkProvider ServChSinkProvider = 
            new BinaryServerFormatterSinkProvider();
        ServChSinkProvider.TypeFilterLevel = TypeFilterLevel.Full;
        HttpServerChannel ServCh = new HttpServerChannel(
            "SRCS_ServerChannel", ClientServerPort, ServChSinkProvider);
        ChannelServices.RegisterChannel(ServCh);

        ErrorPos = "Client-Remoteobjekt registrieren.";

        // Clientobjekt registrieren
        Debug.WriteLine("Registriere Remote-Objekt: http:" + 
            ClientServerPort + "/" + ClientServerUri);
        WellKnownServiceTypeEntry myservice = new WellKnownServiceTypeEntry(
            typeof(RemoteClient),
            ClientServerUri,
            WellKnownObjectMode.Singleton);
        RemotingConfiguration.RegisterWellKnownServiceType(myservice);

        ErrorPos = "Clientobjekt initialisiert Verbindung zu RemoteClient.";

        // zu Client-Instanz connecten:
        string HostUrl = string.Format(@"{0}://{1}:{2}/{3}", "http", 
            "localhost", ClientServerPort.ToString(), ClientServerUri);
        Debug.WriteLine("Verbinde AppHost zu RemoteClient: " + HostUrl);
        this._RCClient = (RemoteClient)Activator.GetObject(
            typeof(RemoteClient),
            HostUrl);
        if (! RemotingServices.IsTransparentProxy(this._RCClient)) {
            throw new ApplicationException("Fehler beim erstellen des Remoteobjekts.");
        }

        ErrorPos = "Clientobjekt verbindet zu RemoteClient.";

        this._RCClient.TouchMe();
    } catch (Exception ex) {
        throw new ApplicationException(
            "Fehler beim initialisieren der Clientumgebung:\r\n" + 
            ErrorPos + "\r\n" + ex.Message);
    }

}

RemoteClient

RemoteClient 对象提供唤醒服务器和向服务器发送心跳消息的所有功能。要唤醒服务器,如果有唤醒命令,则会调用一个单独的应用程序;如果只提供服务器的 MAC 地址而唤醒命令为空,则会使用实现的 Wake On Lan 方法。

PerformServerWakeup() 方法检查是否应该使用外部应用程序,如果不是,则向服务器 MAC 地址发送一个 Wake On Lan “魔术包”。

public void PerformServerWakeup() {
    if (this._ClientWakeupCommand.Length > 0) {
        Process myProcess = new Process();
        myProcess.StartInfo.FileName = this._ClientWakeupCommand;
        myProcess.StartInfo.Arguments = this._ClientWakeupParams;
        myProcess.StartInfo.CreateNoWindow = false;
        myProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
        //myProcess.StartInfo.UseShellExecute = false;
        myProcess.Start();
    } else if (this._ClientWakeupParams.Length > 0) {
        // code from maxburov (https://codeproject.org.cn/KB/IP/cswol.aspx)
        string MAC_ADDRESS = this._ClientWakeupParams;
        MAC_ADDRESS = MAC_ADDRESS.Replace("-","");
        MAC_ADDRESS = MAC_ADDRESS.Replace(".","");
        MAC_ADDRESS = MAC_ADDRESS.Replace(":","");

        WOLClass client=new WOLClass();
        client.Connect(new 
            IPAddress(0xffffffff),  //255.255.255.255  i.e broadcast
            0x2fff); // port=12287 let's use this one 

        client.SetClientToBrodcastMode();

        int counter=0;
        //buffer to be send

        byte[] bytes=new byte[1024];   // more than enough :-)

        //first 6 bytes should be 0xFF

        for(int y=0;y<6;y++)
            bytes[counter++]=0xFF;
        //now repeate MAC 16 times

        for(int y=0;y<16;y++) {
            int i=0;
            for(int z=0;z<6;z++) {
                bytes[counter++]= 
                    byte.Parse(MAC_ADDRESS.Substring(i,2),
                    NumberStyles.HexNumber);
                i+=2;
            }
        }

        //now send wake up packet
        int reterned_value=client.Send(bytes,1024);
    } else {
        throw new ApplicationException("Server-Wakeup nicht möglich, " +
            "Wakeup-Parameter nicht konfiguriert.");
    }
}

HeartBeatLoop() 在一个单独的线程中运行,向服务器发送心跳消息。如果心跳失败,它还会通过调用 GetServerObject() 尝试重新连接。根据布尔值 _WakeupServer_WakeupServerWhenLost,它会在第一次心跳失败时或每次心跳失败后尝试唤醒服务器。这两个布尔值是通过读取注册表项 Client_WakeupModeClient_WakeupServerWhenLost 初始化的,但当前版本中的配置工具无法更改它们。

private void HeartBeatLoop() {
    while (true) {
        if (!this._Pause) {
            SendUIMessage("ShowHeartBeat", Environment.MachineName);
            if (this.HeartBeat!=null) {
                this.HeartBeat(this, new EventArgs());
            }
            if (this.ServerMessageService!=null) {
                try {
                    this.ServerMessageService.PublishCommand(
                        "HeartBeat", System.Environment.MachineName);
                    // Kein Fehler: Verbindung ist hergestellt.
                    if (!this.Connected) { // Server war vorher nicht verbunden
                        this.Connected = true;
                        this._ServerConnectedCount++;
                        if (this.ConnectionStateChanged!=null) {
                            this.ConnectionStateChanged(this, new EventArgs());
                        }
                        SendUIMessage("EnableControl","btnShutDownServer:true");
                        SendUIMessage("EnableControl","btnWakeupServer:false");
                    
                        this._WakeupServer = false;
                    }
                } catch {
                    // Fehler: Nicht verbunden
                    if (this.Connected) { // Server war vorher verbunden
                        this.Connected = false;
                        if (this.ConnectionStateChanged!=null) {
                            this.ConnectionStateChanged(this, new EventArgs());
                        }
                        SendUIMessage("EnableControl","btnShutDownServer:false");
                        SendUIMessage("EnableControl","btnWakeupServer:true");
                    }
                    if (this._WakeupServer) { // Wakeup-modus
                        if (this._WakeupServerWhenLost | 
                            (this._ServerConnectedCount==0)) { 
                            PerformServerWakeup();
                        }
                    }
                    Debug.WriteLine("ReConnect...");
                    GetServerObject();
                }
            }
        }
        // ConnectionState in UI anzeigen:
        ShowConnectionState();

        CheckForChanges(); // Konfiguration checken

        int SleepInterval = this._HeartBeatInterval*1000;
        Thread.Sleep(SleepInterval);
    }
}

ServerRemoteClientForm

这个 Windows 窗体连接到 ServerRemoteClientService 托管的 RemoteClient,可视化心跳和连接状态,并提供按钮来手动启动/关闭服务器,停止发送心跳等等。最棘手的部分是 RemoteClientApplicationContext 类,它是应用程序的入口点。它只显示一个系统托盘图标,双击会打开主窗口。

ServerRemoteClientForm.jpg

安装

这是最无趣的部分。由于我还没有构建安装程序,所以第一个任务是运行 *ServerRemoteConfig.exe*。配置工具将在系统注册表中创建 *HKEY_LOCAL_MACHINE\SOFTWARE\Torkelware\ServerRemote* 并设置一些默认值。将“服务器名称”更改为您的服务器主机名。接下来,必须使用框架的 *installutil.exe* 工具安装服务器和客户端的相应 Windows 服务

  • 服务器:*installutil /i ServerRemoteControl.exe*
  • 客户端:*installutil /i ServerRemoteClientService.exe*

安装的服务现在应该在计算机管理/服务中列出,但尚未启动。服务需要访问注册表,因此 NetworkService 账户必须有此权限,或者服务必须在特权用户账户下运行。

  • 设置 NetworkService 的权限
  • 使用 regedit,转到 *HKEY_LOCAL_MACHINE\SOFTWARE*。在“编辑”菜单上,单击“权限”,并为 *NetworkService* 账户分配读取权限。然后,转到 *\Torkelware* 子项,并为 *NetworkService* 账户分配完全访问权限。

  • 选择其他用户账户
  • 在计算机管理/服务中,转到服务的“属性”对话框,并将账户从 *NetworkService* 更改为特权账户。

现在,您可以使用计算机管理/服务启动服务。如果服务立即停止,请查看事件日志。*ServerRemoteClientForm.exe* 和 *ServerRemoteConfig.exe* 可以直接启动。启动 ServerRemoteClientForm 后别忘了托盘图标 :-) 。如果您在安装客户端时遇到问题,请停止服务器上的服务,否则它将在 10 分钟后(或您配置的倒计时时间)关闭。或者,您可以使用 *ServerRemoteConfig* 来重置倒计时值。如果一切正常,返回计算机管理/服务以配置服务自动启动。在客户端计算机上,您可以将 *ServerRemoteClientForm.exe* 的引用放入自动启动文件夹。

最后

ServerRemoteClientService 和 ServerRemoteControl 都设计为作为 Windows 服务运行,但它们都有第二个入口点,允许它们作为普通可执行文件运行。这在开发过程中很有用,只需更改项目首选项中的入口点即可。错误消息和一些注释仍然是德语,对此很抱歉。关于我的英语,总的来说:请不要太苛刻 :-) 目前,只支持 Windows 系统。我曾考虑从服务器发送 ping,如果没有人响应就关闭,但有些流媒体客户端即使在关闭时也会响应 ping……如果对这个项目有兴趣,我会考虑的。欢迎提出建议。

现在,我因摩托车事故后遗症而度过的闲暇时光结束了,所以这篇文章也将结束。祝您玩我的代码愉快 :-)

霍尔格!

© . All rights reserved.