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

Winsock 升级

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (63投票s)

2007年11月26日

CPOL

14分钟阅读

viewsIcon

1405850

downloadIcon

13742

一个非常易于使用的Winsock组件,允许构建简单的网络应用程序。它是基于同名的VB6组件建模并进行增强而构建的。

Screenshot - Multi-EnvDemo.png

目录

引言

您好,欢迎来到Winsock.NET组件的第四版。之前的版本包括Winsock2007、Winsock2005以及最初的Winsock.NET。

我花了一些时间来尝试制作出您可能想要的最佳版本组件,并为此,我决定为.NET框架的每个版本(1.1、2.0和3.5)创建一个版本!

最初是为了满足我对.NET框架中缺乏Winsock支持的需求而产生的,但现在它已经朝着一个新的方向发展。最初它有些不稳定,数据分离技术(使用EOT字符)存在问题,事件例程的线程安全性也不高,但现在已经演变成使用一个不错的报头和线程安全的事件。最棒的是,可以通过LegacySupport关闭报头,以便与其他服务器/客户端(如Apache Web服务器和Telnet客户端)进行通信。

我甚至测试了在VS2003中构建的服务器和使用VB 2005构建的客户端在不使用LegacySupport的情况下运行,并且运行良好!这意味着,只要Type(完整的Type - 包括命名空间)在两边都存在,您甚至可以在框架之间序列化一个对象。

让我们深入了解这些工具集的功能、结构和用法。

功能

与该组件的上一版本相比,有一些新功能。以下是该控件包含的功能列表。新功能用星号(*)标记。

  • 线程安全的事件调用
  • 对象序列化
  • UDP支持
  • IPv6支持
  • 泛型支持*(2003版本中没有)
  • 设计时UI支持*(重构的Action列表 - 2003版本中没有)
  • 传统支持
  • 增强的传统支持转换*
  • 易于使用的文件发送
  • WinsockCollection,方便处理多个连接

结构

如果我详细介绍这个组件的整个结构,这篇文章会非常长,而且会非常枯燥,所以我将尝试将注意力集中在一些主要的细节上。

这个组件结构的关键是Socket对象。它允许使用TCP/UDP和IPv4/IPv6进行网络通信。唯一的难点是,要同时监听IPv4和IPv6,你需要两个Socket对象 - 因此AsyncSocket类包含两个。

您对这个版本最先注意到的是,我使用了一个接口来定义Winsock对象的部分。这主要是因为我首先处理了AsyncSocket对象,并在开始处理Winsock对象本身之前需要一个占位符。我同样可以很容易地使用该对象的精确引用而不是接口。

为了让您的应用程序顺利运行,组件需要处理的进程应该在一个单独的线程中完成 - 因此,所有操作都使用异步调用。如果不正确处理,异步调用可能会导致问题,当您尝试从事件处理程序更新窗体控件时。为了防止这些问题,我实现了一个线程安全的事件调用函数。

Public Event Connected( _
         ByVal sender As Object, _
         ByVal e As WinsockConnectedEventArgs) _
         Implements IWinsock.Connected

Public Sub OnConnected( _
        ByVal e As WinsockConnectedEventArgs) _
        Implements IWinsock.OnConnected
  RaiseEventSafe(ConnectedEvent, New Object() {Me, e})
End Sub

Private Sub RaiseEventSafe( _
        ByVal ev As System.Delegate, _
        ByRef args() As Object)
  Dim bFired As Boolean
  If ev IsNot Nothing Then
    For Each singleCast As System.Delegate In _
            ev.GetInvocationList()
      bFired = False
      Try
        Dim syncInvoke As ISynchronizeInvoke = _
            CType(singleCast.Target, ISynchronizeInvoke)
        If syncInvoke IsNot Nothing _
              AndAlso syncInvoke.InvokeRequired Then
          bFired = True
          syncInvoke.Invoke(singleCast, args)
        Else
          bFired = True
          singleCast.DynamicInvoke(args)
        End If
      Catch ex As Exception
        If Not bFired Then singleCast.DynamicInvoke(args)
      End Try
    Next
  End If
End Sub

让我们来看看这个。

