[.Net] Serveur TCP Asynchrone et Client Tcp Asynchrone

Asynchronous tcp

J’utilise énormément les deux classes suivantes qui permettent d’instancier en asynchrone des serveurs TCP ou des clients TCP.

La classe ASyncTcpServer m’a permis ainsi de faire un serveur Web, un serveur FTP, un serveur de données et tout cela de manière asynchrone.

La classe ASyncTcpClient quant à elle permet de se connecter à des serveurs TCP divers et variés.

Je tiens à préciser que j’ai juste convertit ces classes en Vb.Net et qu’elles ont été développées à l’origine par Rob Davey.

Imports System.Net.Sockets
Imports System.Net
Imports System.Text
Imports System.Linq
Imports System.Threading
 
Module AsyncTcp
 
    ''' <summary>
    ''' An Asynchronous TCP Server that makes use of system managed threads
    ''' and callbacks to stop the server ever locking up.
    ''' </summary>
    Public Class AsyncTcpServer
 
#Region "Properties"
        Private TcpListener As TcpListener
        Private Clients As New List(Of Client)
 
        ''' <summary>
        ''' The encoding to use when sending / receiving strings.
        ''' </summary>
        Public Property Encoding As Encoding
 
        ''' <summary>
        ''' The data received.
        ''' </summary>
        Public Event DataReceived(ByRef Data() As Byte, ByRef DataLength As Integer, ByRef RemoteIP As String)
 
        ''' <summary>
        ''' An enumerable collection of all the currently connected tcp clients
        ''' </summary>
        Public ReadOnly Property TcpClients As IEnumerable(Of TcpClient)
            Get
                Return From x In Clients Select x.TcpClient
            End Get
        End Property
#End Region
 
#Region "Constructeurs"
        ''' <summary>
        ''' Constructor for a new server using an IPAddress and Port
        ''' </summary>
        ''' <param name="localaddr">The Local IP Address for the server.</param>
        ''' <param name="port">The port for the server.</param>
        Public Sub New(ByVal localaddr As IPAddress, ByVal port As Integer)
            Me.New()
            TcpListener = New TcpListener(localaddr, port)
        End Sub
 
        ''' <summary>
        ''' Constructor for a new server using an end point
        ''' </summary>
        ''' <param name="localEP">The local end point for the server.</param>
        Public Sub New(ByVal localEP As IPEndPoint)
            Me.New()
            TcpListener = New TcpListener(localEP)
        End Sub
 
        ''' <summary>
        ''' Private constructor for the common constructor operations.
        ''' </summary>
        Private Sub New()
            Me.Encoding = Encoding.Default
            Me.Clients = New List(Of Client)
        End Sub
#End Region
 
#Region "Start / Stop"
        ''' <summary>
        ''' Starts the TCP Server listening for new clients.
        ''' </summary>
        Public Sub Start()
            Me.TcpListener.Start()
            Me.TcpListener.BeginAcceptTcpClient(AddressOf AcceptTcpClientCallback, Nothing)
        End Sub
 
        ''' <summary>
        ''' Stops the TCP Server listening for new clients and disconnects
        ''' any currently connected clients.
        ''' </summary>
        Public Sub [Stop]()
            Me.TcpListener.Stop()
            SyncLock Me.Clients
                For Each c As Client In Me.Clients
                    c.TcpClient.Client.Disconnect(False)
                Next
                Me.Clients.Clear()
            End SyncLock
        End Sub
#End Region
 
