最近一直在琢磨Socket通信,也查了很多资料,socket通信还是比较底层的东西,对我一个非科班出身的小菜来说有点难度。学习完就想着写个辅助类以后可以直接用。代码用vb.net写的,部分代码参考了博客园的一位大牛的文章:
主要特色:
- 采用的异步回调模式,不用考虑阻塞主线程的问题。
- 事件驱动,拒绝轮询。
- 对数据报文进行了简单封装,组装报文只要简单的new一个就行了,并且加入了FLAG可用于标识报文类型,加入了报文长度标识,加入了报文内容的MD5校验,保证报文传输的可靠。
- 在3的基础上实现了自动处理分包和粘包。
- 加入了对象序列化功能,可以直接发送对象,此功能待完善。
待加入报文内容加密功能,有空再写。
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
怎么调用就不多说了,很简单。