首先,您会注意到我声明了一个名为Connected的事件(实现了接口中定义的事件)。

接下来,您会看到触发事件的方法。从技术上讲,它并不直接触发事件,而是通过调用OnConnected方法允许AsyncSocket触发它。这个方法只是调用另一个方法,该方法旨在通用地接收事件和参数,并以线程安全的方式触发它。您还会注意到的另一件事是引用了ConnectedEvent。在Visual Basic中,每个事件都会在后台创建一个同名的委托(委托名称与您指定的事件名称相同),只需在后面追加Event。例如,如果您创建一个名为MyEvent的事件,VB会创建一个名为MyEventEvent的委托。这些委托甚至不会出现在IntelliSense菜单中,但它们确实存在 - 并且帮助我们创建这个线程安全的调用方法。

现在,来看RaiseEventSafe方法。我们首先声明一个Boolean变量,以确保事件在同一次调用中不会触发两次。这可能发生在您编写的事件处理程序代码中出现错误时!这里的Try...Catch会捕获该错误,然后尝试再次调用事件 - 导致两次错误。由于事件可以有多个处理程序,我们需要遍历每个处理程序并调用它 - 因此使用了For...Each循环。对于每个处理程序,例程会尝试将处理程序的目標转换为ISynchronizeInvoke接口。这对于在正确的线程上调用事件是必要的。如果转换成功,事件将在原始线程上调用 - 如果不成功,事件仍然会被触发,但只是在当前线程上。

对象序列化是通过内存中的BinaryFormatter(请参阅ObjectPacker类)来处理的。这允许将**任何**可序列化的对象发送到远程计算机。这比尝试发送所有属性并自己重建对象要简单得多。

Public Function [Get](Of dataType)() As dataType
  Dim byt() As Byte = _asSock.GetData()
  Dim obj As Object
  If LegacySupport AndAlso _
        GetType(dataType) Is GetType(String) Then
    obj = System.Text.Encoding.Default.GetString(byt)
  Else
    obj = ObjectPacker.GetObject(byt)
  End If
  Return DirectCast(obj, dataType)
End Function

泛型支持的实现虽然不是非常复杂,但它是我最喜欢的功能之一。泛型允许您通过指定一个Type来调用一个方法,该方法将使用您指定的类型进行任何目的。在这种情况下,它允许您调用GetPeek方法,并让它们返回您想要的Type类型的数据 - 在您的事件处理程序中不再有凌乱的CTypeDirectCast转换例程!不过要小心 - 如果您指定的类型不是缓冲区中对象的类型,可能会导致错误。

Screenshot - DesignMode.png

另一个让我兴奋的功能是设计时UI支持。我终于实现了在2005版本组件中设想的功能 - 那就是事件链接。Action列表中的事件链接将创建并带您到指定事件的事件处理程序。只有当事件处理程序尚未指定时,它才会创建事件处理程序。这一切都依赖于IEventBindingService接口及其ShowCode方法。以下是实现此目的的代码(在WinsockActionList类中指定)

Public Sub TriggerStateChangedEvent()
  CreateAndShowEvent("StateChanged")
End Sub

Private Sub CreateAndShowEvent(ByVal eventName As String)
  Dim evService As IEventBindingService = _
     CType( _
     Me.Component.Site.GetService( _
     GetType( _
      System.ComponentModel.Design.IEventBindingService)), _
      IEventBindingService)
  Dim ev As EventDescriptor = GetEvent(evService, eventName)
  If ev IsNot Nothing Then
    CreateEvent(evService, ev)
    Me.designerActionUISvc.HideUI(Me.Component)
    evService.ShowCode(Me.Component, ev)
  End If
End Sub

Private Sub CreateEvent( _
      ByRef evService As IEventBindingService, _
      ByVal ev As EventDescriptor)
  Dim epd As PropertyDescriptor = _
      evService.GetEventProperty(ev)
  Dim strEventName As String = Me.Component.Site.Name & _
      "_" & ev.Name
  Dim existing As Object = epd.GetValue(Me.Component)
  'Only create if there isn't already a handler


  If existing Is Nothing Then
    epd.SetValue(Me.Component, strEventName)
  End If
End Sub

