From: Peter Duniho on
Michael Ober wrote:
> Peter,
>
> I think I found it this morning. It actually wasn't in the IPServer
> namespace at all.

Just as I suggested might be the case.

I'm glad you were able to find the problem. But I hope you can see,
based on this experience, why a concise-but-complete code example is so
important in terms of obtaining help. Without one, all anyone can do is
make random guesses and suggestions, none of which may have anything at
all to do with the problem.

While comments you received to your question might have been helpful in
a general sense, I doubt any of them were instrumental in leading you to
a solution to the specific problem you were asking about.

> [...]
> I think I corrected all but the _SendResults and _bufOut issues that you
> pointed out and I'm simply not sure how to fix that one, short of using
> SyncLock.

Nothing wrong with a SyncLock, IMHO. It's always most important to make
sure the code works right, then worry about whether there's a better,
more efficient solution. And .NET's Monitor class is reasonably efficient.

> [...]
> I also realized that creating a disconnect timer for each connection was
> extremely inefficient so I recoded this feature to use a single timer in
> the IPServer class and a decrement counter in each ClientInformation
> object.

If I were to code the timeout/disconnect logic from scratch, I might use
a sorted event queue, sorted by the event time and with a single thread,
and a call to Thread.Sleep() to delay until the next event. Add the
various logic to deal with resetting timers, adding new ones with a
future time earlier than the earliest current one in the queue, etc. and
you're done.

Fortunately, I believe that's basically what the System.Timers.Timer
timer does. It's specifically designed for server-related timers (or at
least, so the documentation says :) ), and I'm pretty sure one of its
main features is the ability to track a large number of timed events as
timers, without putting a significant load on the system.

Note that as compared to this approach:

> Since timeouts are always measured in minutes and the exact
> timeout period isn't critical, this change too hard to do. The
> decrement counter is set to the max inactivity time passed to the
> IPServer.New method and decremented once a minute by a timertick handler
> in the IPServer class itself. The Send and Receive methods of the
> ClientInformation class reset this counter to the time out value.

Advantage to your approach: reset of a timer is simply a new counter
value. Disadvantage to your approach: on every tick of the timer, you
have to inspect every connected client's data structure.

One downside to the sorted list of future events is the cost of the
insertion of a new element (either O(n) or O(log n), depending on the
implementation, which I don't know), borne every time you add a timer or
reset one. And of course, for large numbers of clients, this can be
large too.

Of course, if all the connections use the same timeout (which seems
likely), than any reset timer always winds up at the very end of the
sorted list, and the cost of doing that can be almost as cheap as
resetting a counter, because you already know where the new position is
in the list rather than having to search for it.

Obviously it depends somewhat on the specifics. But based on what I
think is likely about your implementation (specifically, every
connection uses the same timeout), you may find it preferable to
actually just create your own single-threaded event queue for the
timing, taking advantage of the fact that you know every new event added
(whether a new connection or one for which i/o operation happened and
thus needs a reset timer) goes to the end of the list.

I don't know for sure, but it's possible System.Timers.Timer includes an
optimization to check for end-of-list insertions, and so could in fact
perform basically as well as a custom implementation would. (I took a
quick look in Reflector, but unfortunately, the meat of the
implementation seems to be in native code, which doesn't show up in
Reflector; I can, however, verify that System.Timers.Timer just uses
System.Threading.Timer underneath, so whatever characteristics one has,
the other is pretty much the same :) ).

> Yes,
> there can be a race condition here, but if it occurs, one of two things
> will happen. Either the server will break down the connection, which
> still complies with the server's contract with its clients, or the
> connection timeout will be reset, which will simply keep the connection
> alive longer. In either case, if the client disconnects, the EndRecieve
> method of the socket will return 0 bytes and the connection will be
> shutdown by the server. [...]

Right. Timeouts always involve race conditions. The best you can do is
just make sure you have a way of knowing who won, and then get that part
right without corrupting your data. :)

Pete
From: Michael Ober on
Pete and all,

