Winsock 升级






4.79/5 (63投票s)
一个非常易于使用的Winsock组件,允许构建简单的网络应用程序。它是基于同名的VB6组件建模并进行增强而构建的。
- Visual Studio 2003 二进制文件 - 18.9 KB; 项目和演示 - 183 KB
- Visual Studio 2005 二进制文件 - 65.8 KB; 项目和演示 - 477 KB
- Visual Studio 2008 二进制文件 - 65.8 KB; 项目和演示 - 505 KB
目录
- 引言
- 特点
- 然后,当你开始迭代 2(这是构建迭代的开始)时,你可能想要复制测试用例并将它们重新分类到迭代 2。这还允许对测试用例进行粒度跟踪,并允许你说某个测试用例在一个迭代中是准备好的,但在另一个迭代中不是。同样,如何做到这一点取决于你以及你希望如何报告。 “场景”部分提供了更多细节。
- 使用组件
- 收尾
- 历史
引言
您好,欢迎来到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
来调用一个方法,该方法将使用您指定的类型进行任何目的。在这种情况下,它允许您调用Get
和Peek
方法,并让它们返回您想要的Type
类型的数据 - 在您的事件处理程序中不再有凌乱的CType
或DirectCast
转换例程!不过要小心 - 如果您指定的类型不是缓冲区中对象的类型,可能会导致错误。
另一个让我兴奋的功能是设计时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
;组件会为您进行转换,从而使发送更加简单。这同样适用于Get
和Peek
方法的泛型增强版本 - 因此,它也适用于返回数据。
使用组件
首先,请确保已将组件添加到您的工具箱(不完全必要,但这是最简单的方法;高级用户应该知道如何从下面使用任何所需的说明,引用组件并完全通过代码创建对象)。
服务器
服务器需要同时进行监听以及发送和接收数据。因此,它们需要做的一些事情与客户端非常相似。在本节中,我将只介绍您需要了解的服务器专用例程。
我假设您将需要处理多个连接。如果您不想处理多个连接,那么您就不需要使用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
)。我们需要使用WinsockCollection
来Accept
传入的请求,使其处于活动状态。通过将以下代码添加到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
通常处理的事件是DataArrival
、ErrorReceived
、Connected
、Disconnected
和StateChanged
- 这适用于服务器和客户端。
客户端
客户端需要能够Connect
到服务器,以及发送和接收数据,但它们只需要处理一个连接。将一个Winsock
添加到您的窗体以进行客户端连接,并将其命名为wskClient
。
我们现在需要一种连接到服务器的方法;您可以在窗体启动时或按下按钮时执行此操作 - 选择您喜欢的方式。我将以按下按钮为例进行演示。您可以手动(通过代码或属性设计器)设置RemoteHost
和RemotePort
,然后调用不带参数的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
在这里,您可以看到我正在从窗体上的其他控件(即TextBox
和NumericUpDown
控件)获取服务器和端口。采用这种方式可以为用户提供更大的灵活性,不过您也可以使用以下代码硬编码服务器和端口: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
进行转换,从而为您完成Object
到String
的转换,使您的处理代码更清晰。不幸的是,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.AddHeader
和AsyncSocket.ProcessIncoming
中.GetUpperBound(0)
的问题。 - 2007年12月26日 - 添加了新事件
ReceiveProgress
。 - 2007年12月28日 - 更新了
Winsock.Get
和Winsock.Peek
以检查Nothing
。向AsyncSocket
中的所有qBuffer
实例以及_buff
(ProcessIncoming
)添加了SyncLock
。 - 2008年2月14日 - 修复了UDP接收中的一个bug,该bug导致它总是以完整的字节缓冲区接收,而不是根据传入数据的大小接收。
- 2008年3月24日 - 修复了
Listen
方法,使其能够正确地为UDP和TCP触发状态更改事件。修改了IWinsock
、Winsock
和AsyncSocket
,以允许AsyncSocket
修改组件的LocalPort
属性。 - 2008年3月25日 - 向
NetworkStream
属性添加了一个属性,以公开使用此组件建立的连接的NetworkStream
对象。 - 2008年4月21日 - 修复了Winsock.vb和WinsockCollection.vb中的
RaiseEventSafe
,使其使用BeginInvoke
而不是Invoked
。更改了ReceiveCallbackUDP
中的操作顺序,以允许正确检测远程IP地址。