Private Function GetEvent( _
      ByRef evService As IEventBindingService, _
      ByVal eventName As String) As EventDescriptor
  If evService Is Nothing Then Return Nothing
  ' Attempt to obtain a PropertyDescriptor for the 


  ' specified component event.


  Dim edc As EventDescriptorCollection = _
      TypeDescriptor.GetEvents(Me.Component)
  If edc Is Nothing Or edc.Count = 0 Then
    Return Nothing
  End If
  Dim ed As EventDescriptor = Nothing
  ' Search for the event.


  Dim edi As EventDescriptor
  For Each edi In edc
    If edi.Name = eventName Then
      ed = edi
      Exit For
    End If
  Next edi
  If ed Is Nothing Then
    Return Nothing
  End If
  Return ed
End Function

我将介绍的最后一个结构部分是增强的传统支持转换功能。

在组件的最后几个迭代版本中(那些使用ObjectPacker的版本),使用传统支持发送数据需要您自己将String转换为Byte数组,然后由组件发送Byte数组。这是必需的,因为ObjectPacker在序列化过程中会忽略Byte数组。

现在,组件会检查传统支持是否激活以及要发送的数据是否为String;组件会为您进行转换,从而使发送更加简单。这同样适用于GetPeek方法的泛型增强版本 - 因此,它也适用于返回数据。

使用组件

首先,请确保已将组件添加到您的工具箱(不完全必要,但这是最简单的方法;高级用户应该知道如何从下面使用任何所需的说明,引用组件并完全通过代码创建对象)。

服务器

服务器需要同时进行监听以及发送和接收数据。因此,它们需要做的一些事情与客户端非常相似。在本节中,我将只介绍您需要了解的服务器专用例程。

我假设您将需要处理多个连接。如果您不想处理多个连接,那么您就不需要使用WinsockCollection - 但为了方便说明,我们将只使用它。

首先,您需要一个监听器。将一个Winsock组件添加到您的窗体,并将其命名为wskListener。继续将LocalPort属性设置为您希望应用程序使用的端口 - 这是我们将监听的端口。将组件添加到窗体会自动添加对DLL的引用,因此我们可以使用Winsock工具集中的任何内容(如WinsockCollection)。

由于我们要处理多个连接,我们需要一个地方来存储所有连接。我们使用WinsockCollection来做到这一点。现在就添加一个 - 将以下代码添加到您的窗体的代码设计器中:Private WithEvents _wsks As New Winsock_Orcas.WinsockCollection(True)。这段代码将创建一个新的WinsockCollection,并启用了自动移除断开连接的连接的选项。我们还指定了WithEvents以便更容易地为其创建事件处理程序。窗体代码应如下所示:

Public Class Form1

  Private WithEvents _wsks As _
      New Winsock_Orcas.WinsockCollection(True)

End Class

您可以选择在窗体启动时开始监听,或在按下按钮时开始监听。转到您选择的任何方法的事件,然后输入以下代码:wskListen.Listen()。这应该会产生一些看起来像这样的代码:

Private Sub cmdListen_Click( _
      ByVal sender As System.Object, _
      ByVal e As System.EventArgs) Handles cmdListen.Click
  wskListener.Listen()
End Sub

您也可以指定一个Integer作为参数,告诉组件要监听的端口。

您还需要做的另一件事是处理传入的连接请求。通过为ConnectionRequest事件创建处理程序来完成此操作(您可以使用Action列表并从事件列表中选择ConnectionRequest)。我们需要使用WinsockCollectionAccept传入的请求,使其处于活动状态。通过将以下代码添加到ConnectionRequest处理程序中来完成此操作:_wsks.Accept(e.Client)。这应该看起来像下面的内容:

Private Sub wskListener_ConnectionRequest( _
      ByVal sender As System.Object, _
      ByVal e As _
      Winsock_Orcas.WinsockConnectionRequestEventArgs) _
      Handles wskListener.ConnectionRequest
  _wsks.Accept(e.Client)
End Sub

在客户端被接受后,它的所有事件都通过WinsockCollection触发。作为服务器端部分的最后一步,我将向您展示如何为WinsockCollection事件创建处理程序。

以下是您可能需要的任何事件的定义:

