zoukankan      html  css  js  c++  java
  • 制作一个网络通讯类

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

  • 相关阅读:
    从POJ1958引发对n盘m塔Hanoi问题的思考
    SPOJGSS3 Can you answer these queries III
    【模板】SPFA判负环(洛谷P3385)
    【模板】强联通缩点(洛谷P3387)
    Luogu P2186 小Z的栈函数
    Luogu P2129 小Z的情书
    LGBT学分块
    LGBT玩扫雷
    A 美丽的子树
    B(升降序列)
  • 原文地址:https://www.cnblogs.com/googlegis/p/2979018.html
Copyright © 2011-2022 走看看