Here's the final code. I actually got this working yesterday but ran into
another problem with cross thread errors inside the TCPServer class itself.
After googling for the specific error message and finding multiple
references as well as a bug report to MS that was closed because MS couldn't
duplicate the bug, I realized that all the bug reports were from VB
developers and none from C# developers and that the bug first appeared in VB
2005 RTM. VB 2005 and 2008 have a "My" class that provides several
shortcuts into the framework, which are really useful to someone just
starting in VB simply because of the sheer size of the framework. Reading
the My class documentation, almost all these classes eventually inherit from
WinForms when used in a WinForms application. Cross thread operations
against WinForms has long been documented, even in the Win32 API, as a major
don't do. Now when I start maintainence on a program, I search the entire
solution for "My." and recode them to use standard framework classes. This
fixed the problem and also probably explains why MS couldn't recreate it -
they were testing using the framework only.

Once again, thanks to everyone, especially Pete, who spent the time to
provide feedback.

Mike.

'============================================
Option Compare Text
Option Strict On
Option Explicit On
Option Infer Off

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

Public MustInherit Class IPServer
Implements IDisposable

Public Event ConnectionList(ByVal sConnection As List(Of String))
Public MustOverride Function ProcessMessage(ByVal message As String) As
String

' My list of clients
Private Clients As New ClientList

Private _tcpServer As Socket = Nothing
Private ReadOnly _CommandTermination As String = BEL

' Timeout support; Asynchronous sockets don't directly support timers.
Protected ReadOnly _SocketTimeout As TimeSpan
Protected ReadOnly _timer As Timer = Nothing

Public Sub New(ByVal Port As Integer)
Me.New(Port, BEL, Nothing)
End Sub
Public Sub New(ByVal Port As Integer, ByVal SocketTimeout As TimeSpan)
Me.New(Port, BEL, SocketTimeout)
End Sub
Public Sub New(ByVal Port As Integer, ByVal CommandTermination As
String)
Me.New(Port, CommandTermination, Nothing)
End Sub
Public Sub New(ByVal Port As Integer, ByVal CommandTermination As
String, ByVal SocketTimeout As TimeSpan)
' save the termination and TTL before doing anything else to avoid
threading issues
_CommandTermination = CommandTermination
_SocketTimeout = SocketTimeout

Try
Dim myAddr As IPAddress =
Dns.GetHostEntry(Environment.MachineName).AddressList(0)
Dim sEndPoint As String = Environment.MachineName & "(" &
myAddr.ToString() & "):" & Port.ToString()
WriteLog("Configure Listener on " & sEndPoint)

_tcpServer = New Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp)
Dim LocalEP As IPEndPoint = New IPEndPoint(myAddr, Port)
_tcpServer.Bind(LocalEP)
_tcpServer.Listen(5) ' 5 is the standard backlog value

_tcpServer.BeginAccept(AddressOf OnAccept, Nothing)
WriteLog("Listening for connections on " & sEndPoint)