Public Event Connected( _
    ByVal sender As Object, _
    ByVal e As WinsockConnectedEventArgs)
Public Event ConnectionRequest( _
    ByVal sender As Object, _
    ByVal e As WinsockConnectionRequestEventArgs)
Public Event CountChanged( _
    ByVal sender As Object, _
    ByVal e As WinsockCollectionCountChangedEventArgs)
Public Event DataArrival( _
    ByVal sender As Object, _
    ByVal e As WinsockDataArrivalEventArgs)
Public Event Disconnected( _
    ByVal sender As Object, _
    ByVal e As System.EventArgs)
Public Event ErrorReceived( _
    ByVal sender As Object, _
    ByVal e As WinsockErrorReceivedEventArgs)
Public Event SendComplete( _
    ByVal sender As Object, _
    ByVal e As WinsockSendEventArgs)
Public Event SendProgress( _
    ByVal sender As Object, _
    ByVal e As WinsockSendEventArgs)
Public Event StateChanged( _
    ByVal sender As Object, _
    ByVal e As WinsockStateChangedEventArgs)

您需要构造一个与您想处理的事件具有相同结构的方法,然后使用方法声明的Handles子句告诉它处理该事件。这是一个例子:

Private Sub _wsks_ErrorReceived( _
   ByVal sender As System.Object, _
   ByVal e As Winsock_Orcas.WinsockErrorReceivedEventArgs) _
   Handles _wsks.ErrorReceived

End Sub

通常处理的事件是DataArrivalErrorReceivedConnectedDisconnectedStateChanged - 这适用于服务器和客户端。

客户端

客户端需要能够Connect到服务器,以及发送和接收数据,但它们只需要处理一个连接。将一个Winsock添加到您的窗体以进行客户端连接,并将其命名为wskClient

我们现在需要一种连接到服务器的方法;您可以在窗体启动时或按下按钮时执行此操作 - 选择您喜欢的方式。我将以按下按钮为例进行演示。您可以手动(通过代码或属性设计器)设置RemoteHostRemotePort,然后调用不带参数的Connect方法,或者您可以将服务器和端口指定为Connect方法的参数 - 我将使用后者。

Private Sub cmdClientConnect_Click( _
      ByVal sender As System.Object, _
      ByVal e As System.EventArgs) _
      Handles cmdClientConnect.Click
  wskClient.Connect(txtServer.Text, CInt(nudPort.Value))
End Sub

在这里,您可以看到我正在从窗体上的其他控件(即TextBoxNumericUpDown控件)获取服务器和端口。采用这种方式可以为用户提供更大的灵活性,不过您也可以使用以下代码硬编码服务器和端口:wskClient.Connect("localhost", 8080)。请注意,您不必使用远程计算机的IP地址 - 您也可以指定名称。该组件会自动为您将名称解析为IP地址。

客户端和服务器

在客户端和服务器上,您都需要处理DataArrival事件。每次组件接收到数据时都会触发此事件。在服务器端,您希望此事件的处理程序附加到WinsockCollection;在客户端,您希望它附加到Winsock对象。

让我们来看一下客户端和服务器端一个非常简单的DataArrival实现。假设一个非常简单的聊天应用程序,服务器必须将消息发送给除了发送消息的客户端之外的所有客户端。

服务器端处理程序

Private Sub SendToAllBut( _
      ByVal sender As Object, _
      ByVal msg As String)
  For Each wsk As Winsock_Orcas.Winsock In _wsks.Values
    If wsk IsNot sender Then wsk.Send(msg)
  Next
End Sub

Private Sub _wsks_DataArrival( _
     ByVal sender As Object, _
     ByVal e As Winsock_Orcas.WinsockDataArrivalEventArgs) _
     Handles _wsks.DataArrival
  Dim strIn As String = CType(sender, Winsock_Orcas.Winsock).Get(Of String)()
  SendToAllBut(sender, strIn)
End Sub

请注意,这里有一个额外的方法。WinsockCollection目前没有发送功能,所以我们需要另一个方法将数据发送给所有已连接的客户端。即使WinsockCollection具有发送功能,您也可能想要自己的方法 - 特别是,如果您需要用户先登录,因为那样您可以检查他们是否已登录,然后再向他们发送数据。

