VB.NET辅助类备忘录之Socket通信-SocketHelper

最近一直在琢磨Socket通信,也查了很多资料,socket通信还是比较底层的东西,对我一个非科班出身的小菜来说有点难度。学习完就想着写个辅助类以后可以直接用。代码用vb.net写的,部分代码参考了博客园的一位大牛的文章:

C# 异步 TCP 服务器完整实现

主要特色:

  1. 采用的异步回调模式,不用考虑阻塞主线程的问题。
  2. 事件驱动,拒绝轮询。
  3. 对数据报文进行了简单封装,组装报文只要简单的new一个就行了,并且加入了FLAG可用于标识报文类型,加入了报文长度标识,加入了报文内容的MD5校验,保证报文传输的可靠。
  4. 在3的基础上实现了自动处理分包和粘包。
  5. 加入了对象序列化功能,可以直接发送对象,此功能待完善。

       待加入报文内容加密功能,有空再写。

Git地址

(类库的后续更新就不手动在这里更新了,自己去下载吧)

https://gitee.com/yangliu0512/SocketHelper

话不多说直接上代码:

服务器端:

Imports System.Net
Imports System.Net.Sockets
Imports System.Text
''' <summary>
''' 异步Socket服务器端
''' </summary>
Public Class AsyncSocketServer
    Implements IDisposable
#Region "变量"
    Dim listener As TcpListener '用于监听连接请求
    Dim clientList As List(Of ClientInfo) '存储已连接的客户端信息
    Dim disposed As Boolean = False
    Dim context As Threading.SynchronizationContext = New Windows.Forms.WindowsFormsSynchronizationContext '主线程上下文,用于与主线程同步
    Dim _isrunning As Boolean
#End Region

#Region "构造函数"
    ''' <summary>
    ''' 创建AsyncSocketServer实例,监听指定的IP地址和端口。
    ''' </summary>
    ''' <param name="localIPAddress">要监听的本地IP地址</param>
    ''' <param name="listenPort">要监听的端口</param>
    Public Sub New(localIPAddress As IPAddress, listenPort As Integer)
        Address = localIPAddress
        Port = listenPort
        clientList = New List(Of ClientInfo)
        listener = New TcpListener(Address, Port)
        listener.AllowNatTraversal(True)
    End Sub
    ''' <summary>
    ''' 创建AsyncSocketServer实例,监听所有本地IP的指定端口。
    ''' </summary>
    ''' <param name="listenPort">要监听的端口</param>
    Public Sub New(listenPort As Integer)
        Me.New(IPAddress.Any, listenPort)
    End Sub
    ''' <summary>
    ''' 创建AsyncSocketServer实例,监听指定的终结点。
    ''' </summary>
    ''' <param name="localIPEndPoint">要监听的终结点</param>
    Public Sub New(localIPEndPoint As IPEndPoint)
        Me.New(localIPEndPoint.Address, localIPEndPoint.Port)
    End Sub
#End Region

#Region "属性"
    ''' <summary>
    ''' 获取服务器是否正在运行
    ''' </summary>
    ''' <returns>正在运行返回True,否则返回False</returns>
    Public ReadOnly Property IsRunning As Boolean
        Get
            Return _isrunning
        End Get
    End Property
    ''' <summary>
    ''' 获取服务器监听的本地IP地址信息
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property Address As IPAddress
    ''' <summary>
    ''' 获取服务器监听的端口
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property Port As Integer
    ''' <summary>
    ''' 获取或设置通信使用的编码
    ''' </summary>
    ''' <returns></returns>
    Public Property Encoding As Encoding = Encoding.Default
#End Region

#Region "事件"
    ''' <summary>
    ''' 与新的客户端建立了连接
    ''' </summary>
    ''' <param name="sender">服务器实例</param>
    ''' <param name="tcpClient">用于与新客户端通信的TcpClient实例</param>
    Public Event OnTCPClientConnected(sender As AsyncSocketServer, tcpClient As TcpClient)
    ''' <summary>
    ''' 与客户端断开了连接
    ''' </summary>
    ''' <param name="sender">服务器实例</param>
    ''' <param name="tcpClient">断开连接的TcpClient实例</param>
    Public Event OnTCPClientDisconnected(sender As AsyncSocketServer, tcpClient As TcpClient)
    ''' <summary>
    ''' 接收到来自客户端的部分或全部报文
    ''' </summary>
    ''' <param name="sender">服务器实例</param>
    ''' <param name="tcpClient">接收到报文的通信TcpClient实例</param>
    ''' <param name="data">接收到的报文</param>
    Public Event OnBytesReceived(sender As AsyncSocketServer, tcpClient As TcpClient, data As Byte())
    ''' <summary>
    ''' 接收到来自客户端的Datagram报文
    ''' </summary>
    ''' <param name="sender">服务器实例</param>
    ''' <param name="tcpClient">接收到报文的通信TcpClient实例</param>
    ''' <param name="datagram">接收到的Datagram报文实例</param>
    Public Event OnDatagramReceived(sender As AsyncSocketServer, tcpClient As TcpClient, datagram As Datagram)
    ''' <summary>
    ''' 接收到的报文发生校验错误
    ''' </summary>
    ''' <param name="sender">服务器实例</param>
    ''' <param name="tcpClient">接收到报文的TcpClient</param>
    ''' <param name="ex">错误信息</param>
    Public Event OnDataVerifyFailed(sender As AsyncSocketServer, tcpClient As TcpClient, ex As Datagram.DatagramException)