If _SocketTimeout <> Nothing Then
Dim Interval As New TimeSpan(0, 1, 0)
_timer = New Threading.Timer(AddressOf TimerTick, Nothing,
Interval, Interval)
WriteLog("Socket Timer Started")
End If
Catch ex As Exception
WriteLog(ex)
SMTPMail.SendMessage("mis", AppName() & ": Unable to create
Listener", ex.Message())
Throw ex
End Try
End Sub

'Handle connection requests
Private Sub OnAccept(ByVal ar As System.IAsyncResult)
Dim ci As New ConnectionInformation(Me, _SocketTimeout)
Try
ci.sock = _tcpServer.EndAccept(ar)

' Start listening for the next connection
_tcpServer.BeginAccept(AddressOf OnAccept, Nothing)

' Wait for data
ci.BeginReceive(AddressOf OnReceive)

Catch ex As Exception
WriteLog(ci.ToString(), ex)
Finally
WriteLog(Clients.ToString(), True)
RaiseEvent ConnectionList(Clients.ToList())
End Try
End Sub

Private Sub OnReceive(ByVal ar As IAsyncResult)
Dim ci As ConnectionInformation = CType(ar.AsyncState,
ConnectionInformation)
Try
Dim bytesReceived As Integer = ci.EndReceive(ar)

Select Case bytesReceived
Case 0
ci.Close("Client Closed Connection")

Case Else
Dim i As Integer = InStr(ci.MessageIn,
_CommandTermination)
Do While i > 0
' Process msg
Dim msg As String = Left$(ci.MessageIn, i - 1)
WriteLog(ci.ToString() & " => " & msg)
Dim msgOut As String = ProcessMessage(msg)
If msgOut <> "" Then
ci.Send(msgOut & _CommandTermination)
WriteLog(ci.ToString() & " <= " & msgOut)
End If

ci.MessageIn = Mid$(ci.MessageIn, i +
_CommandTermination.Length)
i = InStr(ci.MessageIn, _CommandTermination)
Loop

' Finally, read the next chunk of data
ci.BeginReceive(AddressOf OnReceive)
End Select

Catch ex As Exception
WriteLog(ex)
Finally
WriteLog(Clients.ToString())
RaiseEvent ConnectionList(Clients.ToList())
End Try
End Sub

Private Sub TimerTick(ByVal o As Object)
' loop by descending index to ensure no connections are missed
' Cannot use a for each loop as ConnectionInformation.Close()
removes the connection from the clients list
For i As Integer = Clients.Count - 1 To 0 Step -1
Dim ci As ConnectionInformation = Clients(i)
Threading.Interlocked.Decrement(ci.Activity)
If ci.Activity <= 0 Then ci.Close("Socket Inactivity Timeout")
Next
RaiseEvent ConnectionList(Clients.ToList())
End Sub

Public Sub Close()
WriteLog("Server Shutdown Starting")

' Close the listener
WriteLog("Listener being closed")
If _tcpServer IsNot Nothing Then
_tcpServer.Close()
_tcpServer = Nothing
End If

' Stop the socket timer
If _timer IsNot Nothing Then _timer.Dispose()

' Stop the clients; Don't use "for each" as closing a connection
removes it from clients
' Closing a client connection has the side effect of removing the
connection from the clients collection
WriteLog("Closing Clients: " & Clients.ToString())
Do While Clients.Count > 0
Clients(0).Close("Server Shutting Down")
Loop
End Sub

#Region " IDisposable Support "
' This code added by Visual Basic to correctly implement the disposable
pattern.

Protected Overridable Sub Dispose(ByVal disposing As Boolean)
Close()
End Sub

Public Sub Dispose() Implements IDisposable.Dispose
' Do not change this code. Put cleanup code in Dispose(ByVal
disposing As Boolean) above.
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
#End Region

Public Class ConnectionInformation
Implements IDisposable

Public MessageIn As String = ""

' Who's talking to me?
Private _ClientName As String = ""
Private _ClientPort As Integer = 0

' Who am I
Private _server As IPServer = Nothing

' Private structures and storage for sending and receiving data
Private _sock As Socket = Nothing
Private Const _bufSize As Integer = 1500
Private _bufIn(_bufSize) As Byte

' These two variables could be the source of a possible, but never
seen, race condition
Private _SendResults As IAsyncResult = Nothing
Private _bufOut() As Byte

' TimeOut support; Async sockets don't directly support timeouts
Public Activity As Integer = 0
Private _Activity As Integer = 0

Public Sub New(ByVal server As IPServer, ByVal sock As Socket, ByVal
SocketTimeout As TimeSpan)
Me.New(server, SocketTimeout)
Me.sock = sock
End Sub
Public Sub New(ByVal server As IPServer, ByVal SocketTimeout As
TimeSpan)
_server = server
If SocketTimeout <> Nothing Then _Activity =
SocketTimeout.Minutes
Activity = _Activity
_server.Clients.Add(Me)
End Sub

Public Sub BeginReceive(ByVal callback As AsyncCallback)
_sock.BeginReceive(_bufIn, 0, _bufSize, SocketFlags.None,
callback, Me)
Activity = _Activity
End Sub
Public Function EndReceive(ByVal asyncResult As IAsyncResult) As
Integer
Dim ci As ConnectionInformation = CType(asyncResult.AsyncState,
ConnectionInformation)
Try
If ci._sock Is Nothing Then Return 0

ci.Activity = ci._Activity
Dim bytesReceived As Integer =
ci._sock.EndReceive(asyncResult)
ci.MessageIn &= ASCII.GetString(ci._bufIn, 0, bytesReceived)
Return bytesReceived

Catch exSock As SocketException
Dim sockErr As SocketError = exSock.SocketErrorCode
Dim msg As String = [Enum].GetName(GetType(SocketError),
sockErr)
WriteLog("Socket Exception: " & msg, exSock)
Return 0
Catch ex As Exception
WriteLog(ex)
Return 0
Finally
ci.Activity = ci._Activity
End Try

