Contents
- Introduction
- The demo application
- An introduction into sockets, connection-oriented protocols (TCP)
- LanTalk.Core – core functionality and shared objects
- The server application
- The client application
- Logging - NLog
- Conclusions
Sockets, sockets, sockets! Or was it developers, developers, developers? Sockets are used by nearly every program out there, so they deserve their own place on the stand. The birth of this article was caused by the need of a simple chat application that can be run in service mode (like, run once forget forever mode). There are a few good articles here on how to implement a service, using sockets, so I did my research. The resulted application will be dissected in the following pages.
So, grab yourself a coffee, it's going to be a long and, hopefully, interesting read.
This is a typical client-server application. Provided is a GUI server application, a server implemented as a service and a client application.
After the server starts, it accepts TCP connections on port 65534 (or by a port set by the user). Users connected to the server must authenticate, before any message exchange takes place. The possibility of public
and private
messaging is implemented, and a basic logging mechanism.
One may use the GUI server application or the service to accept incoming requests and to serve the clients. However, you should know that in this version, only the GUI server app can be used to create users.
2.1. Setting up the Demo Application
- Edit the Settings.xml file in the LanTalk.Server\Data directory.
- Run the
LanTalk.ServerGui
application and create some users. - Install the service by running “
LanTalk.Service install
”. - Start the server using
LanTalk.ServerGui
, or start the service by running “net start LanTalk
”. - Fire up the
LanTalk.Client
application. - Set the connection settings in the Application -> Application Settings menu.
- Log in to the server, and start talking.
An introduction into IP addresses and the TCP/IP model can be found here[^]. I will not delve into this.
The communication between the server and the client is implemented using the TCP protocol. TCP is a connection-oriented protocol, meaning that it provides reliable, in-order delivery of a stream of bytes. Using TCP applications, we can exchange data using stream sockets. A socket is a communication endpoint. For an endpoint to be uniquely identified, it needs to contain the following data: the protocol used to communicate, an IP address, and a port number.
In the .NET framework, a socket is represented by the Socket
class in the System.Net.Sockets
namespace. The Socket
class defines the following members to uniquely describe an endpoint:
For the Protocol in Use
public AddressFamily AddressFamily
{ get; }
| Specifies the addressing scheme that an instance of the Socket class can use. The two most common AddressFamily values used are InterNetwork for IPV4 addresses, and InterNetworkV6 for IPV6 addresses. |
public SocketType SocketType
{ get; }
| Gets the type of the socket. The SocketType indicates the type of communication that will take place over the socket, the two most common types being Stream (for connection-oriented sockets) and Dgram (for connectionless sockets). |
public ProtocolType ProtocolType
{ get; }
| Specifies the protocol type of the Socket . |
For the local/remote Endpoint
public EndPoint LocalEndPoint/RemoteEndPoint
{ get; }
| Specifies the local/remote endpoint. |
3.1. The EndPoint / IPEndPoint Class
It’s an abstract
class that identifies a network address. The IPEndPoint
class is derived from the EndPoint
class, and represents a network endpoint (a combination of an IP address and a port number).
public IPAddress Address
{ get; set; }
| Gets or sets the IP address of the endpoint. |
public int Port
{ get; set; }
| Gets or sets the port number of the endpoint. |
3.2. Steps to Create Connection-oriented Sockets on the Server Side
Before the server can accept any incoming connection, you must perform four steps on a server socket:
socket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 65534);
socket.Bind(endPoint);
socket.Listen(32);
socket.BeginAccept(new AsyncCallback(HandleIncommingClient), null);
- Create the socket. When a socket is created, it is given an address family and a protocol type, but it has no assigned address and port.
- Bind that socket to a local address and port.
Bind
assigns a socket an address and a port. - Prepare the bound socket to accept incoming connections. The
Listen
method sets up internal queues for a socket. Whenever a client attempts to connect, the connection request is placed in the queue. Pending connections from the queue are pulled with the Accept
method that returns a new Socket
instance. - Accept incoming connections on the socket.
3.3. Steps to Create Connection-oriented Sockets on the Client Side
To create a socket on the client side, you need to create a socket and wire up that socket to the remote address and port. That simple! To connect to the remote server, you have to use the Socket.Connect
method:
socket.Connect(serverEndPoint);
3.4. Blocking on Sockets
The Socket
class methods use blocking, by default, this is why when execution reaches a network call that blocks, the application waits until the operation completes (or an exception is thrown). To avoid blocking on sockets, we may choose to use non-blocking sockets, multiplexed sockets, or asynchronous sockets. There is a fourth way to use a socket, by handling connections with IOCP (IO completion ports).
3.4.1. Non-blocking Sockets
Setting the Socket.Blocking
property to false
can set your socket to function as non-blocking. When a socket is in non-blocking mode, it will not wait for a requested operation to complete; rather, it will check the operation, and if it can’t be completed, the method will fail and program execution will continue.
Disadvantages of non-blocking sockets: Although this allows the program to do other things while the network operations finish, it requires that the program repeatedly poll to find out when each request has finished.
3.4.2. Multiplexed Sockets
This technique is used to watch multiple Socket
instances for the ones that are ready to be read or written to. For this, the Socket.Select(…)
static
method is used. The format of the Select
method is:
public static void Select (
IList checkRead,
IList checkWrite,
IList checkError,
int microSeconds
);
Select
returns when at least one of the sockets of interest (the sockets in the checkRead
, checkWrite
, and checkError
lists) meets its specified criteria, or the microSeconds
parameter is exceeded, whichever comes first. Setting microSeconds
to -1
specifies an infinite time-out. This is a combination of non-blocking sockets. One way to use Select
is to create a timer that fires frequently, and within the timer's event handler, call Select
with a timeout value of zero so that the call is non-blocking.
For a detailed example, look at MSDN.
Disadvantages of multiplexed sockets: This is an inefficient method, because the time needed to serve the sockets depends on how frequently the timer is fired (the microSeconds
parameter). Also, the servicing of the sockets prevents the code from reentering the Select
method, which may cause starvation of threads.
3.4.3. Asynchronous Sockets
Asynchronous sockets use callback methods to handle incoming data and connections. By convention, an asynchronous socket method begins with the word “Begin
”, like BeginReceive
, BeginAccept
, etc. This method allows you to use a separate method when a socket operation is available (the delegate will be called). The asynchronous calls use the AsyncCallback
class to call completion methods when a socket operation is finished.
The Socket
class has the following asynchronous methods:
public IAsyncResult BeginAccept( … )
public IAsyncResult BeginConnect( … )
public IAsyncResult BeginReceive( … )
public IAsyncResult BeginSend( … )
Each of the Begin
methods starts the network function, and registers the AsyncCallback
method. Each of the Begin
methods has to have an End
method that completes the function when the AsyncCallback
method is called.
The ways of multi-threading in .NET are: starting your own threads with ThreadStart
delegates, and using the ThreadPool
class either directly (using ThreadPool.QueueUserWorkItem
), or indirectly by using asynchronous methods (such as Stream.BeginRead
, or calling BeginInvoke
on any delegate). Asynchronous I/O uses the application's thread pool, so when there is data to be processed, a thread from the thread pool performs the callback function. This way, a thread is used for a single I/O operation, and after that, it is returned to the pool.
3.4.5. IOCP Requests
IOCP represents a solution to the one-thread-per-socket problem. Completion ports are used in high performance servers, with a high number of concurrent connections. In this article, we are focusing on asynchronous sockets.
3.5. Closing Down the Socket
For closing the socket, the client application is advised to use the Shutdown
method. This allows all the unsent data to be sent and the un-received data to be received before the connection is closed:
if (socket.Connected) socket.Shutdown(SocketShutdown.Both);
socket.Close();
The SocketShutdown
enumeration defines what operation on the socket will be denied (Send
, Receive
, or Both
).
3.6. Exception Handling and Important Error Codes
If a socket method encounters an error, a SocketException
is thrown. The ErrorCode
member contains the error code associated with the exception thrown. This is the same error code that is received by calling the WSAGetLastError
of the WinSock
library.
Error Number | Winsock Value | SocketError Value | Meaning |
10004 | WSAEINTR | Interrupted | The system call was interrupted. This can happen when a socket call is in progress and the socket is closed. |
10048 | WSAEADDRINUSE | AddressAlreadyInUse | The address you are attempting to bind to or listen on is already in use. |
10053 | WSACONNABORTED | ConnectionAborted | The connection was aborted locally. |
10054 | WSAECONNRESET | ConnectionReset | The connection was reset by the other end. |
10061 | WSAECONNREFUSED | ConnectionRefused | The connection was refused by the remote host. This can happen if the remote host is not running, or if it is unable to accept a connection because it is busy and the listen queue is full. |
3.7. A Few Words about Building Scalable Server Applications
When thinking about building scalable server applications, we have to think about thread pools. Thread pools allow a server application to queue and perform work in an efficient way. Without thread pools, the programmer has two options:
or
- Use a single thread and perform all the work using that thread. This means linear execution (one has to wait for an operation to terminate before proceeding on).
- Spawn a new thread every time a new job needs processing. Having multiple threads performing work at the same time does not necessarily mean that the application is becoming faster or is getting more work done. If multiple threads try to access the same resource, you may have a block scenario where threads are waiting for a resource to become available.
In the .NET framework, the “System.Threading
” namespace has a ThreadPool
class. Unfortunately, it is a static
class, and therefore our server can only have a single thread pool. This isn't the only issue. The ThreadPool
class does not allow us to set the concurrency level of the thread pool.
The LanTalk.Core
assembly contains the common classes used by both the server and the client application.
4.1 The ChatSocket Class
The ChatSocket
class is a wrapper around a Socket
, extending with other data:
The ChatSocket
class has three public
methods:
Raises the Sent
event if the sending operation was successful. On exception, closes the socket and raises the Disconnected
event.
Send
- Using the underlying socket, writes out the encapsulated data to the network. This is done in the following way: the first four bytes are sent - a ChatCommand
enumeration type that notifies the receiver of the command the sender wants to carry out. Then, the command target IP is sent. The last step is to send the metadata (the actual data - more on this later). Receive
- Reads the data using the wrapped socket asynchronously. Basically, the same steps are performed as in the Send
. If an unknown command was sent, the user of the ChatSocket
object disconnects the socket. Raises the Received
event, only if a known command was received. On exception, closes the socket and raises the Disconnected
event. Close
- Flushes the data and closes the socket. Doesn't raise any event.
4.2. A Word about metadata and the Use of Reflection
The metatype data is the string
representation of the object used in the communication between the client and the server, something like: “LanTalk.Core.ResponsePacket
, LanTalk.Core
, Version=1.8.3.2
, Culture=neutral
, PublicKeyToken=null”
, where the meta buffer contains the actual object.
Each metatype received should implement the IBasePacket
interface:
interface IBasePacket
{
void Initialize(byte[] metadata);
byte[] GetBytes();
}
The BasePacket
class defines the Initialize
method to initialize the data members of an IBasePacket
derived object using the Type.GetField
method. This is used when a byte array is received from the network (the meta buffer), and in the knowledge of the metadata type, we have to initialize the created object.
public void Initialize(byte[] data)
{
Type type = this.GetType();
FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic |
BindingFlags.Instance);
int ptr = 0;
int size = 0;
try
{
for (int i = 0; i < fields.Length; i++)
{
FieldInfo fi = fields[i];
size = BitConverter.ToInt32(data, ptr);
ptr += 4;
if (fi.FieldType == typeof(System.String))
{
fi.SetValue(this, Encoding.Unicode.GetString(data, ptr, size));
ptr += size;
}
}
}
catch (Exception exc)
{
Trace.Write(exc.Message + " - " + exc.StackTrace);
}
}
The ChatSocket
class uses the Initialize
method in the following way:
Type typeToCreate = Type.GetType(metatype);
IBasePacket packet = (IBasePacket)Activator.CreateInstance(typeToCreate);
packet.Initialize(metaBuffer);
The GetBytes
method's role is to return a byte array representing the internal state of the calling object. It is used to create the byte array that the socket sends out to the network.
public byte[] GetBytes()
{
byte[] data = new byte[4];
int ptr = 0;
int size = 0;
Type type = this.GetType();
FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic |
BindingFlags.Instance);
try
{
for (int i = 0; i < fields.Length; i++)
{
FieldInfo fi = fields[i];
if (fi.FieldType == typeof(System.String))
{
string strField = (string)fi.GetValue(this);
size = Encoding.Unicode.GetByteCount(strField);
BasePacket.ResizeDataBuffer(ref data, ptr, size);
Buffer.BlockCopy(BitConverter.GetBytes(size), 0, data, ptr, 4);
ptr += 4;
BasePacket.ResizeDataBuffer(ref data, ptr, size);
Buffer.BlockCopy(Encoding.Unicode.GetBytes(strField), 0, data, ptr, size);
ptr += size;
}
}
}
catch (Exception exc)
{
Trace.Write(exc.Message + " - " + exc.StackTrace);
}
byte[] retData = new byte[ptr];
Array.Copy(data, retData, ptr);
return retData;
}
This method is used when sending data using the ChatSocket
class, as follows:
byte[] metaBuffer = null;
Type metaType = metadata.GetType();
MethodInfo mi = metaType.GetMethod("GetBytes");
metaBuffer = (byte[])mi.Invoke(metadata, null);
The classes derived from the IBasePacket
interface are used to encapsulate information into a known format and then communicate the object to the other party. The benefits of using Reflection is that defining new packet types becomes trivial.
The server is implemented as a threaded server, meaning that each new connection spawns a new thread. This implementation does not scale well with a large number of clients, but for my purposes, I only need a maximum of 32 connections. The main drawback is that each running thread has its own stack, which by default is one megabyte in size. This also means that on 32 bit machines with the default of one megabyte stack size, you can create only roughly 2000 threads before you run out of the 2GB user address space per process. This can be avoided by using IO completion ports.
The LanTalk.Server.dll assembly contains the server implementation, and is used by the GUI and the service application. When starting the server, the following operations are performed:
socket.BeginAccept(new AsyncCallback(HandleIncommingClient), null);
private void HandleIncommingClient(IAsyncResult aResult)
{
....
Socket clientSocket = socket.EndAccept(aResult);
....
socket.BeginAccept(new AsyncCallback(HandleIncommingClient), null);
...
}
- The settings are read from the Data\Settings.xml file
- A new socket is created using the IP address and port specified in the settings file (in case of error, the socket is created on
127.0.0.1:65534
) - Associate the socket with the created endpoint (the [IP, port] combination) - this is called binding the socket
- Place the socket in listening state
- If all goes well, the server fires the
Started
event and begins accepting the connections asynchronously.
The ClientManager Class
The ChatServer
object maintains the list of connected clients. For this, when a connection is made, it creates a ClientManager
object. The ClientManager
object holds the connected socket and some user related information. Its responsibility is to send and receive data, and when that happens, to signals it with events. Also, the ClientManager
updates user related information such as last connection time and last IP address of the connection.
The Client
class is implemented as a singleton, only one instance of the client can be created. The Client
class has three methods exposed to the outside (Connect
, Disconnect
, SendCommand
); the rest of the functionality exposed is implemented through the use of events.
This is a simple class with four core functionalities:
- Connect to the server - the
Connect
method - fires the Connected
event on successful connection, or the ConnectFailed
event in case of error. - Send messages to the server - the
SendCommand
method. - Receive messages from the server - responds to the
ChatSocket
's Received
event - fires the CommandReceived
event. - Disconnect from the server - the
Disconnect
method - closes the underlying socket and fires the Disconnected
event.
The Client
object uses the TCP keep-alive feature to prevent disconnecting the clients from the server in case of inactivity:
private void OnConnect(IAsyncResult ar)
{
SockUtils.SetKeepAlive(socket, 600 * 1000, 60 * 1000);
}
The Settings.xml file is used for persisting application settings, and is saved in the currently logged on user's profile folder. The exact location is “%Documents and settings%\%user%\Application Data\Netis\LanTalk Client”.
Archiving messages is also provided on a per Windows user/per LanTalk user basis. For example, for me:
- The directory “C:\Documents and Settings\zbalazs\Application Data\Netis\LanTalk Client\” will hold the Settings.xml file, and I will have a directory created with the username which I use to authenticate in the LanTalk application (“zoli”).
- The “C:\Documents and Settings\zbalazs\Application Data\Netis\LanTalk Client\zoli\” directory will hold the message archive for me.
For logging, the application uses the NLog library. NLog provides a simple logging interface, and is configured by the application.exe.nlog file. If this file is missing, logging will not occur.
I recommend downloading the samples provided and playing with it a few times. After that, check out the sources. I hope that the article follows a logical line describing briefly the highlights of the projects.
References
- Transmission control protocol[^]
- Which I/O strategy should I use?[^]
- Get Closer to the Wire with High-Performance Sockets in .NET[^]
- TCP/IP Chat Application Using C#[^]
- TCP Keep-Alive overview [^]
- How expensive is TCP's Keep-Alive feature?[^]
- TCP/IP and NBT configuration parameters for Windows XP [^]
- DockManager control [^]
- TaskbarNotifier[^]
- Self installing .NET service using the Win32 API[^]
- NLog[^]