Introduction
For a different project, I was looking for a TCP server example that I could use as a good starting point. I poked around the various TCP server articles here on The Code Project and didn't find anything that was simple enough! The articles I came across either required deriving specialized classes to handle the connection, were entangled with command processing, or were entangled with thread pool management.
What I wanted was a completely stand-alone, fire an event when a connection is established, TCP server. The event mechanism appeals to me because I can use the multicast delegate not only for processing the connection, but also to hook up monitoring/diagnostic handlers. There are, of course, issues that I don't discuss in this article, because that's not the point. Beginners are advised to read further on:
- Managing
TcpClient
connections - Managed thread pools
- Worker threads
- TCP communication
However, the point is that with this server you can decide how your server manages the client connections rather than having some scheme foisted on you.
The Simplest TCP Server
So, here's the simplest TCP server. You can construct it by passing in:
- An
IPEndPoint
- A port; the server will listen to
IPAddress.Any
for that port - An IP4 or IP6 address and a port
The Connected Event
The server application needs to hook one event, Connected
, which is fired when a client connection is established. If there are no event handlers at the time the client connection is established, the TcpServer
will immediately close the client connection. This event passes a ConnectionState
instance, which is a simple wrapper for the Socket
instance and the TcpServer
instance:
using System;
using System.Net;
using System.Net.Sockets;
namespace Clifton.TcpLib
{
public class ConnectionState
{
protected Socket connection;
protected TcpServer server;
public TcpServer Server
{
get
{
if (server == null)
{
throw new TcpLibException("Connection is closed.");
}
return server;
}
}
public Socket Connection
{
get
{
if (server == null)
{
throw new TcpLibException("Connection is closed.");
}
return connection;
}
}
public ConnectionState(Socket connection, TcpServer server)
{
this.connection = connection;
this.server = server;
}
public void Close()
{
if (server == null)
{
throw new TcpLibException("Connection already is closed.");
}
connection.Shutdown(SocketShutdown.Both);
connection.Close();
connection = null;
server = null;
}
}
}
}
The ConnectionState
instance can be used by your communication code throughout the lifetime of the connection. Ideally, you would want to use the Close
method of ConnectionState
, as this also clears the internal fields so that you can't accidentally use the ConnectionState
instance after the connection has been closed.
The HandleApplicationException Event
Optionally, you can also hook the HandleApplicationException
event, which is useful when developing your application and detecting exceptions that you inadvertently forgot to catch yourself.
The TcpServer Class
The TcpServer
provides two methods:
StartListening
StopListening
StartListening
is used to begin listening to the IP address and port. This method is non-blocking, meaning that the method returns immediately rather than waiting for a connection to be established. Therefore it is the responsibility of the application to stay alive to receive connections.
The StopListening
method is used to stop listening to the IP address and port. This method locks on the TcpServer
instance so that it doesn't stop listening in the middle of accepting a new connection. The code for the connection acceptance also locks on the TcpServer
instance. Two virtual methods implement the OnConnected
and OnHandleApplicationException
events calls, which is the standard .NET approach for calling events. Here's the code:
using System;
using System.Net;
using System.Net.Sockets;
namespace Clifton.TcpLib
{
public class TcpServer
{
public delegate void TcpServerEventDlgt(object sender,
TcpServerEventArgs e);
public delegate void ApplicationExceptionDlgt(object sender,
TcpLibApplicationExceptionEventArgs e);
public event TcpServerEventDlgt Connected;
public event ApplicationExceptionDlgt HandleApplicationException;
protected IPEndPoint endPoint;
protected Socket listener;
protected int pendingConnectionQueueSize;
public int PendingConnectionQueueSize
{
get { return pendingConnectionQueueSize; }
set
{
if (listener != null)
{
throw new TcpLibException("Listener has already started.
Changing the pending queue size is not allowed.");
}
pendingConnectionQueueSize = value;
}
}
public Socket Listener
{
get { return listener; }
}
public IPEndPoint EndPoint
{
get { return endPoint; }
set
{
if (listener != null)
{
throw new TcpLibException("Listener has already
started. Changing the endpoint is not allowed.");
}
endPoint = value;
}
}
public TcpServer()
{
pendingConnectionQueueSize = 100;
}
public TcpServer(IPEndPoint endpoint)
{
this.endPoint = endpoint;
pendingConnectionQueueSize = 100;
}
public TcpServer(int port)
{
endPoint = new IPEndPoint(IPAddress.Any, port);
pendingConnectionQueueSize = 100;
}
public TcpServer(string address, int port)
{
endPoint = new IPEndPoint(IPAddress.Parse(address), port);
pendingConnectionQueueSize = 100;
}
public void StartListening()
{
if (endPoint == null)
{
throw new TcpLibException("EndPoint not initialized.");
}
if (listener != null)
{
throw new TcpLibException("Already listening.");
}
listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
ProtocolType.Tcp);
listener.Bind(endPoint);
listener.Listen(pendingConnectionQueueSize);
listener.BeginAccept(AcceptConnection, null);
}
public void StopListening()
{
lock (this)
{
listener.Close();
listener = null;
}
}
protected void AcceptConnection(IAsyncResult res)
{
Socket connection;
lock (this)
{
connection = listener.EndAccept(res);
listener.BeginAccept(AcceptConnection, null);
}
if (Connected == null)
{
connection.Close();
}
else
{
ConnectionState cs = new ConnectionState(connection, this);
OnConnected(new TcpServerEventArgs(cs));
}
}
protected virtual void OnConnected(TcpServerEventArgs e)
{
if (Connected != null)
{
try
{
Connected(this, e);
}
catch (Exception ex)
{
e.ConnectionState.Close();
TcpLibApplicationExceptionEventArgs appErr =
new TcpLibApplicationExceptionEventArgs(ex);
try
{
OnHandleApplicationException(appErr);
}
catch (Exception ex2)
{
System.Diagnostics.Trace.WriteLine(ex2.Message);
}
}
}
}
}
}
So, that's it. Granted, it isn't that simple because of the non-blocking listener and the exception handling, which really adds robustness to the server.
Example
A simple TcpServer
example requires only a few lines of code:
using System;
using System.Collections.Generic;
using System.Text;
using Clifton.TcpLib;
namespace TcpServerDemo
{
class Program
{
static void Main(string[] args)
{
TcpServer tcpServer = new TcpServer("127.0.0.1", 14000);
tcpServer.Connected += new TcpServer.TcpServerEventDlgt(OnConnected);
tcpServer.StartListening();
Console.WriteLine("Press ENTER to exit the server.");
Console.ReadLine();
tcpServer.StopListening();
}
static void OnConnected(object sender, TcpServerEventArgs e)
{
Console.WriteLine("Connected.");
}
}
}
This code initializes the TcpServer
to listen to the localhost address on port number 14000. Any time a client connects, it writes out "Connected." to the console window. If you press Enter, it stops the listener and exits the demo. The client side is even simpler, but of course, all it does right now is create the connection and then close it! Of course, your program would do something more than this:
using System;
using System.Net.Sockets;
namespace TcpClientDemo
{
class Program
{
static void Main(string[] args)
{
TcpClient tcpClient = new TcpClient();
tcpClient.Connect("127.0.0.1", 14000);
tcpClient.Close();
}
}
}
Conclusion
I put this article in the Beginner section, as it is a core tutorial on TcpServer
, using events and handling exceptions. If anyone finds any flaws in this approach, let me know, as I don't want to lead beginners astray! As you will see in future articles, this simple TcpServer
will be used as a basis for working with a NetworkStream
, GZipStream
and CryptoStream
.
Additional Reading