#End Region
#Region "监听"
    ''' <summary>
    ''' 启动服务器,开始监听。
    ''' </summary>
    ''' <returns>返回当前AsyncSocketServer实例</returns>
    Public Function StartListen() As AsyncSocketServer
        If Not IsRunning Then
            _isrunning = True
            listener.Start() '开始监听
            listener.BeginAcceptTcpClient(New AsyncCallback(AddressOf handleTcpClientAccepted), listener) '准备接受连接请求
        End If
        Return Me
    End Function
    ''' <summary>
    ''' 启动服务器,开始监听。
    ''' </summary>
    ''' <param name="backlog">服务器允许的最大挂起连接数</param>
    ''' <returns></returns>
    Public Function StartListen(backlog As Integer) As AsyncSocketServer
        If Not IsRunning Then
            _isrunning = True
            listener.Start(backlog)
            listener.BeginAcceptTcpClient(New AsyncCallback(AddressOf handleTcpClientAccepted), listener)
        End If
        Return Me
    End Function
    ''' <summary>
    ''' 停止监听,不接受新的连接请求,但不断开已有连接。
    ''' </summary>
    ''' <returns>返回当前AsyncSocketServer实例</returns>
    Public Function StopListen() As AsyncSocketServer
        If IsRunning Then
            _isrunning = False
            listener.Stop() '停止监听
        End If
        Return Me
    End Function
    ''' <summary>
    ''' 停止服务器
    ''' </summary>
    ''' <returns>返回当前AsyncSocketServer实例</returns>
    Public Function StopServer() As AsyncSocketServer
        StopListen() '停止监听
        '断开所有连接,释放占用资源,并清空客户端列表
        SyncLock clientList
            If clientList IsNot Nothing AndAlso clientList.Count <> 0 Then
                For Each client As ClientInfo In clientList
                    client.TcpClient.Client.Disconnect(False)
                    client.TcpClient.Close()
                Next
                clientList.Clear()
            End If
        End SyncLock
        Return Me
    End Function
#End Region

#Region "接收"
    Private Sub handleTcpClientAccepted(ar As IAsyncResult)
        If IsRunning Then
            Dim tcpListener = CType(ar.AsyncState, TcpListener)
            Dim tcpClient = tcpListener.EndAcceptTcpClient(ar)
            Dim buffer(tcpClient.ReceiveBufferSize - 1) As Byte
            Dim client = New ClientInfo(buffer, tcpClient)
            client.TcpClient.ReceiveBufferSize = 8388608
            client.TcpClient.SendBufferSize = 16777216
            SyncLock clientList
                clientList.Add(client)
                context.Post(New Threading.SendOrPostCallback(Sub()
                                                                  RaiseEvent OnTCPClientConnected(Me, tcpClient)
                                                              End Sub), Nothing)
            End SyncLock

            Dim stream = tcpClient.GetStream
            client.ReceivedBuffer = Nothing
            stream.BeginRead(buffer, 0, buffer.Length, New AsyncCallback(AddressOf handleDatagramReceived), client) '开始尝试读取数据

            tcpListener.BeginAcceptTcpClient(New AsyncCallback(AddressOf handleTcpClientAccepted), ar.AsyncState) '继续监听
        End If
    End Sub
    Private Sub handleDatagramReceived(ar As IAsyncResult)
        If IsRunning Then
            Dim client = CType(ar.AsyncState, ClientInfo)
            Dim stream = client.TcpClient.GetStream
            Dim readBytesCount = 0
            Try
                readBytesCount = stream.EndRead(ar) '获取本次读取到的字节数
            Catch
                readBytesCount = 0
            End Try
            If readBytesCount = 0 Then
                '连接断开或被关闭了
                SyncLock clientList
                    clientList.Remove(client)
                    context.Post(New Threading.SendOrPostCallback(Sub()
                                                                      RaiseEvent OnTCPClientDisconnected(Me, client.TcpClient)
                                                                  End Sub), Nothing)
                End SyncLock
                Exit Sub
            End If
            Dim receivedBytes(readBytesCount - 1) As Byte
            Buffer.BlockCopy(client.Buffer, 0, receivedBytes, 0, readBytesCount) '获取本次读取到的数据
            context.Post(New Threading.SendOrPostCallback(Sub()
                                                              RaiseEvent OnBytesReceived(Me, client.TcpClient, receivedBytes)
                                                          End Sub), Nothing)
            '将本次读取到的数据累存到ReceivedBuffer
            If client.ReceivedBuffer Is Nothing Then
                client.ReceivedBuffer = receivedBytes
            Else
                Dim l = client.ReceivedBuffer.Length
                ReDim Preserve client.ReceivedBuffer(l + receivedBytes.Length - 1)
                Buffer.BlockCopy(receivedBytes, 0, client.ReceivedBuffer, l, receivedBytes.Length)
            End If
            '判断是否已经接收到一个完整数据包
            If client.ReceivedBuffer(0) = 1 AndAlso client.ReceivedBuffer.Length > 53 Then 'Flag=1且长度>53(flag+length+md5占53位)
                Try
                    Dim datagram As New Datagram(client.ReceivedBuffer, Encoding) '如果不抛出异常说明已经接收到完整数据包。
                    '判断是否粘包
                    Dim splicing = client.ReceivedBuffer.Length - datagram.Length
                    If splicing > 0 Then '发生了粘包
                        Dim tempbuffer(splicing - 1) As Byte
                        Buffer.BlockCopy(client.ReceivedBuffer, client.ReceivedBuffer.Length - splicing, tempbuffer, 0, splicing) '获取下一个包的数据
                        client.ReceivedBuffer = tempbuffer '将下一个包的数据存入ReceivedBuffer
                    Else
                        client.ReceivedBuffer = Nothing '未发生粘包则将ReceivedBuffer置为null
                    End If
                    context.Post(New Threading.SendOrPostCallback(Sub()
                                                                      RaiseEvent OnDatagramReceived(Me, client.TcpClient, datagram)
                                                                  End Sub), Nothing)
                Catch ex As Datagram.DatagramException '如果抛出了DatagramException,则触发事件:数据包校验错误。注意如果是MD5校验错误说明发生了丢包,如果是长度校验错误则可能是还没接收完。
                    context.Post(New Threading.SendOrPostCallback(Sub()
                                                                      RaiseEvent OnDataVerifyFailed(Me, client.TcpClient, ex)
                                                                  End Sub), Nothing)
                End Try
            End If
            If client.TcpClient.Available = 0 Then '如果缓冲区没有数据可读了
                client.ReceivedBuffer = Nothing
            End If
            stream.BeginRead(client.Buffer, 0, client.Buffer.Length, New AsyncCallback(AddressOf handleDatagramReceived), client) '继续下一次读取
        End If
    End Sub
#End Region
#Region "发送"
    ''' <summary>
    ''' 发送指定的报文到指定的客户端
    ''' </summary>
    ''' <param name="tcpClient">目标客户端</param>
    ''' <param name="bytes">要发送的报文</param>
    Public Sub SendAsync(tcpClient As TcpClient, bytes As Byte())
        If Not IsRunning Then
            Throw New InvalidOperationException("服务器还没有启动")
        End If
        If tcpClient Is Nothing Then
            Throw New ArgumentNullException("参数tcpClient不能为null")
        End If
        If bytes Is Nothing Then
            Throw New ArgumentNullException("参数bytes不能为null")
        End If
        Dim d As New Datagram(Encoding, bytes)
        tcpClient.GetStream.BeginWrite(d.BytesofAll, 0, d.Length, New AsyncCallback(AddressOf handleDatagramWritten), tcpClient)
    End Sub
    ''' <summary>
    ''' 发送指定的字符串到指定的客户端
    ''' </summary>
    ''' <param name="tcpClient">目标客户端</param>
    ''' <param name="txt">要发送的字符串</param>
    Public Sub SendAsync(tcpClient As TcpClient, txt As String)
        If txt = String.Empty Then
            Throw New ArgumentException("不能发送空字符串")
        End If
        SendAsync(tcpClient, Encoding.GetBytes(txt))
    End Sub
    ''' <summary>
    ''' 发送指定的Datagram报文到指定客户端
    ''' </summary>
    ''' <param name="tcpClient">目标客户端</param>
    ''' <param name="datagram">要发送的Datagram报文实例</param>
    Public Sub SendAsync(tcpClient As TcpClient, datagram As Datagram)
        If datagram Is Nothing Then
            Throw New ArgumentNullException("数据报文不能为null")
        End If
        SendAsync(tcpClient, datagram.BytesofAll)
    End Sub
    ''' <summary>
    ''' 发送指定报文到所有客户端
    ''' </summary>
    ''' <param name="bytes">要发送的报文</param>
    Public Sub SendAllAsync(bytes As Byte())
        If Not IsRunning Then
            Throw New InvalidOperationException("服务器还没有启动")
        End If
        For Each client As ClientInfo In clientList
            SendAsync(client.TcpClient, bytes)
        Next
    End Sub
    ''' <summary>
    ''' 发送指定字符串到所有客户端
    ''' </summary>
    ''' <param name="txt">要发送的字符串</param>
    Public Sub SendAllAsync(txt As String)
        If Not IsRunning Then
            Throw New InvalidOperationException("服务器还没有启动")
        End If
        Dim bytes = Encoding.GetBytes(txt)
        For Each client As ClientInfo In clientList
            SendAsync(client.TcpClient, bytes)
        Next
    End Sub
    ''' <summary>
    ''' 发送指定Datagram报文到所有客户端
    ''' </summary>
    ''' <param name="datagram">要发送的Datagram报文</param>
    Public Sub SendAllAsync(datagram As Datagram)
        If Not IsRunning Then
            Throw New InvalidOperationException("服务器还没有启动")
        End If
        Dim bytes = datagram.BytesofAll
        For Each client As ClientInfo In clientList
            SendAsync(client.TcpClient, bytes)
        Next
    End Sub
    Private Sub handleDatagramWritten(ar As IAsyncResult)
        Dim tcpClient = CType(ar.AsyncState, TcpClient)
        tcpClient.GetStream.EndWrite(ar)
    End Sub
