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

VB.NET 客户端-服务骨架 VISTA 就绪

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.75/5 (7投票s)

2009年3月25日

CPOL

5分钟阅读

viewsIcon

30176

downloadIcon

391

一个客户端-服务器 GUI-服务应用程序骨架。为 Vista 就绪。

引言

在我从事银行业务的职业生涯中,我花了大量时间开发需要在机器启动时运行而无需用户登录的软件组件。这意味着要编写一个 Windows 服务。此外,我还必须提供一个 GUI,允许用户更改一些基本配置设置。

随着 .NET Framework 的引入,编写系统服务比以前简单得多。这可以通过使用 System.ServiceProcess 命名空间中的类来实现。直到 Windows XP,没有严格的安全规则需要遵守,最重要的是,服务和用户进程可以自由地共享资源,没有多少问题。但随着下一代 Windows 的发布,其中一些任务变得更加复杂,开发人员必须付出很多努力才能使其再次工作。在本文中,我想与您分享我用来升级一些服务并使其准备好支持 Vista 的骨架,就像我的 GekoMirror 应用程序 一样。

架构

该架构严格遵循客户端-服务器模式,其中服务器部分运行在 Windows 服务中,而客户端是一个窗口窗体,允许用户使用该服务并配置其行为。首先,重要的是要记住,在新的 Windows 版本中,客户端和服务经常在不同的安全上下文中运行:Windows 服务以“Local System”或“Network Service”权限运行,而 GUI 使用已登录用户的安全上下文运行。我建议您阅读 Windows VISTA 版本引入的新功能,例如用户访问控制 (UAC)。您可以在 此链接 找到一篇有用的文档。您还会对 Windows 如何限制对核心系统资源(文件夹、注册表、系统对象)的访问感兴趣。我们的服务必须打补丁才能满足这些新要求。

实现

GUI 和服务之间的通信是使用命名管道构建的,信息交换是基于文本的,采用“命令行”语法:客户端将 string 消息发送到管道,格式为“command param1 param2 …”,然后等待服务器的响应。数据和配置的持久化是通过文件和系统注册表实现的。

VISTA 就绪指南

假设我们的服务以本地系统身份运行,GUI 以已登录用户身份运行。那么服务可以具有对 %programfiles% 文件夹和 LocalMachine 注册表蜂巢的读写权限,而 GUI 运行在低级别安全令牌下,则不能。因此,每当 GUI 需要对配置进行永久性更改时,它都必须请求服务来完成这项工作。此外,当服务创建客户端发送命令的命名管道时,默认的操作系统行为是只授予非管理员用户读取权限。然后有必要明确授予“标准用户”写入管道的权限。

总的来说,这些指南是

在服务器端

  • 执行创建、修改和删除核心系统文件夹(%systemroot%%programfiles%)所需的所有工作。
  • 执行创建、修改和删除注册表蜂巢或值所需的所有工作。
  • 明确授予标准用户对系统对象(管道、计时器、互斥体等)的正确权限。

在客户端

  • 请求服务执行所有它无法完成的任务。
  • 请求服务返回客户端所需的所有配置设置。

通过应用这些“小”更正,您可以修补您的服务/客户端应用程序,使其能够与下一代 Windows 一起工作。

源代码

源代码分为三个部分

  1. 一个用于处理管道的通用部分:“Interop”类
  2. 一个管理命令执行、客户端请求和服务器命令的服务器部分:“CommandEngine”和“Server”类
  3. 一个客户端部分,是一个窗口窗体:“frmClient”类

关注点

服务线程

一个有趣的点是使用多线程来响应系统命令。当系统调用“OnStart”子例程时,会创建一个新线程,该线程函数将通过无限循环保持“活动”状态,直到被终止。否则,当收到停止请求时,所有活动客户端连接都会终止,工作线程和监听新客户端连接的线程都会停止。

Public Class MyService

    Private ps As Server 'Server Class
    Private st As Thread 'Server thread

    Protected Overrides Sub OnStart(ByVal args() As String)
        ' Add code here to start your service. This method should set things
        ' in motion so your service can do its work.

        Try
            'Create a new server class instance
            ps = New Server()

            'Add an handler for message received by server
            AddHandler ps.MessageReceived, AddressOf ReceiveMessage

            'Start the server working function in a separate thread
            st = New Thread(AddressOf ps.Start)
            st.Start()

        Catch ex As Exception
            EventLog.WriteEntry("MyService", _
"An error occurred creating server class." & vbCrLf & ex.Message, _
EventLogEntryType.Error)
            Throw ex

        End Try

    End Sub
    Protected Overrides Sub OnStop()
        ' Add code here to perform any tear-down necessary to stop your service.
        If st.ThreadState <> Threading.ThreadState.Unstarted Then
            'Request the server to shutdown
            ps.Stop()
            Try
                'Wait 10 seconds for server shutting down
                st.Join(10000)
                EventLog.WriteEntry("MyService", _
"Server thread is terminated successfully.", _
EventLogEntryType.Information)

            Catch ex As Exception
                'if the server has trouble shutting down, abort the thread
                st.Abort()
                EventLog.WriteEntry("MyService", _
"An error occurred terminating server thread." & vbCrLf & ex.Message, _
EventLogEntryType.Error)

            End Try
        End If

    End Sub