#Region "Write"
        ''' <summary>
        ''' Writes a string to a given TCP Client
        ''' </summary>
        ''' <param name="tcpClient">The client to write to</param>
        ''' <param name="data">The string to send.</param>
        Public Sub Write(ByVal tcpclient As TcpClient, ByVal data As String)
            Dim bytes() As Byte = Me.Encoding.GetBytes(data)
            Write(tcpclient, bytes)
        End Sub
 
        ''' <summary>
        ''' Writes a string to all clients connected.
        ''' </summary>
        ''' <param name="data">The string to send.</param>
        Public Sub Write(ByVal data As String)
            For Each c As Client In Me.Clients
                Write(c.TcpClient, data)
            Next
        End Sub
 
        ''' <summary>
        ''' Writes a byte array to all clients connected.
        ''' </summary>
        ''' <param name="bytes">The bytes to send.</param>
        Public Sub Write(ByVal bytes() As Byte)
            For Each c As Client In Me.Clients
                Write(c.TcpClient, bytes)
            Next
        End Sub
 
        ''' <summary>
        ''' Writes a byte array to a given TCP Client
        ''' </summary>
        ''' <param name="tcpClient">The client to write to</param>
        ''' <param name="bytes">The bytes to send</param>
        Public Sub Write(ByVal tcpClient As TcpClient, ByVal bytes() As Byte)
            Dim networkStream As NetworkStream = tcpClient.GetStream()
            networkStream.BeginWrite(bytes, 0, bytes.Length, AddressOf WriteCallback, tcpClient)
        End Sub
 
        ''' <summary>
        ''' Callback for the write opertaion.
        ''' </summary>
        ''' <param name="result">The async result object</param>
        Private Sub WriteCallback(ByVal result As IAsyncResult)
            Dim tcpClient As TcpClient = CType(result.AsyncState, TcpClient)
            Dim networkStream As NetworkStream = tcpClient.GetStream()
            networkStream.EndWrite(result)
        End Sub
#End Region
 
        ''' <summary>
        ''' Callback for the accept tcp client opertaion.
        ''' </summary>
        ''' <param name="result">The async result object</param>
        Private Sub AcceptTcpClientCallback(ByVal result As IAsyncResult)
            Dim tcpClient As TcpClient = TcpListener.EndAcceptTcpClient(result)
            Dim buffer(tcpClient.ReceiveBufferSize) As Byte
            Dim client As New Client(tcpClient, buffer)
            SyncLock Me.Clients
                Me.Clients.Add(client)
            End SyncLock
            Dim networkStream As NetworkStream = client.NetworkStream
            networkStream.BeginRead(client.Buffer, 0, client.Buffer.Length, AddressOf ReadCallback, client)
            TcpListener.BeginAcceptTcpClient(AddressOf AcceptTcpClientCallback, Nothing)
        End Sub
 
        ''' <summary>
        ''' Callback for the read opertaion.
        ''' </summary>
        ''' <param name="result">The async result object</param>
        Private Sub ReadCallback(ByVal result As IAsyncResult)
            Dim client As Client = CType(result.AsyncState, Client)
            If client Is Nothing Then Exit Sub
            Try
                Dim networkStream As NetworkStream = client.NetworkStream
                Dim read As Integer = networkStream.EndRead(result)
 
                If read = 0 Then
                    SyncLock Me.Clients
                        Me.Clients.Remove(client)
                        Exit Sub
                    End SyncLock
                End If
                Dim data As String = Me.Encoding.GetString(client.Buffer, 0, read)
                'Do something with the data object here.
                RaiseEvent DataReceived(client.Buffer, read, CType(client.TcpClient.Client.LocalEndPoint, IPEndPoint).Address.ToString())
                'Then start reading from the network again.
                networkStream.BeginRead(client.Buffer, 0, client.Buffer.Length, AddressOf ReadCallback, client)
            Catch ex As Exception
            End Try
        End Sub
 
#Region "Class Client"
        ''' <summary>
        ''' Internal class to join the TCP client and buffer together
        ''' for easy management in the server
        ''' </summary>
        Class Client
            ''' <summary>
            ''' Constructor for a new Client
            ''' </summary>
            ''' <param name="tcpClient">The TCP client</param>
            ''' <param name="buffer">The byte array buffer</param>
            Public Sub New(ByVal tcpClient As TcpClient, ByVal buffer() As Byte)
                If tcpClient Is Nothing Then
                    Throw New ArgumentNullException("tcpClient")
                End If
                If buffer Is Nothing Then
                    Throw New ArgumentNullException("buffer")
                End If
                Me.TcpClient = tcpClient
                Me.Buffer = buffer
            End Sub
 
            ''' <summary>
            ''' Gets the TCP Client
            ''' </summary>
            Public Property TcpClient As TcpClient
 
            ''' <summary>
            ''' Gets the Buffer.
            ''' </summary>
 
            Public Property Buffer As Byte()
 
            ''' <summary>
            ''' Gets the network stream
            ''' </summary>
 
            Public ReadOnly Property NetworkStream As NetworkStream
                Get
                    Return TcpClient.GetStream()
                End Get
            End Property
        End Class