#End Region
#Region "实现IDisposable接口"
    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        'GC.SuppressFinalize(Me)
    End Sub
    Protected Overridable Sub Dispose(disposing As Boolean)
        If Not disposed Then
            If disposing Then
                StopServer()
                If listener IsNot Nothing Then
                    listener = Nothing
                End If
                disposed = True
            End If
        End If
    End Sub
#End Region
End Class

客户端:

Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Public Class AsyncSocketClient
    Implements IDisposable
#Region "变量"
    Dim client As TcpClient
    Dim disposed As Boolean = False
    Dim mybuffer As Byte() '用于暂存每一次从接收缓冲区读取到的数据,每次接收都覆盖上一次数据
    Dim receivedBuffer As Byte() '累存从接收缓冲区读取到的数据,直到接收完一个完整数据包或者缓冲区无数据时,置为null
    Dim context As Threading.SynchronizationContext = New Windows.Forms.WindowsFormsSynchronizationContext '主线程上下文,主要用于触发事件时与主线程同步。
#End Region

#Region "属性"
    ''' <summary>
    ''' 获取或设置接收缓冲区大小(B)
    ''' </summary>
    ''' <returns></returns>
    Public Property ReceiveBufferSize As Integer
        Get
            If client IsNot Nothing Then
                Return client.ReceiveBufferSize
            Else
                Return 0
            End If
        End Get
        Set(value As Integer)
            client.ReceiveBufferSize = value
        End Set
    End Property
    ''' <summary>
    ''' 获取或设置发送缓冲区大小(B)
    ''' </summary>
    ''' <returns></returns>
    Public Property SendBufferSize As Integer
        Get
            If client IsNot Nothing Then
                Return client.SendBufferSize
            Else
                Return 0
            End If
        End Get
        Set(value As Integer)
            client.SendBufferSize = value
        End Set
    End Property
    ''' <summary>
    ''' 获取或设置服务器终结点信息
    ''' </summary>
    ''' <returns></returns>
    Public Property ServerIPEndPoint As IPEndPoint
    ''' <summary>
    ''' 获取或设置服务器主机名
    ''' </summary>
    ''' <returns></returns>
    Public Property ServerHostName As String = String.Empty
    ''' <summary>
    ''' 获取或设置服务器端口
    ''' </summary>
    ''' <returns></returns>
    Public Property ServerPort As Integer
    ''' <summary>
    ''' 获取或设置本地终结点信息
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property LocalIPEndPoint As IPEndPoint
    ''' <summary>
    ''' 获取或设置与服务器通信使用的字符编码
    ''' </summary>
    ''' <returns></returns>
    Public Property Encoding As Encoding = Encoding.Default
    ''' <summary>
    ''' 获取是否与服务器端建立连接
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property Connected As Boolean
        Get
            If client Is Nothing Then
                Return False
            Else
                Return client.Connected
            End If
        End Get
    End Property