[…]
End Class

Class Server

    Public Sub Start()
        'start the listening thread
        Me._listenThread = New Thread(New ThreadStart(AddressOf ListenForClients))
        Me._listenThread.Start()
    End Sub

    Public Sub [Stop]()
	  'Terminate all active client connections
        For Each c As ClientInfo In Me._clients
            Interop.DisconnectNamedPipe(c.handle)
            c.handle.Close()
        Next
	  'close the last handle
        Interop.DisconnectNamedPipe(Me._new_clientHandle)
        Me._new_clientHandle.Close()
	  'terminate the listening thread
        Me._listenThread.Abort()
    End Sub
[…]
End Class 

管道安全

另一个有趣的方面是创建管道以及如何授予标准用户写入权限。要创建具有修改安全属性的命名管道,我使用了 “安全描述符字符串”表示法,并使用 ConvertStringSecurityDescriptorToSecurityDescriptor() 函数创建了一个安全描述符。

Dim sd As IntPtr
Dim sds As String
Dim sdl As Integer
 sds = "D:(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;BA)(A;;FA;;;BU)"
rc = Interop.ConvertStringSecurityDescriptorToSecurityDescriptor(sds, 1, sd, sdl)
If Not rc Then
    EventLog.WriteEntry("MyService", _ 
    "ConvertStringSecurityDescriptorToSecurityDescriptor:" & _
	Marshal.GetHRForLastWin32Error, _
    EventLogEntryType.Error)
     Exit Sub
End If
 Dim sa As Interop.SECURITY_ATTRIBUTES
sa = New Interop.SECURITY_ATTRIBUTES
sa.nLength = Marshal.SizeOf(GetType(Interop.SECURITY_ATTRIBUTES))
sa.bInheritHandle = 1
sa.lpSecurityDescriptor = sd
 '
'Creates an instance of a named pipe with the security 
'attribute and returns a handle for subsequent pipe operations
'
Me._new_clientHandle = Interop.CreateNamedPipe( _
    Interop.PIPE_NAME, _
    Interop.DUPLEX Or Interop.FILE_FLAG_OVERLAPPED, _
    0, _
    255, _
    Interop.BUFFER_SIZE, _
    Interop.BUFFER_SIZE, _
    0, _
    sa)
If (Me._new_clientHandle.IsInvalid) Then
    Exit Sub
End If

命令解析与执行

如前所述,客户端/服务器消息交换具有“命令行”语法。客户端以“command argument1 argument2 …”的格式向服务器发送消息,并等待一个非空响应。请注意,即使发生异常也要返回某些内容,否则客户端进程可能会挂起。

Public Class CommandEngine

    Public Class Commands
        Public Const CMD_ADD = "add"
        Public Const CMD_MUL = "mul"
        Public Const CMD_DIV = "div"
        Public Const CMD_SUB = "sub"
    End Class

    Public Const CMD_ERROR = "ERROR."
    Public Const CMD_NOTFOUND = "NOT FOUND."

    Public Shared Function ExecuteCommand(ByVal cmd As String) As String

        Dim r As String
        Dim x As Double
        Dim y As Double

        r = String.Empty
        x = 0
        y = 0

        Dim args As String()
        args = cmd.ToLower.Split(" ")
        If args.Length <> 3 Then
            Return CMD_ERROR
        End If
        x = args(1)
        y = args(2)

        If args(0) = Commands.CMD_ADD Then
            Try
                r = (x + y).ToString
            Catch ex As Exception
                Return CMD_ERROR
            End Try

        ElseIf args(0) = Commands.CMD_SUB Then
            Try
                r = (x - y).ToString
            Catch ex As Exception
                Return CMD_ERROR
            End Try

        ElseIf args(0) = Commands.CMD_MUL Then
            Try
                r = (x * y).ToString
            Catch ex As Exception
                Return CMD_ERROR
            End Try

        ElseIf args(0) = Commands.CMD_DIV Then
            Try
                r = (x / y).ToString
            Catch ex As Exception
                Return CMD_ERROR
            End Try

        Else
            Return CMD_NOTFOUND

        End If

        Return r

    End Function

End Class

运行示例

示例包括一个服务可执行文件 Myservice.exe 和一个 GUI 客户端应用程序 Client.exe。要运行示例,您需要使用“installutil”命令安装服务。您必须以管理员权限运行“Visual Studio 命令提示符”,并在项目的可执行文件文件夹中执行该实用程序。

C:\Temp\VBClient-Service\MyService\bin\Debug> Installutil MyService.exe

结论

Windows VISTA 在安全管理方面迈出了重要一步,但要修补一些向后兼容性问题需要付出很多努力。祝您升级顺利。

© . All rights reserved.