http://coffecoco.bokee.com/806282.html
制作一个网络通讯类(二)- -
CTcpTalk类代码
Public Class CTcpTalk
'状态枚举
Public Enum StateConstants
sckClosed = 0 '已经关闭
sckListening = 1 '正在监听
sckConnectionPending = 2 '连接未决
sckResolvingHost = 3 '正在解析主机
sckHostResolved = 4 '主机解析完毕
sckConnecting = 5 '正在连接
sckConnected = 6 '已连接
sckClosing = 7 '正在关闭
sckError = 100 '错误
End Enum
'事件
'监听关闭时触发
Public Event Closed()
'建立新连接时触发
Public Event Connect()
'接收到数据时触发
Public Event DataArrival(ByVal bytesTotal As Long)
'发生错误时触发
Public Event ErrorEvt(ByVal ex As CTcpTalkException)
'发送完成时触发
Public Event SendComplete()
'成员
'收到的字节总数
Private m_BytesReceived As Long
'存储错误信息的对象
Private m_Error As CTcpTalkException
'索引号
Private m_Index As Integer
'本机名称
Private m_LocalHostName As String
'本机IP
Private m_LocalIP As String
'监听端口
Private m_LocalPort As Long
'远程主机
Private m_RemoteHost As String
'远程主机IP
Private m_RemoteHostIP As String
'远程端口
Private m_RemotePort As Long
'状态
Private m_State As StateConstants
'接收到的字符串
Private m_DataReceived As String
'要发送的字符串
Private m_DataSend As String
'监听器
Private m_sckListen As TcpListener
'接收外部申请的TCPClient
Private m_sckAccept As TcpClient
'用于申请连接的TCPClient
Private m_sckClient As TcpClient
'停止监听控制变量
Private m_stopListen As Boolean
'监听线程
Private m_thdListen As Thread
'发送线程
Private m_thdSend As Thread
'属性
'已经收到的字节总数
'只读
Public ReadOnly Property BytesReceived() As Long
Get
Return m_BytesReceived
End Get
End Property
'索引号。用于在控件数列中唯一标识控件对象
'只读
Public ReadOnly Property Index() As Integer
Get
Return m_Index
End Get
End Property
'本机的名称
'只读
Public ReadOnly Property LocalHostName() As String
Get
Return m_LocalHostName
End Get
End Property
'本机的IP
'只读
Public ReadOnly Property LocalIP() As String
Get
Return m_LocalIP
End Get
End Property
'监听端口, 允许在没有连接或监听的方法下访问
'如果没有设置, 则为0
Public Property LocalPort()
Get
Return m_LocalPort
End Get
Set(ByVal Value)
m_LocalPort = Value
End Set
End Property
'远程机器
'可以是IP地址,也可以是可解析的主机名
'必须在发送数据之前进行设置
Public Property RemoteHost() As String
Get
Return m_RemoteHost
End Get
Set(ByVal Value As String)
m_RemoteHost = Value
End Set
End Property
'远程机器IP
'只有在与远方主机连接建立之后才可以读取
'只读
Public ReadOnly Property RemoteHostIP() As String
Get
Return m_RemoteHostIP
End Get
End Property
'远程机器端口
'必须在发送数据或建立连接之前设置
Public Property RemotePort() As Long
Get
Return m_RemotePort
End Get
Set(ByVal Value As Long)
m_RemotePort = Value
End Set
End Property
'状态
Public ReadOnly Property State() As StateConstants
Get
Return m_State
End Get
End Property
'构造函数
Public Sub New()
InitObj()
End Sub
'用端口号初始化实例
'用于TCPListener
Public Sub New(ByVal listenport As Long)
InitObj()
m_LocalPort = listenport
End Sub
'用远端主机名和端口号初始化实例
'用于TCPClient
Public Sub New(ByVal hostname As String, ByVal hostport As Long)
InitObj()
m_RemoteHost = hostname
m_RemotePort = hostport
End Sub
'基本初始化
Private Sub InitObj()
'已经收到的字节总数
m_BytesReceived = 0
'需要传送的总字节数
m_BytesTotal = 0
'存储错误信息的对象
m_Error = New CTcpTalkException
'索引号
m_Index = -1
'本机名称
m_LocalHostName = Dns.GetHostName
m_Error = New CTcpTalkException(ex.Message)
'本机IP
m_LocalIP = Dns.Resolve(m_LocalHostName).AddressList(0).ToString
'绑定的端口
m_LocalPort = 0
'远程主机
m_RemoteHost = ""
'远程主机IP
m_RemoteHostIP = ""
'远程端口
m_RemotePort = 0
'状态
m_State = StateConstants.sckClosed
' 监听器
m_sckListen = Nothing
'接收外部申请的TCPClient
m_sckAccept = Nothing
'用于申请连接的TCPClient
m_sckClient = Nothing
'接收到的字符串
m_DataReceived = ""
'要发送的字符串
m_DataSend = ""
'停止监听
m_stopListen = True
End Sub
'方法
'启动监听线程
Public Sub Open()
If m_thdListen Is Nothing Then
Else '如果正在监听则关闭当前监听
If m_State <> StateConstants.sckClosed Then
Close()
End If
End If
m_thdListen = New Thread(AddressOf StartListen)
m_thdListen.Start()
End Sub
'获取收到的数据
Public Function GetData() As String
Dim str As String = m_DataReceived
m_DataReceived = ""
Return str
End Function
'启动发送数据线程
Public Sub Send(ByVal datasend As String)
m_DataSend = datasend
m_thdSend = New Thread(AddressOf SendMsg)
m_thdSend.Start()
End Sub
'发送数据
Private Sub SendMsg()
'进入连接状态
SetState(StateConstants.sckConnecting)
'检查参数
If m_RemotePort = 0 Then
ErrorHandle("Send", "没有设置端口号")
Exit Sub
End If
Try
Dns.Resolve(Me.RemoteHost)
Catch ex As Exception
ErrorHandle("Send", ex)
Exit Sub
End Try
'开始连接
Try
SetState(StateConstants.sckResolvingHost)
m_sckClient = New TcpClient(RemoteHost, RemotePort)
SetState(StateConstants.sckHostResolved)
SetState(StateConstants.sckConnected)
RaiseEvent Connect()
Catch ex As Exception
ErrorHandle("Send", ex)
Exit Sub
End Try
'开始发送数据
Try
Dim data As Byte() = System.Text.Encoding.Unicode.GetBytes(m_DataSend)
Dim stream As NetworkStream = m_sckClient.GetStream
stream.Write(data, 0, data.Length)
m_sckClient.Close()
SetState(StateConstants.sckListening)
RaiseEvent SendComplete()
Catch ex As Exception
ErrorHandle("Send", ex)
Exit Sub
End Try
End Sub
'监听线程
Private Sub StartListen()
'参数检查
If LocalPort = 0 Then
ErrorHandle("StartListen", "没有设置端口号")
Exit Sub
End If
'初始化监听用的套接字
Try
m_sckListen = New TcpListener(Dns.Resolve(LocalHostName).AddressList(0), LocalPort)
Catch ex As SocketException
ErrorHandle("StartListen", ex)
Exit Sub
End Try
Try
m_sckAccept = New TcpClient
m_sckListen.Start()
Catch ex As Exception
ErrorHandle("StartListen", ex)
Exit Sub
End Try
m_stopListen = True
'读缓冲
Dim bytes(5120) As [Byte]
Dim data As String = Nothing
'开始监听
Try
'进入监听循环
While m_stopListen
SetState(StateConstants.sckListening)
Try
'接收连接请求
m_sckAccept = m_sckListen.AcceptTcpClient
Catch ex As Exception
ErrorHandle("Listening and Accepting AcceptTcpClient", ex)
Exit Sub
End Try
SetState(StateConstants.sckConnected)
RaiseEvent Connect()
'开始接收数据
data = Nothing
m_BytesReceived = 0
m_DataReceived = ""
'用流对象进行读写
Dim stream As NetworkStream = m_sckAccept.GetStream
Dim i As Int32
i = stream.Read(bytes, 0, bytes.Length - 1)
m_BytesReceived = m_BytesReceived + i
'将数据字节转换为UNICODE字符串
data = System.Text.Encoding.Unicode.GetString(bytes, 0, i)
m_DataReceived = m_DataReceived + data
'循环接收客户端发来的所有数据
While (stream.DataAvailable)
i = stream.Read(bytes, 0, bytes.Length - 1)
m_BytesReceived = m_BytesReceived + bytes.Length
'将数据字节转换为UNICODE字符串
data = System.Text.Encoding.Unicode.GetString(bytes, 0, i)
m_DataReceived = m_DataReceived + data
End While
'触发DataArrival事件
RaiseEvent DataArrival(m_BytesReceived)
'关闭连接
m_sckAccept.Close()
End While
'关闭监听
m_sckListen.Stop()
SetState(StateConstants.sckClosed)
RaiseEvent Closed()
Catch ex As Exception
ErrorHandle("Listening and Accepting", ex)
Exit Sub
End Try
End Sub
'关闭监听
Public Sub Close()
'设置状态
SetState(StateConstants.sckClosing)
'设置监听循环终止标志
m_stopListen = False
'使用一个目标为本地主机的TCPClient
m_RemotePort = m_LocalPort
m_RemoteHost = m_LocalHostName
'发送结束符以解除监听线程的阻塞
Send("")
End Sub
'错误处理
Private Sub ErrorHandle(ByVal src As String, ByVal description As String)
'设置状态
SetState(StateConstants.sckError)
'设置错误通知
m_Error = New CTcpTalkException(src + " : " + description)
'触发错误事件
RaiseEvent ErrorEvt(m_Error)
End Sub
Private Sub ErrorHandle(ByVal src As String, ByVal ex As Exception)
'设置状态
SetState(StateConstants.sckError)
'设置错误通知
m_Error = New CTcpTalkException(src + " : " + ex.Message)
'触发错误事件
RaiseEvent ErrorEvt(m_Error)
End Sub
'设置状态
Private Sub SetState(ByVal state As StateConstants)
m_State = state
End Sub
End Class
用于传递错误信息的类CTcpTalkException
Public Class CTcpTalkException
Inherits Exception
Public Sub New()
MyBase.New()
End Sub
Public Sub New(ByVal msg As String)
MyBase.New(msg)
End Sub
End Class
常见问题
监听线程的处理
监听线程会在下列语句处阻塞,直到有连接请求进入。
'接收连接请求
m_sckAccept = m_sckListen.AcceptTcpClient
在阻塞的状况下,简单的使用Abort,仅仅是将线程设置为AbortRequest状态,而没有真正的解除线程的阻塞,甚至使用Application.Exit(),也无法真正的终止线程并释放线程所占有的资源。这样在下次在同一个端口调用监听时就会有错误发生。并且在应用程序退出后,监听线程依然像孤魂野鬼一般在内存中阻塞着。
如果在此时直接调用m_sckListen.Stop来终止监听,则会发生以下描述信息的错误:
一个封锁操作被对 WSACancelBlockingCall 的调用中断。
但是这样似乎并不会影响以后对此端口监听的调用,并且能够结束线程。
彻底解决问题的一种方法是用一个终止标志作为监听循环的条件,需要终止监听时,先设置终止标志为退出监听循环,然后向自己的监听器发送一个连接请求解除监听线程的阻塞,然后就可以安全的退出监听循环,关闭监听,并结束监听线程。这样在程序结束以后也不会有线程滞留的现象。
多个连接请求
对于多个连接请求,TcpListener将他们放入一个队列,直到到达可接收的连接的最大数,通过Accept的调用用队列中删除已经接收的连接请求。另外由于采用了在传输时动态建立连接的结构,不需要长期维护多个连接有效,使等待处理的队列非常短,所以对于不是特别频繁的多个连接请求,本例子都可以轻松的处理。但是并没有进行非常严格的极限测试,所以不保证对于大量的、并发性较强的多个连接能够有效处理。