#End Region
#Region "事件"
    ''' <summary>
    ''' 与服务器建立连接成功。
    ''' </summary>
    ''' <param name="sender">与服务器建立连接的AsyncSocketClient实例</param>
    ''' <param name="tcpClient">与服务器建立连接的TCPClient实例</param>
    Public Event OnConnected(sender As AsyncSocketClient, tcpClient As TcpClient)
    ''' <summary>
    ''' 与服务器的连接已断开。
    ''' </summary>
    ''' <param name="sender">与服务器断开连接的AsyncSocketClient实例</param>
    ''' <param name="tcpClient">与服务器断开连接的TCPClient实例</param>
    Public Event OnDisconnected(sender As AsyncSocketClient, tcpClient As TcpClient)
    ''' <summary>
    ''' 接受到来自服务器的部分或全部数据报文。
    ''' </summary>
    ''' <param name="sender">收到消息的AsyncSocketClient实例</param>
    ''' <param name="bytes">接收到的报文</param>
    Public Event OnBytesReceived(sender As AsyncSocketClient, bytes As Byte())
    ''' <summary>
    ''' 接收到来自服务器的Datagram报文
    ''' </summary>
    ''' <param name="sender">收到报文的AsyncSocketClient实例</param>
    ''' <param name="datagram">接收到的Datagram报文实例</param>
    Public Event OnDatagramReceived(sender As AsyncSocketClient, datagram As Datagram)
    ''' <summary>
    ''' 向服务器发送报文成功
    ''' </summary>
    ''' <param name="sender">发送报文的AsyncSocketClient实例</param>
    ''' <param name="bytes">发送的报文</param>
    Public Event OnDatagramSend(sender As AsyncSocketClient, bytes As Byte())
    ''' <summary>
    ''' 接收到的报文发生校验错误
    ''' </summary>
    ''' <param name="sender">收到报文的AsyncSocketClient实例</param>
    ''' <param name="ex">错误信息</param>
    Public Event OnDataVerifyFailed(sender As AsyncSocketClient, ex As Datagram.DatagramException)
#End Region

