#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2009 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Nuclex.Networking {
/// Listens for incoming TCP/IP connections on a specific port
///
///
/// This listener can handle moderate loads. It doesn't use the new support for
/// IO completion ports in .NET 2.0 SP1 for the listening socket, but the
/// user is free to use them for the actual transmissions on accepted connections.
///
///
/// Using the .NET asynchronous pattern, this listener will be able to accept
/// incoming connections until the thread pool reaches full capacity (which will
/// take quite a beating), at which point the listening socket's queue of pending
/// connections (backlog) will start to load up, meaning that unless another
/// method of limiting the number of allowed client connections is implemented,
/// a maximum of ThreadPool.GetMaxThreads() + Socket Backlog Size connections
/// can be made before the listener will begin rejecting incoming connections.
///
///
public class SocketListener : IDisposable {
/// Triggered whenever a client connects
public event EventHandler ClientConnected;
/// Initializes a new incoming TCP/IP connection listener
/// Port to listen on
public SocketListener(short port) {
setupSocket(port);
}
/// Shuts down the TCP/IP connection listener
public void Dispose() {
// This variable is mainly used by the acceptIncomingConnection() callback to
// distinguish whether it was called as an effect of a new incoming connection
// or due to the socket being shut down.
int wasShuttingDown = Interlocked.Exchange(ref this.shutdownState, 1);
// We can also use this to avoid double shut downs a little more efficiently :)
if(wasShuttingDown != 1) {
if(this.listeningSocket != null) {
this.listeningSocket.Close();
this.listeningSocket = null;
}
}
}
/// Begins listening for incoming TCP/IP connections
///
/// This method is provided so you have the chance to subscribe to the
/// event before incoming connection attempts
/// start being accepted by the listener.
///
public void StartListening() {
// Make sure we're still alive
if(this.shutdownState == 1) {
throw new ObjectDisposedException("Socket listener has already been disposed");
}
// If we're already listening, do nothing (redundant StartListening() calls
// are accepted by this library)
int wasListening = Interlocked.Exchange(ref this.listeningState, 1);
if(wasListening == 1) {
return;
}
// Bring the socket into a listening state. This is a seperate function since,
// after accepting a connection, this has to be repeated
internalStartListening();
}
///
/// Fires the ClientConnected event when a client connects to the listener
///
/// Socket of the client that has connected
protected virtual void OnClientConnected(Socket connectedClient) {
EventHandler copy = ClientConnected;
if(copy != null) {
copy(this, new SocketEventArgs(connectedClient));
}
}
/// Internal listening start method for use in the accept callback
///
/// This method is called once to initiate listening when the user calls
/// and then by the connection callback to resume
/// listening after an incoming connection is accepted.
///
private void internalStartListening() {
for(; ; ) {
// Asynchronously wait for the next client to connect to the socket. If there's
// already a client in the socket's pending connection queue, there's a chance
// that the call will complete synchronously and, thus, invoke the callback
// from this thread.
IAsyncResult asyncAcceptResult = this.listeningSocket.BeginAccept(
acceptIncomingConnectionDelegate, null
);
// If the callback didn't run synchronously, we can safely exit here. When the
// callback is made, it will be from a ThreadPool thread. We loop here otherwise
// to avoid calling BeginAccept() again from the callback, which theoretically
// could cause a stack overflow.
if(!asyncAcceptResult.CompletedSynchronously) {
break;
}
}
}
///
/// Callback that will be invoked by the socket when a client connects
///
/// Handle of the asynchronous call
private void acceptIncomingConnection(IAsyncResult asyncResult) {
// Take a copy of the listening socket so we don't run into a NullReferenceException
// when the socket is being destroyed at the same time a connection arrives
// (or the callback is invoked because the socket is being destroyed and the
// destroying thread was faster than us)
Socket listeningSocket = this.listeningSocket;
if((this.shutdownState == 1) || (this.listeningSocket == null)) {
return;
}
// The call was not tiggered by the socket shutting down, so we have
// a connection to accept!
Socket acceptedConnection = this.listeningSocket.EndAccept(asyncResult);
// Register and process the new client connection
OnClientConnected(acceptedConnection);
// If this callback was made from a ThreadPool thread, we can resume listening here.
// Otherwise, we're still in the thread that called BeginReceive() and will let
// that thread call BeginReceive() another time to avoid going deeper in the stack.
if(!asyncResult.CompletedSynchronously) {
internalStartListening();
}
}
///
/// Constructs the socket we're using to listen for incoming connections
///
/// Port the socket is to listen on
private void setupSocket(short port) {
this.acceptIncomingConnectionDelegate = new AsyncCallback(
acceptIncomingConnection
);
this.listeningSocket = new Socket(
AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp
);
try {
IPEndPoint localEndPoint = new IPEndPoint(
IPAddress.Parse("127.0.0.1"), port
);
this.listeningSocket.Bind(localEndPoint);
this.listeningSocket.Listen(-1);
}
catch(Exception) {
this.listeningSocket.Close();
this.listeningSocket = null;
throw;
}
}
/// Socket we're using to listen for incoming connection attempts
private volatile Socket listeningSocket;
/// Delegate for the acceptIncomingConnection() method
private AsyncCallback acceptIncomingConnectionDelegate;
/// Whether the socket receiver is currently shutting down
private int shutdownState;
/// Whether the socket receiver is currently listening
private int listeningState;
}
} // namespace Nuclex.Networking