#End Region
 
    End Class
 
    ''' <summary>
    ''' An Asynchronous TCP Client
    ''' </summary>
    Public Class AsyncTcpClient
 
#Region "Properties"
        Private Addresses As IPAddress()
        Private Port As Integer
        Private AddressesSet As WaitHandle
        Private TcpClient As TcpClient
        Private FailedConnectionCount As Integer
        Public Property Encoding As Encoding
        Public Event DataReceived(ByRef Data() As Byte, ByRef DataLength As Integer)
#End Region
 
#Region "Constructeurs"
        ''' <summary>
        ''' Construct a new client from a known IP Address
        ''' </summary>
        ''' <param name="address">The IP Address of the server</param>
        ''' <param name="port">The port of the server</param>
        Public Sub New(ByVal address As IPAddress, ByVal port As Integer)
            Me.New(New IPAddress() {address}, port)
        End Sub
 
        ''' <summary>
        ''' Construct a new client where multiple IP Addresses for
        ''' the same client are known.
        ''' </summary>
        ''' <param name="addresses">The array of known IP Addresses</param>
        ''' <param name="port">The port of the server</param>
        Public Sub New(ByVal addresses As IPAddress(), ByVal port As Integer)
            Me.New(port)
            Me.Addresses = addresses
        End Sub
 
        ''' <summary>
        ''' Construct a new client where the address or host name of
        ''' the server is known.
        ''' </summary>
        ''' <param name="hostNameOrAddress">The host name or address of the server</param>
        ''' <param name="port">The port of the server</param>
        Public Sub New(ByVal hostNameOrAddress As String, ByVal port As Integer)
            Me.New(port)
            AddressesSet = New AutoResetEvent(False)
            Dns.BeginGetHostAddresses(hostNameOrAddress, AddressOf GetHostAddressesCallback, Nothing)
        End Sub
 
        ''' <summary>
        ''' Private constuctor called by other constuctors
        ''' for common operations.
        ''' </summary>
        ''' <param name="port"></param>
        Private Sub New(ByVal port As Integer)
            If port < 0 Then
                Throw New ArgumentException()
            End If
            Me.Port = port
            Me.TcpClient = New TcpClient()
            Me.Encoding = Encoding.Default
        End Sub
#End Region
 
#Region "Connect"
        ''' <summary>
        ''' Attempts to connect to one of the specified IP Addresses
        ''' </summary>
        Public Sub Connect()
            If AddressesSet IsNot Nothing Then
                'Wait for the addresses value to be set
                AddressesSet.WaitOne()
            End If
            'Set the failed connection count to 0
            Interlocked.Exchange(FailedConnectionCount, 0)
            'Start the async connect operation
            TcpClient.BeginConnect(Addresses, Port, AddressOf ConnectCallback, Nothing)
        End Sub
 
        ''' <summary>
        ''' Callback for Connect operation
        ''' </summary>
        ''' <param name="result">The AsyncResult object</param>
        Private Sub ConnectCallback(ByVal result As IAsyncResult)
            Try
                TcpClient.EndConnect(result)
            Catch ex As Exception
                'Increment the failed connection count in a thread safe way
                Interlocked.Increment(FailedConnectionCount)
                If (FailedConnectionCount >= Addresses.Length) Then
                    'We have failed to connect to all the IP Addresses connection has failed overall.
                    Exit Sub
                End If
            End Try
            'We are connected successfully.
            Dim networkStream As NetworkStream = TcpClient.GetStream()
            Dim buffer(TcpClient.ReceiveBufferSize) As Byte
            'Now we are connected start asyn read operation.
            networkStream.BeginRead(buffer, 0, buffer.Length, AddressOf ReadCallback, buffer)
        End Sub