#Region "构造函数"
    ''' <summary>
    ''' 使用指定的服务器终结点,创建新的AsyncSocketClient实例
    ''' </summary>
    ''' <param name="remoteIPEndPoint">服务器终结点</param>
    Sub New(remoteIPEndPoint As IPEndPoint)
        ServerIPEndPoint = remoteIPEndPoint
        ServerPort = ServerIPEndPoint.Port
        LocalIPEndPoint = New IPEndPoint(IPAddress.Any, 0)
        client = New TcpClient(LocalIPEndPoint)
    End Sub
    ''' <summary>
    ''' 使用指定的服务器主机名,创建新的AsyncSocketClient实例
    ''' </summary>
    ''' <param name="remoteHostName">服务器主机名</param>
    Sub New(remoteHostName As String, port As Integer)
        ServerHostName = remoteHostName
        ServerPort = port
        LocalIPEndPoint = New IPEndPoint(IPAddress.Any, 0)
        client = New TcpClient(LocalIPEndPoint)
    End Sub
    ''' <summary>
    ''' 使用指定的本地终结点和服务器终结点,创建新的AsyncSocketClient实例
    ''' </summary>
    ''' <param name="remoteIPEndPoint"></param>
    ''' <param name="localIPEndPoint"></param>
    Sub New(remoteIPEndPoint As IPEndPoint, localIPEndPoint As IPEndPoint)
        ServerIPEndPoint = remoteIPEndPoint
        ServerPort = ServerIPEndPoint.Port
        Me.LocalIPEndPoint = localIPEndPoint
        client = New TcpClient(localIPEndPoint)
    End Sub
    ''' <summary>
    ''' 使用指定的本地终结点和服务器终结点,按规定的通信编码,创建新的AsyncSocketClient实例
    ''' </summary>
    ''' <param name="remoteIPEndPoint">服务器终结点</param>
    ''' <param name="localIPEndPoint">本地终结点</param>
    ''' <param name="encoding">通信编码</param>
    Sub New(remoteIPEndPoint As IPEndPoint, localIPEndPoint As IPEndPoint, encoding As Encoding)
        Me.New(remoteIPEndPoint, localIPEndPoint)
        Me.Encoding = encoding
    End Sub
    ''' <summary>
    ''' 使用指定的本地终结点和服务器终结点,按规定的通信编码,创建新的AsyncSocketClient实例
    ''' </summary>
    ''' <param name="remoteHostName">服务器主机名</param>
    ''' <param name="localIPEndPoint">本地终结点</param>
    ''' <param name="encoding">通信编码</param>
    Sub New(remoteHostName As String, port As Integer, localIPEndPoint As IPEndPoint, encoding As Encoding)
        Me.New(remoteHostName, port, localIPEndPoint)
        Me.Encoding = encoding
    End Sub
    ''' <summary>
    ''' 使用指定的本地终结点和服务器主机名,创建新的AsyncSocketClient实例
    ''' </summary>
    ''' <param name="remoteHostName">服务器主机名</param>
    ''' <param name="localIPEndPoint">本地终结点</param>
    Sub New(remoteHostName As String, port As Integer, localIPEndPoint As IPEndPoint)
        ServerHostName = remoteHostName
        ServerPort = port
        Me.LocalIPEndPoint = localIPEndPoint
        client = New TcpClient(localIPEndPoint)
    End Sub
    ''' <summary>
    ''' 使用指定的服务器IP地址和端口,创建新的AsyncSocketClient实例
    ''' </summary>
    ''' <param name="remotIPAddress">服务器地址信息</param>
    ''' <param name="remotePort">服务器端口</param>
    Sub New(remotIPAddress As IPAddress, remotePort As Integer)
        Me.New(New IPEndPoint(remotIPAddress, remotePort))
    End Sub
    ''' <summary>
    ''' 使用指定的服务器终结点,按规定的通信编码,创建新的AsyncSocketClient实例
    ''' </summary>
    ''' <param name="remoteIPEndPoint">服务器终结点</param>
    ''' <param name="encoding">通信编码</param>
    Sub New(remoteIPEndPoint As IPEndPoint, encoding As Encoding)
        Me.New(remoteIPEndPoint)
        Me.Encoding = encoding
    End Sub
    ''' <summary>
    ''' 使用指定的服务器IP地址和端口,按规定的通信编码,创建新的AsyncSocketClient实例
    ''' </summary>
    ''' <param name="remoteIP">服务器IP地址</param>
    ''' <param name="remotePort">服务器端口</param>
    ''' <param name="encoding">通信编码</param>
    Sub New(remoteIP As String, remotePort As Integer, encoding As Encoding)
        Me.New(remoteIP, remotePort)
        Me.Encoding = encoding
    End Sub
    ''' <summary>
    ''' 使用指定的服务器IP地址和端口,按规定的通信编码,创建新的AsyncSocketClient实例
    ''' </summary>
    ''' <param name="remotIPAddress">服务器IP地址</param>
    ''' <param name="remotePort">服务器端口</param>
    ''' <param name="encoding">通信编码</param>
    Sub New(remotIPAddress As IPAddress, remotePort As Integer, encoding As Encoding)
        Me.New(remotIPAddress, remotePort)
        Me.Encoding = encoding
    End Sub
