Asynchronous socket operations and threadpool

M

Michael D. Ober

Chris,

I have replaced the listening/accept code with the following:

Private ConnectionCounter As Long = 0
Private tcpServer As TcpListener

Public Sub CreateListener()
Dim ServerAddress As IPAddress =
Dns.GetHostEntry(My.Computer.Name).AddressList(0)
Dim LocalHost As New IPEndPoint(ServerAddress,
OSInterface.iniWrapper.ReadInt("Dialer", "Port", "Wakefield.ini"))
tcpServer = New TcpListener(LocalHost)
tcpServer.Start()
WriteLog("Ready for IP Connections")
tcpServer.BeginAcceptSocket(AddressOf AcceptRequest, tcpServer)
Debug.Print("Waiting for a connection")
End Sub

Private Sub AcceptRequest(ByVal ar As System.IAsyncResult)
Dim sock As Socket = Nothing
Dim ClientEndPoint As New IPEndPoint(0, 0)
Dim ClientName As String = ""
Dim MsgIn As String = ""
Dim BytesIn(1024) As Byte
Dim i As Integer

Try
Debug.Print("Incoming Connection")
Dim Listener As TcpListener = CType(ar.AsyncState, TcpListener)
sock = Listener.EndAcceptSocket(ar)
' Start listening for the next connection
tcpServer.BeginAcceptSocket(AddressOf AcceptRequest, tcpServer)

ClientEndPoint = CType(sock.RemoteEndPoint, IPEndPoint)
ClientName = ClientEndPoint.Address.ToString
ClientName = Dns.GetHostEntry(ClientName).HostName
ClientName &= ":" & ClientEndPoint.Port.ToString

Interlocked.Increment(ConnectionCounter)
UpdateCaption("Client Connection", ClientName)

' If the socket remains unused for 5 minutes, error out and release
server resources
If Not ClientName.Contains("Lily_Tomlin") Then sock.ReceiveTimeout =
5 * 60 * 1000
WriteLog(ClientName & ": Socket Timeout is set to " &
sock.ReceiveTimeout.ToString("#,##0") & " milliseconds")

I left the do loop in as this provides the state information for bookeeping
(ClientName) and de-blocking the TCP stream (MsgIn). This change eliminates
the need to have CreateListener started in a seperate thread and also the
need for the AutoResetEvent object. I understand your comments (I think)
about using the BeginRead/ReadComplete callback methods, but the number of
clients I have connected at any given time is less than 100, so scaleability
isn't an issue. I need to keep a small amount of state information around
between socket reads and the do loop allows me to do that easily, since each
callback to the AcceptRequest starts a new thread with its own stack and
thread local data.

Mike.
 
W

William Stacey [MVP]

There is really no advantage to having the Accept logic be async. You may a
well have your listener on its own single thread, then kick off the
BeginRead and loop again on accept. This also gives you a nice single point
to check for server shutdown/pause before doing another blocking accept.

--
William Stacey [MVP]

|I think I see the problem:
|
| Your algorithm is:
| 1 - You create a TcpListener, and call BeginAccept
| 2 - Inside the BeginAccept Callback, you call EndAccept, and then call
| Receive in an endless loop.
 
C

Chris Mullins

Michael D. Ober said:
I have replaced the listening/accept code with the following:

The big question now is, did it work? :)

It looked as if that would properly handle incoming connections now without
stalling out, which I believe was your original problem...
 
M

Michael D. Ober

I've released the changes and it appears to be working. One additional
benefit is the reduction of the number of threads in the application by 1.
BeginListener now runs inline with the main thread. I'll keep you posted -
it may take a day or two.

Mike.
 
C

Chris Mullins

There is really no advantage to having the Accept logic be async.
You may a well have your listener on its own single thread, then
kick off the BeginRead and loop again on accept.

I agree, you could do it either way - and the logic is a bit simpler without
the Async Accept. I think for me, it's more habit than anything else.

The only real difference is that doing Accept async means you don't need to
manage the life of a thread manually. Granted, this is a very small win, but
is nice. It's one less thing to worry about on App Shutdown - and your app
will be that much less likley to leave 'ghost' processes around.
 
M

Michael D. Ober

Chris and William,

The code works and is stable. Async Accepts are easier for me to handle
than synchronous Accepts because then each of the client connections is
handled by a single thread and I can keep the state information in local
variables. I can also let the framework manage the thread pool. If the
client thread itself stalls on the socket read statement, that's fine since
this means that there is no new data to process.

Two major benefits of the new code over my original code. First,
CreateListener can be called in-line and not as an additional thread.
Second, I got rid of a synchronization object and simplified the restart of
the listener.