#End Region
 
#Region "Write"
        ''' <summary>
        ''' Writes a string to the network using the defualt encoding.
        ''' </summary>
        ''' <param name="data">The string to write</param>
        ''' <returns>A WaitHandle that can be used to detect
        ''' when the write operation has completed.</returns>
        Public Sub Write(ByVal data As String)
            Dim bytes() As Byte = Encoding.GetBytes(data)
            Write(bytes)
        End Sub
 
        ''' <summary>
        ''' Writes an array of bytes to the network.
        ''' </summary>
        ''' <param name="bytes">The array to write</param>
        ''' <returns>A WaitHandle that can be used to detect
        ''' when the write operation has completed.</returns>
        Public Sub Write(ByVal bytes() As Byte)
            Dim networkStream As NetworkStream = TcpClient.GetStream
            'Start async write operation
            networkStream.BeginWrite(bytes, 0, bytes.Length, AddressOf WriteCallback, Nothing)
        End Sub
 
        ''' <summary>
        ''' Callback for Write operation
        ''' </summary>
        ''' <param name="result">The AsyncResult object</param>
        Private Sub WriteCallback(ByVal result As IAsyncResult)
            Dim networkStream As NetworkStream = TcpClient.GetStream
            networkStream.EndWrite(result)
        End Sub
#End Region
 
#Region "Read"
        ''' <summary>
        ''' Callback for Read operation
        ''' </summary>
        ''' <param name="result">The AsyncResult object</param>
        Private Sub ReadCallback(ByVal result As IAsyncResult)
            Dim read As Integer
            Dim networkStream As NetworkStream
            Try
                networkStream = TcpClient.GetStream()
                read = networkStream.EndRead(result)
            Catch ex As Exception
                'An error has occured when reading
                Exit Sub
            End Try
 
            If read = 0 Then
                Exit Sub
            End If
 
            Dim buffer() As Byte = CType(result.AsyncState, Byte())
            Dim data As String = Me.Encoding.GetString(buffer, 0, read)
            'Do something with the data object here.
            RaiseEvent DataReceived(buffer, read)
            'Then start reading from the network again.
            networkStream.BeginRead(buffer, 0, buffer.Length, AddressOf ReadCallback, buffer)
        End Sub
 
#End Region
 
#Region "Diverses fonctions"
        ''' <summary>
        ''' Callback for Get Host Addresses operation
        ''' </summary>
        ''' <param name="result">The AsyncResult object</param>
        Private Sub GetHostAddressesCallback(ByVal result As IAsyncResult)
            Addresses = Dns.EndGetHostAddresses(result)
            'Signal the addresses are now set
            CType(AddressesSet, AutoResetEvent).Set()
        End Sub
#End Region
    End Class
 
End Module

2 Responses to [.Net] Serveur TCP Asynchrone et Client Tcp Asynchrone

  1. Jlau says:

    Bonsoir,

    Je suis entrain de me lancer dans l’écriture d’un soft qui utilisera TCP pour échanger des données. Je vais faire les parties client et serveur.
    D’ou mon intérêt pour ces deux classes 😉
    Auriez vous un exemple d’utilisation de ces deux classes (client et serveur) ?
    Je dois faire envoyer des données dont la taille peut aller jusqu’à 300ko, est ce qu’il faut faire une gestion particulière des paquets (découpage ?) ou est ce que l’envoi se fait en une seule fois ?

    Merci d’avance.

  2. jhd says:

    Pour l’exemple je ne pense pas que tu en ai besoin c’est relativement simple à utiliser et le code est assez commenté.

    Concernant l’envoi et la réception de gros fichiers tu n’as besoin de rien faire. La pile IP des ordinateurs s’occupera de couper en paquets adaptés au réseau. C’est totalement transparent pour toi.

Leave a Reply

Your email address will not be published. Required fields are marked *