#End Region
#Region "连接"
    ''' <summary>
    ''' 向服务器发起异步连接请求。
    ''' </summary>
    Public Sub ConnectAsync()
        If Not Connected Then
            If ServerIPEndPoint IsNot Nothing Then '如果用户传入的是IP
                client.BeginConnect(ServerIPEndPoint.Address, ServerIPEndPoint.Port, New AsyncCallback(AddressOf handleConnected), client) '开始请求连接
            ElseIf ServerHostName <> String.Empty Then '如果用户传入的是 主机名
                client.BeginConnect(ServerHostName, ServerPort, New AsyncCallback(AddressOf handleConnected), client) '开始请求连接
            Else
                Throw New ArgumentNullException("ServerIPEndPoint属性与ServerHostName属性不能同时为空。")
            End If
        End If
    End Sub
    Private Sub handleConnected(result As IAsyncResult)
        Dim tcpc = CType(result.AsyncState, TcpClient)
        tcpc.EndConnect(result)
        context.Post(New Threading.SendOrPostCallback(Sub()
                                                          RaiseEvent OnConnected(Me, tcpc)
                                                      End Sub), Nothing) '触发已连接事件
        Dim stream = tcpc.GetStream
        ReDim mybuffer(tcpc.ReceiveBufferSize - 1)
        stream.BeginRead(mybuffer, 0, mybuffer.Length, New AsyncCallback(AddressOf handleDatagramReceived), tcpc) '开始从接收缓冲区读取数据
    End Sub
    Private Sub handleDatagramReceived(ar As IAsyncResult)
        If Connected Then
            Dim client = CType(ar.AsyncState, TcpClient)
            Dim stream = client.GetStream
            Dim readBytesCount = 0
            Try
                readBytesCount = stream.EndRead(ar) '本次读取到的字节数
            Catch
                readBytesCount = 0
            End Try
            If readBytesCount = 0 Then
                '连接断开了或者被关闭了
                RaiseEvent OnDisconnected(Me, client)
                Exit Sub
            End If
            Dim receivedBytes(readBytesCount - 1) As Byte
            Buffer.BlockCopy(mybuffer, 0, receivedBytes, 0, readBytesCount) '获取本次读取到的数据
            context.Post(New Threading.SendOrPostCallback(Sub()
                                                              RaiseEvent OnBytesReceived(Me, receivedBytes)
                                                          End Sub), Nothing) '触发事件:接收到数据
            '将本次读取到的数据累存到receivedBuffer
            If receivedBuffer Is Nothing Then
                receivedBuffer = receivedBytes
            Else
                Dim l = receivedBuffer.Length
                ReDim Preserve receivedBuffer(l + receivedBytes.Length - 1) '累存前,先扩大receivedBuffer的容量
                Buffer.BlockCopy(receivedBytes, 0, receivedBuffer, l, receivedBytes.Length)
            End If

            '判断是否已经接收到一个完整数据包
            If receivedBuffer(0) = 1 AndAlso receivedBuffer.Length > 53 Then 'Flag=1且长度>53(flag+length+md5占53位)
                Try
                    Dim datagram As New Datagram(receivedBuffer, Encoding) '如果不抛出异常说明已经接收到完整数据包。
                    '判断是否粘包
                    Dim splicing = receivedBuffer.Length - datagram.Length
                    If splicing > 0 Then '发生了粘包
                        Dim tempbuffer(splicing - 1) As Byte
                        Buffer.BlockCopy(receivedBuffer, receivedBuffer.Length - splicing, tempbuffer, 0, splicing) '获取下一个包的数据
                        receivedBuffer = tempbuffer '将下一个包的数据存入receivedBuffer
                    Else
                        receivedBuffer = Nothing '未发生粘包则将receivedBuffer置为null
                    End If
                    context.Post(New Threading.SendOrPostCallback(Sub()
                                                                      RaiseEvent OnDatagramReceived(Me, datagram)
                                                                  End Sub), Nothing) '触发事件:接收到完整数据包
                Catch ex As Datagram.DatagramException
                    context.Post(New Threading.SendOrPostCallback(Sub()
                                                                      RaiseEvent OnDataVerifyFailed(Me, ex)
                                                                  End Sub), Nothing) '如果抛出了DatagramException,则触发事件:数据包校验错误。注意如果是MD5校验错误说明发生了丢包,如果是长度校验错误则可能是还没接收完。
                End Try
            End If
            If client.Available = 0 Then '如果缓冲区没有数据可读了
                receivedBuffer = Nothing
            End If
            stream.BeginRead(mybuffer, 0, mybuffer.Length, New AsyncCallback(AddressOf handleDatagramReceived), client) '继续下一次读取
        End If
    End Sub
    Public Sub DisconnectAsync()
        If client.Connected Then
            client.Client.BeginDisconnect(True, New AsyncCallback(AddressOf handDisconnected), client)
        End If
    End Sub
    Private Sub handDisconnected(ar As IAsyncResult)
        Dim tcp = CType(ar.AsyncState, TcpClient)
        tcp.Client.EndDisconnect(ar)
        context.Post(New Threading.SendOrPostCallback(Sub()
                                                          RaiseEvent OnDisconnected(Me, tcp)
                                                      End Sub), Nothing)
    End Sub
#End Region
#Region "发送"
    ''' <summary>
    ''' 发送指定的报文到服务器
    ''' </summary>
    ''' <param name="bytes">要发送的报文</param>
    Public Sub SendAsync(bytes As Byte())
        If Not Connected Then
            Throw New InvalidOperationException("尚未请求与服务器的连接或连接已断开")
        End If
        If bytes Is Nothing Then
            Throw New ArgumentNullException("参数bytes不能为null")
        End If
        Dim d = New Datagram(Encoding, bytes)
        client.GetStream.BeginWrite(d.BytesofAll, 0, d.Length, New AsyncCallback(AddressOf handleDatagramWritten), bytes)
    End Sub
    ''' <summary>
    ''' 发送指定的字符串到服务器
    ''' </summary>
    ''' <param name="txt">要发送的字符串</param>
    Public Sub SendAsync(txt As String)
        If txt = String.Empty Then
            Throw New ArgumentException("不能发送空字符串")
        End If
        SendAsync(Encoding.GetBytes(txt))
    End Sub
    ''' <summary>
    ''' 发送指定的Datagram报文到服务器
    ''' </summary>
    ''' <param name="datagram">要发送的Datagram报文实例</param>
    Public Sub SendAsync(datagram As Datagram)
        If datagram Is Nothing Then
            Throw New ArgumentNullException("数据报文不能为null")
        End If
        SendAsync(datagram.BytesofAll)
    End Sub
    Private Sub handleDatagramWritten(ar As IAsyncResult)
        Dim bytes = CType(ar.AsyncState, Byte())
        client.GetStream.EndWrite(ar)
        context.Post(New Threading.SendOrPostCallback(Sub()
                                                          RaiseEvent OnDatagramSend(Me, bytes)
                                                      End Sub), Nothing)
    End Sub
#End Region
#Region "IDisposable Support"
    ' IDisposable
    Protected Overridable Sub Dispose(disposing As Boolean)
        If Not disposed Then
            DisconnectAsync()
            client.Close()

            If disposing Then
                ' TODO: 释放托管状态(托管对象)。
                client = Nothing
            End If

            ' TODO: 释放未托管资源(未托管对象)并在以下内容中替代 Finalize()。
            ' TODO: 将大型字段设置为 null。
        End If
        disposed = True
    End Sub

    ' TODO: 仅当以上 Dispose(disposing As Boolean)拥有用于释放未托管资源的代码时才替代 Finalize()。
    'Protected Overrides Sub Finalize()
    '    ' 请勿更改此代码。将清理代码放入以上 Dispose(disposing As Boolean)中。
    '    Dispose(False)
    '    MyBase.Finalize()
    'End Sub

    ' Visual Basic 添加此代码以正确实现可释放模式。
    Public Sub Dispose() Implements IDisposable.Dispose
        ' 请勿更改此代码。将清理代码放入以上 Dispose(disposing As Boolean)中。
        Dispose(True)
        ' TODO: 如果在以上内容中替代了 Finalize(),则取消注释以下行。
        'GC.SuppressFinalize(Me)
    End Sub
#End Region
End Class

客户端实体类:

''' <summary>
''' 客户端信息
''' </summary>
Public Class ClientInfo
    ''' <summary>
    ''' '用于暂存每一次从接收缓冲区读取到的数据,每次接收都覆盖上一次数据
    ''' </summary>
    ''' <returns></returns>
    Public Property Buffer As Byte()
    ''' <summary>
    '''  '用于累存从接收缓冲区读取到的数据,直到接收完一个完整数据包或者缓冲区无数据时,置为null
    ''' </summary>
    ''' <returns></returns>
    Public Property ReceivedBuffer As Byte()
    ''' <summary>
    ''' 用于与服务器连接并通信的TcpClient实例
    ''' </summary>
    ''' <returns></returns>
    Public Property TcpClient As System.Net.Sockets.TcpClient
    Public Sub New(buffer As Byte(), client As Net.Sockets.TcpClient)
        Me.Buffer = buffer
        TcpClient = client
    End Sub
