制作一个网络通讯类
简介
TcpListener类提供一些简单方法,用于在同步阻塞模式下侦听和接受传入连接请求。
TcpClient 类提供了一些简单的方法,用于在同步阻塞模式下通过网络来连接、发送和接收流数据。
为了使用方便,我利用.Net提供的这两个类作了一个网络通讯用的类CTcpTalk。
工作原理和使用方法
* 每个CTcpTalk对象中包含一个用于监听的TcpListener部件,一个用于传输数据的TcpClient部件,和一个用于接收连接请求的TcpClient部件。
* 在创建一个CTcpTalk时需要指定要使用的端口号。然后使用CTcpTalk.Open开启对网络的监听。
* 接收数据:当监听到有数据传送到本机时,使用接收连接请求的TcpClient部件接收对方的连接请求以及发送来的数据。接收完毕后关闭TcpClient部件,并触发DataArrival事件,可以使用GetData()函数获取收到的数据。
* 发送数据:设置接收方的名称和端口,使用传输数据的TcpClient部件请求连接,连接成功后发送数据。数据发送完毕之后关闭TcpClient部件,并触发SendComplete事件。
设计中的问题
* 由于TcpListener和TcpClient都是工作在同步阻塞模式下,因此数据传输和监听都使用了单独的线程。
* 对于TcpListener的监听线程,因为是阻塞的模式,所以在关闭监听时,需要先由本机向本机自己发一个连接请求,以解除监听线程的阻塞,然后通过相应量的设置,退出监听循环,关闭监听。在监听阻塞状态下直接关闭监听会导致错误,通过错误陷阱隐藏后,似乎也不会影响后面的使用。
* 使用流模式读取和发送数据,为了方便而采用了流的同步读写。
* 设计为发送方申请建立连接、发送接收完毕后立刻断开连接的模式。类似于点对点的模型,没有服务器客户端之分。参加通讯的机器只需要维持一个监听线程就可以了。而不必保留已连接列表并随时检查列表中各个项的连接状态。这也是因为采用了同步读写模式,如果阻塞流的读线程反而会大大降低性能。
* 对于传输数据量的大小,有8K字节的限制。由于使用了Unicode编码解码,所以实际的传输量测试为每次4K以下。可以通过外部编程对大数据量进行分页传输,但是在内部仍然是每次传输前建立连接、传输完毕后断开连接的方式。因此对于过大的数据需要消耗额外的资源用于频繁建立和断开连接。
* 因为可能要用于.Net Framework精简版,所以方法、事件和属性都考虑使用受精简版支持的版本。
测试程序界面(单机测试)
本界面为单机测试结果。此程序也可用于多机。
按钮加入网络
启动本机的网络监听。此按钮在已经启动监听后不可用
Name = BJoinNet
按钮退出网络
关闭本机的网络监听。关闭之后将无法再接收连接请求。此按钮在监听关闭时不可用
Name = BExitNet
按钮关闭程序
关闭程序
Name = BClose
按钮发送
发送文本框中的内容。在未加入网络时此按钮不可用。
Name = BSend
文本框发送的内容
Name = TBSend
MultiLine = True
ScrollBars = Vertical
文本框接收的内容
Name = TBRecv
MultiLine = True
ScrollBars = Vertical
ReadOnly = True
文本框状态监视
Name = TBState
MultiLine = True
ScrollBars = Vertical
ReadOnly = True
测试程序代码
组件声明
Private WithEvents sck1 As CTcpTalk
界面加载
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
'获取本机名称和IP
Try
LLocalName.Text = Dns.GetHostName
LLocalIP.Text = Dns.Resolve(LLocalName.Text).AddressList(0).ToString
Catch ex As Exception
LLocalName.Text = "无法获得主机名"
LLocalIP.Text = "无法获得主机IP"
End Try
sck1 = New CTcpTalk
'重绘界面
SetUIDisconnect()
End Sub
界面关闭
Private Sub Form1_Closing(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
If sck1 Is Nothing Then
Else
If sck1.State <> CTcpTalk.StateConstants.sckClosed Then
sck1.Close()
End If
End If
Application.Exit()
End Sub
按钮加入网络
Private Sub BJoinNet_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BJoinNet.Click
'检查端口号
If TBPort.Text = "" Then
MsgBox("请输入端口号")
Exit Sub
End If
Dim port As Long
Try
port = CLng(TBPort.Text)
Catch ex As Exception
MsgBox("端口号格式错误, 请重新设置")
Exit Sub
End Try
'开启监听
sck1 = New CTcpTalk(port)
sck1.Open()
'设置界面
SetUIListen()
End Sub
按钮退出网络
Private Sub BExitNet_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BExitNet.Click
'设置界面
SetUIWait()
AppendTxt(TBState, "正在退出网络...")
'关闭监听
sck1.Close()
'设置界面
SetUIDisconnect()
AppendTxt(TBState, "已经退出网络")
End Sub
按钮关闭程序
Private Sub BClose_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BClose.Click
If sck1.State <> CTcpTalk.StateConstants.sckClosed Then
'关闭监听
sck1.Close()
SetUIDisconnect()
End If
'退出程序
Application.Exit()
End Sub
按钮发送
Private Sub BSend_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BSend.Click
'检查参数
If TBRemote.Text = "" Then
MsgBox("请输入对方计算机名称或IP")
TBRemote.Focus()
Exit Sub
End If
If TBPort.Text = "" Then
MsgBox("请输入端口号")
Exit Sub
End If
Dim port As Long
Try
port = CLng(TBPort.Text)
Catch ex As Exception
MsgBox("端口号格式错误")
Exit Sub
End Try
'设置远程主机名称和端口
sck1.RemotePort = port
sck1.RemoteHost = TBRemote.Text
'发送数据
sck1.Send(TBSend.Text)
End Sub
sck1的DataArrival事件
Private Sub sck1_DataArrival(ByVal bytesTotal As Long) Handles sck1.DataArrival
AppendTxt(TBRecv, sck1.GetData)
End Sub
sck1的ErrorEvt事件
Private Sub sck1_ErrorEvt(ByVal ex As CTcpTalkException) Handles sck1.ErrorEvt
AppendTxt(TBState, ex.Message)
End Sub
sck1的Connect事件
Private Sub sck1_Connect() Handles sck1.Connect
AppendTxt(TBState, "Connected")
End Sub
sck1的SendComplete事件
Private Sub sck1_SendComplete() Handles sck1.SendComplete
AppendTxt(TBState, "Send Complete")
End Sub
设置界面(无监听状态)
Private Sub SetUIDisconnect()
BJoinNet.Enabled = True
BExitNet.Enabled = False
BSend.Enabled = False
End Sub
设置界面(监听状态)
Private Sub SetUIListen()
BJoinNet.Enabled = False
BExitNet.Enabled = True
BSend.Enabled = True
End Sub
设置界面(等待状态)
Private Sub SetUIWait()
BJoinNet.Enabled = False
BExitNet.Enabled = False
BSend.Enabled = False
End Sub
向指定文本框添加文本
Private Sub AppendTxt(ByVal tb As TextBox, ByVal txt As String)
tb.Text = tb.Text + txt + vbCrLf
End Sub
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的调用用队列中删除已经接收的连接请求。另外由于采用了在传输时动态建立连接的结构,不需要长期维护多个连接有效,使等待处理的队列非常短,所以对于不是特别频繁的多个连接请求,本例子都可以轻松的处理。但是并没有进行非常严格的极限测试,所以不保证对于大量的、并发性较强的多个连接能够有效处理。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/zhangjie_xiaoke/archive/2008/11/25/3370850.aspx