Return 0 ' Forces the socket to close elsewhere in code
End Function

Public Sub Send(ByVal MessageOut As String)
Try
Activity = _Activity
Do Until _SendResults Is Nothing OrElse _bufOut.Length = 0
_SendResults.AsyncWaitHandle.WaitOne()
Loop

_bufOut = System.Text.Encoding.ASCII.GetBytes(MessageOut)
_SendResults = _sock.BeginSend(_bufOut, 0, _bufOut.Length,
SocketFlags.None, AddressOf OnSend, Me)

Catch ex As Exception
WriteLog(ex)
Finally
Activity = _Activity
End Try
End Sub

Private Sub OnSend(ByVal ar As IAsyncResult)
Dim ci As ConnectionInformation = CType(ar.AsyncState,
ConnectionInformation)
ci.Activity = ci._Activity
Try
Dim bytesSent As Integer = ci._sock.EndSend(ar)

' Did we send the entire buffer?
If bytesSent >= _bufOut.Length Then
Array.Resize(_bufOut, 0)

Else
' No; reset the buffer to contain only the bytes needed
to be sent
Dim tBuf(_bufOut.Length - bytesSent - 1) As Byte
Array.Copy(_bufOut, bytesSent, tBuf, 0, tBuf.Length)
_bufOut = tBuf
' Send them
_SendResults = ci._sock.BeginSend(_bufOut, 0,
_bufOut.Length, SocketFlags.None, AddressOf OnSend, ci)
End If
ci.Activity = ci._Activity
Catch ex As Exception
WriteLog(ci.ToString() & vbNewLine & ex.Message, True)
Finally
ci.Activity = ci._Activity
End Try
End Sub

Public WriteOnly Property sock() As Socket
Set(ByVal value As Socket)
_sock = value

If _sock Is Nothing Then
_ClientName = ""
_ClientPort = 0
Activity = 0
Else
Dim ClientEndPoint As IPEndPoint =
CType(_sock.RemoteEndPoint, IPEndPoint)
_ClientName =
Dns.GetHostEntry(ClientEndPoint.Address.ToString()).HostName
_ClientPort = ClientEndPoint.Port
Activity = _Activity
End If
End Set
End Property

Public ReadOnly Property ClientName() As String
Get
Return _ClientName
End Get
End Property
Public ReadOnly Property ClientPort() As Integer
Get
Return _ClientPort
End Get
End Property

Public Shadows Function ToString() As String
Return _ClientName & ":" & _ClientPort.ToString()
End Function

Public ReadOnly Property ClientCount() As Integer
Get
Return _server.Clients.Count
End Get
End Property

Public Function Close(ByVal Reason As String) As Integer
WriteLog(RemoveSpaces(Reason & ": " & Me.ToString()))
If _sock IsNot Nothing Then
_sock.Shutdown(SocketShutdown.Both)
_sock.Close()
_sock = Nothing
End If
If _server.Clients.Contains(Me) Then _server.Clients.Remove(Me)
Return _server.Clients.Count
End Function

#Region " IDisposable Support "
' This code added by Visual Basic to correctly implement the
disposable pattern.
Protected Overridable Sub Dispose(ByVal disposing As Boolean)
Close("Server Disposing Connection")
End Sub

Public Sub Dispose() Implements IDisposable.Dispose
' Do not change this code. Put cleanup code in Dispose(ByVal
disposing As Boolean) above.
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
#End Region

End Class

Private Class ClientList
Inherits SynchronizedCollection(Of ConnectionInformation)

Public Function ToList() As List(Of String)
Dim sa As New List(Of String)
For Each ci As ConnectionInformation In MyBase.Items.ToArray()
sa.Add(ci.ToString() & "/" & ci.Activity.ToString("#,##0"))
Next
Return sa
End Function

Public Overloads Function ToString() As String
Dim sa As List(Of String) = Me.ToList()
Dim s As String = "Connections: " & sa.Count.ToString("#,##0")
For Each li As String In sa
s &= vbNewLine & li
Next
Return s
End Function
End Class
End Class


From: Peter Duniho on
Michael Ober wrote:
> [...]
> Once again, thanks to everyone, especially Pete, who spent the time to
> provide feedback.

I have more. :)

Here are some things that I noticed looking through the code you posted:

-- There's a fair amount of redundant code, particularly with respect
to the "Activity = _Activity" statement that appears a lot. Not that
big of a deal, but you'll wish you'd made the "Activity" member a
property when you see the next item.

-- You have a threading bug in the handling of the "Activity" field.
In particular, while you do use the Interlocked class to decrement the
value, none of the assignments are protected in any way. So you have no
guarantee that when you decrement the value, it's actually the correct,
most recent value. Instead, any line that reads "Activity = _Activity"
should read "Thread.VolatileWrite(Activity, _Activity)". This will
ensure volatile semantics for the assignment. (Had you made "Activity"
a property, you could've fixed it in one place, rather than having to go
visit each and every statement in the code where "Activity" is used).

-- It also appears to me that you have a numerical bug in the
initialization of the "_Activity" field. In particular, you are using
TimeSpan.Minutes, while I believe you really want TimeSpan.TotalMinutes
(truncated to an integer, of course). As the code is now, if someone
specifies a 90-minute timeout, you'll only wait 30 minutes before
expiring the connection.

-- You also have managed to incorrectly implement the ToString()
method, in two completely different ways. :) You should make the
method an "Overrides", but you have one place where you've specified
"Shadows", and another place where you've specified "Overloads".

-- In your ConnectionInformation.Close() method, there is no point in
calling Contains(). It is not an error to call Remove() with an element
not actually in the collection, and since half of the work of the
Remove() method is scanning the list to find the element, which is the
same work the Contains() method does, you effectively do that twice for
no benefit to the code.

-- You have left in the behavior I commented on before, with respect
to having the per-connection "_SendResults" member, and a per-connection
buffer. If done correctly, this would at worst be a potential
performance issue, but because there's no synchronization around either
the _SendResults member, nor the _bufOut member, you have a potential
data corruption bug. You should either fix the design so that you have
a per-i/o-operation data structure that isolates each operation from any
other operation, or synchronize the code where you use the shared member
variables.

On that last point, note that just adding synchronization is probably
fine. Generalizing the data structures so you have a per-i/o structure
would make the code more flexible, but then you'd have to worry about
keeping overlapped sends in the correct order, which is just one more
complication you'll then need to code for. :)

That's all I see for now. :)

Pete
From: Michael Ober on

"Peter Duniho" <no.peted.spam(a)no.nwlink.spam.com> wrote in message
news:OaFSgSEgKHA.4528(a)TK2MSFTNGP06.phx.gbl...
> Michael Ober wrote:
>> [...]
>> Once again, thanks to everyone, especially Pete, who spent the time to
>> provide feedback.
>
> I have more. :)
>
> Here are some things that I noticed looking through the code you posted:
>
> -- There's a fair amount of redundant code, particularly with respect to
> the "Activity = _Activity" statement that appears a lot. Not that big of
> a deal, but you'll wish you'd made the "Activity" member a property when
> you see the next item.
>
> -- You have a threading bug in the handling of the "Activity" field. In
> particular, while you do use the Interlocked class to decrement the value,
> none of the assignments are protected in any way. So you have no
> guarantee that when you decrement the value, it's actually the correct,
> most recent value. Instead, any line that reads "Activity = _Activity"
> should read "Thread.VolatileWrite(Activity, _Activity)". This will ensure
> volatile semantics for the assignment. (Had you made "Activity" a
> property, you could've fixed it in one place, rather than having to go
> visit each and every statement in the code where "Activity" is used).
>

The _Activity field is part of the timeout system. If it gets an
inconsistent value, I don't think it's an an issue. It will either be
decremented early, which will cause an early timeout, or reset to the
initial value late, which will keep an inactive client connected longer. I
had actually used Interlocked.Decrement originally in the TimerTick method,
but couldn't find an equivalent for resetting this value. I didn't know
about the VolatileWrite method. That said - here's the new code for setting
and resetting Activity. I'm using

Private _TimeRemaining As Integer = 0
Public Property Activity() As Integer
Get
Thread.VolitileRead(_TimeRemaining)
End Get
Set(ByVal value As Integer)
Thread.VolitileWrite(_TimeRemainaing, value)
End Set
End Property

C#'s volitile keyword would have made this easier. One question - how do
the VolitileRead and VolitileWrite methods interact with the
Interlocked.Decrement method used in the IPServer.TimerTick callback?