End Class

报文实体类:

''' <summary>
''' 根据一定规则格式化的报文
''' </summary>
Public Class Datagram
#Region "属性"
    ''' <summary>
    ''' 获取或设置报文标志位:报文类型(为数据报的第0位)
    ''' </summary>
    ''' <returns></returns>
    Public Property Flag As Byte
    ''' <summary>
    ''' 获取或设置报文长度位:表示数据报的总长度(为数据报的1-4位)
    ''' </summary>
    ''' <returns></returns>
    Public Property Length As Integer
    ''' <summary>
    ''' 获取或设置报文数据位:报文内容(为数据报的第5位到最后)
    ''' </summary>
    ''' <returns></returns>
    Public Property BytesofData As Byte()
    ''' <summary>
    ''' 获取或设置数据报全文
    ''' </summary>
    ''' <returns></returns>
    Public Property BytesofAll As Byte()
    ''' <summary>
    ''' 获取或设置报文的字符编码
    ''' </summary>
    ''' <returns>报文格式:Flag(1)+Length(4)+MD5(48)+Data</returns>
    Public Property Encoding As Text.Encoding
    ''' <summary>
    ''' 报文内容(BytesofData)的MD5校验值
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property MD5 As String
#End Region
#Region "构造函数"
    ''' <summary>
    ''' 将符合Datagram规则的字节数组,按照指定的字符编码,生成新的Datagram实例
    ''' </summary>
    ''' <param name="receivedBytes">符合Datagram规则的字节数组</param>
    ''' <param name="encoding">字符编码</param>
    Public Sub New(receivedBytes As Byte(), encoding As Text.Encoding)
        BytesofAll = receivedBytes
        Me.Encoding = encoding
        Flag = receivedBytes(0)
        '长度校验
        Length = BitConverter.ToInt32(receivedBytes, 1)
        If Length > receivedBytes.Length Then
            Dim ex As New DatagramException($"报文长度校验错误!{vbNewLine}报文总长度为{receivedBytes.Length},报文长度位声明的报文长度应为{Length}")
            ex.ErrorCode = DatagramException.DatagramErrorCode.DatagramLengthError
            ex.Datagram = Me
            Throw ex
        End If
        '获取报文中携带的MD5值
        Dim md5bytes(47) As Byte
        Buffer.BlockCopy(receivedBytes, 5, md5bytes, 0, 48)
        MD5 = encoding.GetString(md5bytes)
        '获取报文内容
        Dim dataBuffer(Length - 54) As Byte
        Buffer.BlockCopy(receivedBytes, 53, dataBuffer, 0, Length-53)
        BytesofData = dataBuffer
        Dim md5str = getMD5Str(BytesofData) '根据报文内容计算MD5值
        'md5校验
        If MD5 <> md5str Then
            Dim ex As New DatagramException($"报文MD5校验错误!{vbNewLine}报文内容MD5值为:{md5str}{vbNewLine}报文中的MD5值为:{MD5}")
            ex.ErrorCode = DatagramException.DatagramErrorCode.DatagramMD5Error
            ex.Datagram = Me
            Throw ex
        End If
    End Sub
    ''' <summary>
    ''' 将字符串,按照指定的字符编码,生成新的Datagram实例
    ''' </summary>
    ''' <param name="txt">要生成Datagram实例的字符串</param>
    ''' <param name="encoding">字符编码</param>
    Public Sub New(txt As String, encoding As Text.Encoding)
        Flag = 1
        Me.Encoding = encoding
        BytesofData = encoding.GetBytes(txt)
        Length = BytesofData.Length + 53
        MD5 = getMD5Str(BytesofData)
        BytesofAll = ToBytes()
    End Sub
    ''' <summary>
    ''' 将字节数组,按照指定的字符编码,生成新的Datagram实例
    ''' </summary>
    ''' <param name="encoding">字符编码</param>
    ''' <param name="bytesofData">要生成Datagram实例的字节数组</param>
    Public Sub New(encoding As Text.Encoding, bytesofData As Byte())
        Flag = 1
        Me.Encoding = encoding
        Me.BytesofData = bytesofData
        MD5 = getMD5Str(bytesofData)
        Length = bytesofData.Length + 53
        BytesofAll = ToBytes()
    End Sub
    ''' <summary>
    ''' 将可序列化对象,按照指定的字符编码,生成新的Datagram实例
    ''' </summary>
    ''' <param name="encoding">字符编码</param>
    ''' <param name="obj">可序列化对象(自定义类用Serializable标记)</param>
    Public Sub New(encoding As Text.Encoding, obj As Object)
        Me.New(encoding, Format(obj))
    End Sub
#End Region
    ''' <summary>
    ''' 返回当前Datagram的报文全文
    ''' </summary>
    ''' <returns></returns>
    Private Function ToBytes() As Byte()
        '组装报文
        Dim tempb(Length - 1) As Byte
        tempb(0) = Flag '报文Flag
        Buffer.BlockCopy(BitConverter.GetBytes(Length), 0, tempb, 1, 4) '报文Length
        Buffer.BlockCopy(getMD5Bytes(MD5), 0, tempb, 5, 48) '报文MD5
        Buffer.BlockCopy(BytesofData, 0, tempb, 53, BytesofData.Length) '报文内容
        Return tempb
    End Function
    ''' <summary>
    ''' 返回当前Datagram的报文内容的字符串
    ''' </summary>
    ''' <returns></returns>
    Public Overrides Function ToString() As String
        Return Encoding.GetString(BytesofData)
    End Function
#Region "MD5"
    Private Shared Function getMD5Str(bytes As Byte()) As String
        Dim md5 = New Security.Cryptography.MD5CryptoServiceProvider
        Dim data = md5.ComputeHash(bytes)
        Dim sb = New Text.StringBuilder
        For i = 0 To data.Length - 1
            sb.Append(data(i).ToString("x2") + "1")
        Next
        Return sb.ToString.ToUpper
    End Function
    Private Function getMD5Bytes(bytes As Byte()) As Byte()
        Dim md5str = getMD5Str(bytes)
        Return Encoding.GetBytes(md5str)
    End Function
    Private Function getMD5Bytes(md5str As String) As Byte()
        Return Encoding.GetBytes(md5str)
    End Function
#End Region
    'format方法用于序列化对象
    Private Shared Function Format(obj As Object) As Byte()
        Dim f As New Runtime.Serialization.Formatters.Binary.BinaryFormatter
        Dim stream As IO.Stream = New IO.MemoryStream
        f.Serialize(stream, obj)
        Dim buffer(stream.Length - 1) As Byte
        stream.Read(buffer, 0, stream.Length)
        Return buffer
    End Function
#Region "Exception"
    ''' <summary>
    ''' 报文异常
    ''' </summary>
    Public Class DatagramException
        Inherits Exception
        Dim _message As String
        Public Sub New(msg As String)
            _message = msg
        End Sub
        Public Overrides ReadOnly Property Message As String
            Get
                Return _message
            End Get
        End Property
        Public Property ErrorCode As DatagramErrorCode
        Public Property Datagram As Datagram
        Public Enum DatagramErrorCode
            DatagramLengthError = 1
            DatagramMD5Error = 2
        End Enum
    End Class
#End Region
End Class

怎么调用就不多说了,很简单。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值