制作一个网络通讯类

简介
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

原文地址:https://www.cnblogs.com/googlegis/p/2979018.html