For reference, here's my code with error handling:

Option Compare Text
Option Strict On
Option Explicit On

Imports System.Net.Sockets
Imports System.Net
Imports System.Threading
Imports System.Text.ASCIIEncoding

Module IPMessageHandler
Private ConnectionCounter As Long = 0
Private tcpServer As TcpListener

Public Sub CreateListener()
Dim ServerAddress As IPAddress =
Dns.GetHostEntry(My.Computer.Name).AddressList(0)
Dim LocalHost As New IPEndPoint(ServerAddress,
OSInterface.iniWrapper.ReadInt("Dialer", "Port", "Wakefield.ini"))
tcpServer = New TcpListener(LocalHost)
tcpServer.Start()
WriteLog("Ready for IP Connections")
tcpServer.BeginAcceptSocket(AddressOf AcceptRequest, tcpServer)
End Sub

Private Sub AcceptRequest(ByVal ar As System.IAsyncResult)
Dim sock As Socket = Nothing
Dim ClientEndPoint As New IPEndPoint(0, 0)
Dim ClientName As String = ""
Dim MsgIn As String = ""
Dim i As Integer
Dim ListenerRestart As Boolean = False

Try
Debug.Print("Incoming Connection")
Dim Listener As TcpListener = CType(ar.AsyncState, TcpListener)
sock = Listener.EndAcceptSocket(ar)

' Start listening for the next connection
tcpServer.BeginAcceptSocket(AddressOf AcceptRequest, tcpServer)
ListenerRestart = True

' Do some bookkeeping
ClientEndPoint = CType(sock.RemoteEndPoint, IPEndPoint)
ClientName = ClientEndPoint.Address.ToString
ClientName = Dns.GetHostEntry(ClientName).HostName
ClientName &= ":" & ClientEndPoint.Port.ToString
Interlocked.Increment(ConnectionCounter)
UpdateCaption("Client Connection", ClientName)

' Detect "dead" clients
' If the socket remains unused for 5 minutes, error out and
release server resources
If Not ClientName.Contains("Lily_Tomlin") Then
sock.ReceiveTimeout = 5 * 60 * 1000
WriteLog(ClientName & ": Socket Timeout is set to " &
sock.ReceiveTimeout.ToString("#,##0") & " milliseconds")

' Enter the Read/Eval/Print loop
Do
Dim BytesIn(1024) As Byte
Dim BytesReceived As Integer = sock.Receive(BytesIn)
Select Case BytesReceived
Case 0
WriteLog(ClientName & ": Client closed
connection")
Exit Do

Case Else
MsgIn &= ASCII.GetString(BytesIn, 0, BytesReceived)
i = InStr(MsgIn, BEL)
Do While i > 0
Dim msg As String = Left$(MsgIn, i - 1)
MsgIn = Mid$(MsgIn, i + 1)
' Process msg
WriteLog(ClientName & " => " & msg)
Dim msgOut As String = ProcessMessage(msg)
If msgOut <> "" Then
Dim BytesOut() As Byte =
ASCII.GetBytes(msgOut & BEL)
sock.Send(BytesOut)
WriteLog(ClientName & " <= " & msgOut)
End If
i = InStr(MsgIn, BEL)
Loop
End Select
Loop

Catch ex As Exception
WriteLog(ex.Message)

Finally
' Catch errors occurring prior to restarting the sckServer
listening
If Not ListenerRestart Then
tcpServer.BeginAcceptSocket(AddressOf AcceptRequest, tcpServer)

' Ensure the client socket is closed; we don't care about
flushing pending outbound bytes so sock.ShutDown() isn't required
sock.Close()

' Bookkeeping stuff
Interlocked.Decrement(ConnectionCounter)
UpdateCaption("Socket Closed", ClientName)
End Try
End Sub

Private Sub UpdateCaption(ByVal msg As String, ByVal Client As String)
Dim MsgConnections As String
Dim Connections As Long = Interlocked.Read(ConnectionCounter)
Console.Title = Connections.ToString("#,##0") & ": " & AppName()
Select Case ConnectionCounter
Case 0 : MsgConnections = "No Connections"
Case 1 : MsgConnections = "1 Connection"
Case Else : MsgConnections = Connections.ToString("#,##0") & "
Connections"
End Select
WriteLog(msg & ": " & Client.ToString & ": " & MsgConnections)
End Sub

Private Function ProcessMessage(ByVal msg As String) As String
Dim msgReturn As String = ""
Dim i As Integer

' Application Specific message processing

Return msgReturn
End Function
End Module

Mike.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top