您会注意到的第二件事是Get方法中的Of String。Visual Studio 2005和2008版本支持泛型,这允许将该方法作为String进行转换,从而为您完成ObjectString的转换,使您的处理代码更清晰。不幸的是,VS的2003版本不支持泛型,因此您只会从Get方法返回一个Object,然后您可以根据自己的需要将其转换为String

您还可以在服务器端添加另一项功能,那就是日志记录功能(记录到TextBox或文件等)以提供反馈。我们将保持简单,服务器只转发消息 - 不发送任何自己的消息。

现在,让我们看看客户端处理程序。

Private Sub wskClient_DataArrival( _
     ByVal sender As Object, _
     ByVal e As Winsock_Orcas.WinsockDataArrivalEventArgs) _
     Handles wskClient.DataArrival
  Dim msg As String = _
      CType(sender, Winsock_Orcas.Winsock).Get(Of String)()
  txtLog.AppendText(msg & vbCrLf)
End Sub

这同样相当简单。处理程序从Winsock中检索数据到String,然后将String添加到窗体上的TextBox中。

还有一件事您需要知道 - 尽管您在上面的代码中看到了它:发送数据。发送数据也应该非常简单。只需在Winsock上调用Send方法。您必须传递您想要发送的数据。这可以是**任何**对象,一个String,一个Integer,甚至是您设计的自定义类 - 只要您将其标记为Serializable。如果您要使用自定义类,您必须确保**客户端和服务器**都在其项目中拥有相同的类。

以下是发送UI中输入文本的示例:

Private Sub cmdSend_Click( _
      ByVal sender As System.Object, _
      ByVal e As System.EventArgs) Handles cmdSend.Click
  Dim dt As String = txtSend.Text.Trim()
  If dt <> "" Then wskClient.Send(dt)
  txtSend.Text = ""
  txtSend.Focus()
End Sub

首先,检查数据以确保有要发送的数据,然后发送 - 最后清除TextBox并将其焦点设置回该控件。

收尾

这是该组件迄今为止最先进的版本,我非常喜欢制作它。我相信其中肯定有很多bug/陷阱等待被发现。如果您发现任何问题,请告诉我,我会尽快进行改进。此外,关于功能请求,我很乐意听取您的意见 - 尽管它们将根据难度/有用性进行评估,并且可能不会出现在未来的版本中,但我仍然很乐意听取。

请注意LegacySupport的使用场景。任何时候当您与不使用此控件的客户端/服务器交互时,您**必须**启用LegacySupport,因为它们无法理解此组件在传出数据前附加的报头。

如果您正在调试出现问题的内容,您可以做的最好事情之一就是处理ErrorReceived事件。这可以帮助您识别事件处理程序代码中似乎存在的任何错误,因为组件在触发事件时会实际捕获它们。

请享用该组件!

历史记录

  • 2007年12月13日 - 修复了PacketHeader.AddHeaderAsyncSocket.ProcessIncoming.GetUpperBound(0)的问题。
  • 2007年12月26日 - 添加了新事件ReceiveProgress
  • 2007年12月28日 - 更新了Winsock.GetWinsock.Peek以检查Nothing。向AsyncSocket中的所有qBuffer实例以及_buffProcessIncoming)添加了SyncLock
  • 2008年2月14日 - 修复了UDP接收中的一个bug,该bug导致它总是以完整的字节缓冲区接收,而不是根据传入数据的大小接收。
  • 2008年3月24日 - 修复了Listen方法,使其能够正确地为UDP和TCP触发状态更改事件。修改了IWinsockWinsockAsyncSocket,以允许AsyncSocket修改组件的LocalPort属性。
  • 2008年3月25日 - 向NetworkStream属性添加了一个属性,以公开使用此组件建立的连接的NetworkStream对象。
  • 2008年4月21日 - 修复了Winsock.vbWinsockCollection.vb中的RaiseEventSafe,使其使用BeginInvoke而不是Invoked。更改了ReceiveCallbackUDP中的操作顺序,以允许正确检测远程IP地址。
Winsock Revamped - CodeProject - 代码之家
© . All rights reserved.