> -- It also appears to me that you have a numerical bug in the
> initialization of the "_Activity" field. In particular, you are using
> TimeSpan.Minutes, while I believe you really want TimeSpan.TotalMinutes
> (truncated to an integer, of course). As the code is now, if someone
> specifies a 90-minute timeout, you'll only wait 30 minutes before expiring
> the connection.
>

I hadn't even thought of this scenerio. The longest timeout I had used was
15 minutes. Fixed. Thanks.

> -- You also have managed to incorrectly implement the ToString() method,
> in two completely different ways. :) You should make the method an
> "Overrides", but you have one place where you've specified "Shadows", and
> another place where you've specified "Overloads".
>

This is unfortunately one of the things that VB allows. There is definitely
a weaknesses here in the compiler itself. Another one that I find
troublesome is that properties and methods can be referenced with or without
the (), unlike C# that strictly enforces this. Fixed.

> -- In your ConnectionInformation.Close() method, there is no point in
> calling Contains(). It is not an error to call Remove() with an element
> not actually in the collection, and since half of the work of the Remove()
> method is scanning the list to find the element, which is the same work
> the Contains() method does, you effectively do that twice for no benefit
> to the code.
>

Updated. I was expecting an exception if the object wasn't already in the
collection. RTM (Read The Manual).

> -- You have left in the behavior I commented on before, with respect to
> having the per-connection "_SendResults" member, and a per-connection
> buffer. If done correctly, this would at worst be a potential performance
> issue, but because there's no synchronization around either the
> _SendResults member, nor the _bufOut member, you have a potential data
> corruption bug. You should either fix the design so that you have a
> per-i/o-operation data structure that isolates each operation from any
> other operation, or synchronize the code where you use the shared member
> variables.
>
> On that last point, note that just adding synchronization is probably
> fine. Generalizing the data structures so you have a per-i/o structure
> would make the code more flexible, but then you'd have to worry about
> keeping overlapped sends in the correct order, which is just one more
> complication you'll then need to code for. :)
>

SyncLock _outBuf
' All operations on _SendResults and _outBuf in the method.
End SyncLock

Since I can't send the next message until the previous one completes, any
performance hit would already have been experienced simply by the wait loops
that are in the code.

> That's all I see for now. :)
>
> Pete

From: Peter Duniho on
Michael Ober wrote:
> The _Activity field is part of the timeout system. If it gets an
> inconsistent value, I don't think it's an an issue.

Without volatile semantics, in theory writes to the field might _never_
be visible to other threads.

On x86, I believe this would happen only due to compiler optimizations,
and given that it's a member field of a class, I'd guess that wouldn't
happen. But I prefer code that's provably correct, rather than
"probably won't break" in a specific environment. :)

> [...]
> C#'s volitile keyword would have made this easier. One question - how
> do the VolitileRead and VolitileWrite methods interact with the
> Interlocked.Decrement method used in the IPServer.TimerTick callback?

I think it should be fine.

You'll notice that Volatile...() methods only support data types that
can be handled atomically. The Interlocked methods all do atomic
operations as well.

If you'd rather stick only with the Interlocked class, there is the
Interlocked.Exchange() method which you can use to write to a variable,
and the Interlocked.Read() method. But my recollection is that you have
an atomicity guarantee for 32-bit values in .NET, so other than the race
conditions that, as you say, aren't of concern, I don't see a problem
mixing the APIs.

By the way, as I scan through the code again, I see at least one more
problem: you haven't synchronized the ClientInformation.Close() method.
You could get an ObjectDisposedException if two different threads try
to close the same object at the same time, as one reaches the
_sock.Close() before the other reaches the _sock.Shutdown(). (I don't
recall off the top of my head, but it's possible you can't even call
Shutdown() twice with the SocketShutdown.Both value...but for sure,
trying to call Shutdown() on a disposed/closed socket is not good).

You should definitely put a lock around the Socket shutdown/closure
code, so that only one caller to Close() ever finds the _sock field
non-null.

Related to this is the fact that the Close() method returns the new
client count. I didn't bother to look at how this return value is used,
but you should review that to make sure that having two different calls
to the Close() method returning the same value is okay, and that having
any given call to Close() return a value that is more than one less than
what the client count was before the Close() call is also okay (the
latter could happen if one client is removed in one thread, and another
is removed by a different thread at the same time).

Pete