Imports System.Net.Sockets
Imports System.Net
Imports System.Threading
Imports MethodBase = System.Reflection.MethodBase

Public Class TcpServerSocket
    Private Class SocketState
        Public ListenerSocket As TcpListener
        Public ClientSocket As TcpClient
        Public ReceivedBytes(2048) As Byte
    End Class

    Public Event Connected(ByVal remoteIP As String, _
                           ByVal remotePort As Integer)
    Public Event Disconnected(ByVal remoteIP As String, _
                              ByVal remotePort As Integer)
    Public Event ErrorOccurred(ByVal clientIP As String, _
                               ByVal remotePort As Integer, _
                               ByVal errorMessage As String)
    Public Event DataReceived(ByVal clientIP As String, _
                              ByVal remotePort As Integer, _
                              ByRef dataByte() As Byte)
    Public Event SendComplete(ByVal clientIP As String, _
                              ByVal remotePort As Integer, _
                              ByVal bytesSent As Integer)

    Dim m_listenerSocket As TcpListener
    Dim m_listenEndPoint As IPEndPoint
    Dim m_clientSocketList As New List(Of SocketState)

    Dim m_endAcceptTcpClientCallback As New AsyncCallback(AddressOf EndAcceptTcpClientCallback)
    Dim m_endReceiveCallback As New AsyncCallback(AddressOf EndReceiveCallback)
    Dim m_endSendCallback As New AsyncCallback(AddressOf EndSendCallback)

    Dim m_findClientIP As String
    Dim m_findClientPort As Integer

    Dim m_lockString As String = "LockMe"
    Dim m_isListening As Boolean = False

    Public Sub GetLocalIP(ByRef localIPs() As String)
        Dim ipCount As Integer
        Dim i As Integer

        ipCount = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName).AddressList.Length

        localIPs = New String(ipCount - 1) {}

        i = 0
        For Each ip As Net.IPAddress In System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName).AddressList
            localIPs(i) = ip.ToString
            i += 1
        Next
    End Sub

    Shared Function GetEndPointIP(ByVal endPointx As EndPoint) As String
        Return CType(endPointx, IPEndPoint).Address.ToString
    End Function

    Shared Function GetEndPointPort(ByVal endPointx As EndPoint) As Integer
        Return CType(endPointx, IPEndPoint).Port
    End Function

    Private Sub EndReceiveCallback(ByVal ar As IAsyncResult)
        Dim procedureName As String = MethodBase.GetCurrentMethod.Name

        If m_isListening Then
            Dim receivedDataLength As Integer
            Dim clientIP As String = ""
            Dim clientPort As Integer

            Try
                Dim ss As SocketState = CType(ar.AsyncState, SocketState)

                If ss.ClientSocket.Client.Connected Then
                    clientIP = GetEndPointIP(ss.ClientSocket.Client.RemoteEndPoint)
                    clientPort = GetEndPointPort(ss.ClientSocket.Client.RemoteEndPoint)

                    receivedDataLength = ss.ClientSocket.Client.EndReceive(ar)
                    If receivedDataLength > 0 Then
                        Dim buf As Byte() = New Byte(receivedDataLength - 1) {}
                        Array.Copy(ss.ReceivedBytes, 0, buf, 0, receivedDataLength)

                        Try
                            RaiseEvent DataReceived(clientIP, clientPort, buf)
                            ss.ClientSocket.Client.BeginReceive(ss.ReceivedBytes, 0, ss.ReceivedBytes.Length, SocketFlags.None, m_endReceiveCallback, ss)
                        Catch ex As Exception
                            RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": " & ex.Message)
                        End Try
                    Else
                        'No incoming data, the cleint Socket maybe close
                        RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": Abort Receive Data")
                        CloseClientSocket(clientIP, clientPort)
                    End If

                Else
                    'the cleint Socket maybe close
                    RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": Abort Receive Data")
                    CloseClientSocket(clientIP, clientPort)
                End If
            Catch e As SocketException
                If e.ErrorCode = 10053 Then
                    'The client close the connection
                    RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": Client may close the connection. " & e.Message)
                    CloseClientSocket(clientIP, clientPort)
                Else
                    RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": Error Code: " & e.ErrorCode & " " & e.Message)
                End If
            Catch e As Exception
                RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": " & e.Message)
            End Try
        Else
            'Listener already stops listening,
            'Do nothing here
        End If
    End Sub

    Private Sub EndAcceptTcpClientCallback(ByVal ar As IAsyncResult)
        Dim procedureName As String = MethodBase.GetCurrentMethod.Name

        If m_isListening Then
            Dim ss As New SocketState
            Dim clientIP As String = ""
            Dim clientPort As Integer

            Try
                ' Get the listener that handles the client request.
                ss.ListenerSocket = CType(ar.AsyncState, TcpListener)

                ' Get the socket to communicate with client
                ss.ClientSocket = ss.ListenerSocket.EndAcceptTcpClient(ar)
                m_clientSocketList.Add(ss)

                clientIP = GetEndPointIP(ss.ClientSocket.Client.RemoteEndPoint)
                clientPort = GetEndPointPort(ss.ClientSocket.Client.RemoteEndPoint)

                RaiseEvent Connected(clientIP, clientPort)

                'Start to read incoming data
                Try
                    ss.ClientSocket.Client.BeginReceive(ss.ReceivedBytes, 0, ss.ReceivedBytes.Length, SocketFlags.None, m_endReceiveCallback, ss)
                Catch e As SocketException
                    RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": " & e.Message)
                Catch e As Exception
                    RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": " & e.Message)
                End Try

                'Start to listen for next client request
                Try
                    m_listenerSocket.BeginAcceptTcpClient(m_endAcceptTcpClientCallback, m_listenerSocket)
                Catch ex As Exception
                    RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": " & ex.Message)
                End Try
            Catch e As SocketException

                If ss.ListenerSocket.Server.Connected = False Then
                    RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": Abort client request")
                Else
                    RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": " & e.Message)
                End If
            Catch e As Exception
                If ss.ListenerSocket.Server.Connected = False Then
                    RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": Abort client request")
                Else
                    RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": " & e.Message)
                End If

            End Try
        Else
            'Listener already stops listening,
            'Do nothing here
        End If

    End Sub

    ''' <summary>
    ''' Start to listen for the request of client socket
    ''' </summary>
    ''' <param name="ipString">The local IP address to be bound</param>
    ''' <param name="port">The port number to listen</param>
    ''' <remarks></remarks>
    Public Sub StartListen(ByVal ipString As String, _
                           ByVal port As Integer)
        Dim ip As IPAddress = Nothing

        m_isListening = False
        If IPAddress.TryParse(ipString, ip) = True Then
            Debug.Print(ip.ToString)

            'Set the locaal address and port to be bound
            m_listenEndPoint = New IPEndPoint(ip, port)

            Dim bResult As Boolean = False
            Try
                If m_listenerSocket IsNot Nothing Then

                    m_listenerSocket.Stop()
                End If
                m_listenerSocket = New TcpListener(m_listenEndPoint)

                m_listenerSocket.Start()
                m_listenerSocket.BeginAcceptTcpClient(m_endAcceptTcpClientCallback, m_listenerSocket)
                m_isListening = True
            Catch ex As Exception
                Throw ex
            End Try
        Else
            Throw New Exception("Invalid IP format")
        End If

    End Sub

    Private Function RemoveAllClientSocketStateList(ByVal ss As SocketState) As Boolean
        ss.ClientSocket.Client.Disconnect(False)
        ss.ClientSocket.Client.Close()
        Return True
    End Function

    Public Sub StopListen()
        Try
            If m_listenerSocket IsNot Nothing Then
                SyncLock m_lockString
                    m_clientSocketList.RemoveAll(AddressOf RemoveAllClientSocketStateList)

                    m_listenerSocket.Stop()
                    m_listenerSocket.Server.Close()
                    m_isListening = False
                End SyncLock
            End If
        Catch ex As Exception
            Throw ex
        End Try
    End Sub


    Private Sub EndSendCallback(ByVal ar As IAsyncResult)
        Dim procedureName As String = MethodBase.GetCurrentMethod.Name

        If m_isListening Then
            Dim bytesSent As Integer
            Dim clientIP As String = ""
            Dim clientPort As Integer

            Try
                Dim ss As SocketState = CType(ar.AsyncState, SocketState)

                bytesSent = ss.ClientSocket.Client.EndSend(ar)
                clientIP = GetEndPointIP(ss.ClientSocket.Client.RemoteEndPoint)
                clientPort = GetEndPointPort(ss.ClientSocket.Client.RemoteEndPoint)

                RaiseEvent SendComplete(clientIP, clientPort, bytesSent)
            Catch ex As Exception
                'connection may be closed, remove it from client list
                m_findClientIP = clientIP
                m_clientSocketList.RemoveAll(AddressOf RemoveSpecifiedClientSocketStateList)
                RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": " & ex.Message)
            End Try
        Else
            'Listener already stops listening,
            'Do nothing here
        End If
    End Sub

    Private Function FindSpecifiedClientSocketStateList(ByVal ss As SocketState) As Boolean
        Dim ip As String
        Dim port As Integer

        ip = GetEndPointIP(ss.ClientSocket.Client.RemoteEndPoint)
        port = GetEndPointPort(ss.ClientSocket.Client.RemoteEndPoint)

        If ip = m_findClientIP And (m_findClientPort = 0 Or port = m_findClientPort) Then
            Return True
        Else
            Return False
        End If
    End Function

    Private Function RemoveSpecifiedClientSocketStateList(ByVal ss As SocketState) As Boolean
        Dim clientIP As String = GetEndPointIP(ss.ClientSocket.Client.RemoteEndPoint)
        Dim clientPort As Integer = GetEndPointPort(ss.ClientSocket.Client.RemoteEndPoint)

        If clientIP = m_findClientIP And (m_findClientPort = 0 Or clientPort = m_findClientPort) Then
            ss.ClientSocket.Client.Disconnect(False)
            ss.ClientSocket.Client.Close()
            Return True
        Else
            Return False
        End If

    End Function

    Private Sub CloseClientSocket(ByVal clientIP As String, _
                                  ByVal clientPort As Integer)
        Dim procedureName As String = MethodBase.GetCurrentMethod.Name
        Dim match As Integer

        m_findClientIP = clientIP
        m_findClientPort = clientPort
        match = m_clientSocketList.RemoveAll(AddressOf RemoveSpecifiedClientSocketStateList)
        Try
            If match > 0 Then
                RaiseEvent Disconnected(clientIP, clientPort)
            Else
                'the closed client is not in list,
                'do nothing here
            End If
        Catch ex As Exception
            RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": " & ex.Message)
        End Try
    End Sub

    Private Sub CloseClientSocket(ByVal clientIP As String)
        CloseClientSocket(clientIP, 0)
    End Sub

    Public Sub Send(ByVal clientIP As String, _
                    ByVal clientPort As Integer, _
                    ByVal dataByte() As Byte)
        Dim procedureName As String = MethodBase.GetCurrentMethod.Name

        Try
            m_findClientIP = clientIP
            m_findClientPort = clientPort
            Dim ssList As List(Of SocketState) = m_clientSocketList.FindAll(AddressOf FindSpecifiedClientSocketStateList)

            If ssList.Count > 0 Then
                Dim ss As SocketState

                For Each ss In ssList
                    ss.ClientSocket.Client.BeginSend(dataByte, 0, dataByte.Length, SocketFlags.None, m_endSendCallback, ss)
                Next
            Else
                Throw New Exception(String.Format("The selected client IP {0} is not connected.", clientIP))
            End If

        Catch e As SocketException
            If e.ErrorCode = 10053 Then
                CloseClientSocket(clientIP, clientPort)
            Else
                RaiseEvent ErrorOccurred(clientIP, clientPort, procedureName & ": Unknown socket exception: " & e.Message)
            End If
            Throw e
        Catch ex As Exception
            'connection may be closed
            CloseClientSocket(clientIP, clientPort)
            Throw ex
        End Try
    End Sub

    Public Sub Send(ByVal clientIP As String, _
                    ByVal dataByte() As Byte)
        Send(clientIP, 0, dataByte)
    End Sub

    Public Sub Close()
        StopListen()
    End Sub

    Protected Overrides Sub Finalize()
        MyBase.Finalize()
    End Sub


End Class
