Introduction
I've been working with sockets since 2000, using Delphi 5.0 and some third-party libraries (Synapse). My very first socket application just copied files between many clients and one server. The client app checks a folder to see if files exist, asks the server where to copy the files in the network and, after copying the files, flags the database record indicating that a file has been moved. The server listens to the client connections, and both exchange XML messages indicating the state of each file copy. Synapse is a blocking socket implementation, and I needed a thread pooling mechanism that works like an HTTP server, because I couldn't keep the connection open (one thread per connection). My solution was to use some IOCP functions to pool the client requests (code) and close the connection after the message exchange was terminated.
Now, using C#, I decided to write a socket server and client library that helps me to only have to think about the message exchange (the process) and let .NET do the hard job. So, I needed the following features:
- Asynchronous processing
- Some encryption and compression capabilities
- Encapsulate the socket, and encrypt the services in the interfaces and separate them from the host implementation
Socket Connection
The ISocketConnection
is the base interface for the socket connections, and describes all the connection properties and the methods. The ConnectionID
property defines a unique connection ID using a GUID string. The CustomData
property defines a custom object that can be associated with the connection. The Header
property is the socket service header used in each message that is encapsulated in a packet message. Only messages with a defined header will be accepted. The LocalEndPoint
and RemoteEndPoint
are the socket IP end points used in the connection. SocketHandle
is the socket handle given by the OS.
The IClientSocketConnection
and IServerSocketConnection
inherit the ISocketConnection
, and each one has special functions. The IClientSocketConnection
can reconnect to the server using the BeginReconnect
method, and the IServerSocketConnection
can communicate with other connections in the server host using the BeginSendTo
and BeginSendToAll
methods, and can get the ConnectionId
using the GetConnectionById
method. Every connection knows the host, the encryption, the compression type, and can send, receive, and disconnect itself from the other part. This interface is used in the ISocketService
interface to allow the user to interact with socket connections.
Internally, in the library implementation, all the connection interfaces are created using the base connection implementations: BaseSocketConnection
, ClientSocketConnection
, and ServerSocketConnection
.
Socket Service
The ISocketService
describes the connection events. These events are fired by the host, and have a ConnectionEventArgs
argument which has an ISocketConnection
that identifies the connection. In the OnReceived
and OnSent
events, a MessageEventArgs
is passed, which has the sent or received array of bytes. In the OnDisconnected
event, a DisconnectedEventArgs
is passed; the Exception
property indicates if the disconnection has been caused by an exception.
Here is an example of a ISocketService
implementation:
public class SimpleEchoService : ISocketService
{
public void OnConnected(ConnectionEventArgs e)
{
if (e.Connection.HostType == HostType.htServer)
{
e.Connection.BeginReceive();
}
else
{
byte[] b =
GetMessage(e.Connection.SocketHandle.ToInt32());
e.Connection.BeginSend(b);
}
}
public void OnSent(MessageEventArgs e)
{
if (e.Connection.HostType == HostType.htServer)
{
e.Connection.BeginReceive();
}
else
{
e.Connection.BeginReceive();
}
}
public override void OnReceived(MessageEventArgs e)
{
if (e.Connection.HostType == HostType.htServer)
{
byte[] b = e.Buffer;
e.Connection.BeginSend(b);
}
else
{
byte[] b = GetMessage(e.Connection.SocketHandle.ToInt32());
e.Connection.BeginSend(b);
}
}
public override void OnDisconnected(DisconnectedEventArgs e)
{
if (e.Connection.HostType == HostType.htServer)
{
}
else
{
e.Connection.AsClientConnection().BeginReconnect();
}
}
}
The ISocketService
implementation can be done in the same host assembly, or another assembly referenced by the host. This allows the user to separate the host implementation from the socket service, helping the administration in a server or a domain.
Connection Host
With the ISocketService
created, you need to host the service and the service connections. Both the server and the client host have the same parent class, BaseSocketConnectionHost
, which keeps a list of connections, encrypts and compresses the data buffers, enqueues the service requests and ensures that all data buffer has been sent or received, checks messages headers, and checks for idle connections. The CheckTimeoutTimer
, periodically, at IdleCheckInterval
, checks if the connections become idle, using the IdleTimeOutValue
as the idle timeout. Header
is the socket service header used by the host. HostType
indicates if a host is a server or a client host. SocketBufferSize
defines the size of the socket send and receive buffer. SocketService
is the instance of ISocketService
that drives the message exchange between the connections.
Encrypt and Compress
Every time you send and receive messages, the host checks if the data must be encrypted and/or compressed, and this work is made by the CryptUtils
static class. The CreateSymmetricAlgoritm
creates an ISymmetricAlgoritm
based on the encryptType
parameter. The DecryptData
and DecryptDataForAuthenticate
are used, respectively, to decrypt the received message and check the hash sign on the authenticate procedure. The EncryptData
and EncryptDataForAuthenticate
, respectively, encrypt the data to be sent and sign the authenticated message.
The encrypted data buffer is labelled with the service header and the data buffer length, becoming a packet buffer. This packet buffer is controlled by the MessageBuffer
class that keeps information about the packet buffer offset, length, the remaining bytes, and the raw buffer.
Enqueuing requests
Every time you call BeginReceive
or BeginSend
in ISocketService
, the host checks if some request has been initiated. If a request is in process, the host enqueues the request. If not, it fires the request.
Send request
In the BeginSend
method, the following enqueuing is used:
internal void BeginSend(BaseSocketConnection connection, byte[] buffer)
{
...
lock (connection.WriteQueue)
{
if (connection.WriteQueueHasItems)
{
connection.WriteQueue.Enqueue(writeMessage);
}
else
{
connection.WriteQueueHasItems = true;
...
When the message is sent, in the send callback, the host checks the queue again and initiates another send process, if needed:
private void BeginSendCallback(IAsyncResult ar)
{
...
lock (connection.WriteQueue)
{
if (connection.WriteQueue.Count > 0)
{
MessageBuffer dequeueWriteMessage =
connection.WriteQueue.Dequeue();
...
}
else
{
connection.WriteQueueHasItems = false;
}
}
...
Receive request
The same technique applies to the receive method: all the calls to BeginReceive
are enqueued if the receive method is in action. If no receive process was initiated, the host starts to receive:
internal void BeginReceive(BaseSocketConnection connection)
{
...
lock (connection.SyncReadCount)
{
if (connection.ReadCanEnqueue)
{
if (connection.ReadCount == 0)
{
MessageBuffer readMessage = new MessageBuffer
(FSocketBufferSize);
...
}
connection.ReadCount++;
}
}
...
After that, when the message is received and parsed in the receive callback, the host checks the read queue again, and initiates another receive process, if needed:
private void BeginReadCallback(IAsyncResult ar)
{
...
lock (connection.SyncReadCount)
{
connection.ReadCount--;
if (connection.ReadCount > 0)
{
...
}
}
...
Ensure send and receive
To ensure that all data buffer is sent, the BaseSocketConnectionHost
checks the bytes sent, and compares it to the MessageBuffer
class. It continues to send the remaining bytes till all the data buffer is sent:
private void BeginSendCallback(IAsyncResult ar)
{
...
byte[] sent = null;
int writeBytes = .EndSend(ar);
if (writeBytes < writeMessage.PacketBuffer.Length)
{
writeMessage.PacketOffSet += writeBytes;
.BeginSend(writeMessage.PacketBuffer, writeMessage.PacketOffSet,
writeMessage.PacketRemaining, SocketFlags.None ...);
}
else
{
sent = new byte[writeMessage.RawBuffer.Length];
Array.Copy(writeMessage.RawBuffer, 0, sent, 0,
writeMessage.RawBuffer.Length);
FireOnSent(connection, sent);
}
}
The same approach is used in the receive data buffers because, to read data, a MessageBuffer
is used as the read buffer. When the receive callback is called, it continues to read till all the bytes in the message are read:
private void BeginReadCallback(IAsyncResult ar)
{
...
CallbackData callbackData = (CallbackData)ar.AsyncState;
connection = callbackData.Connection;
readMessage = callbackData.Buffer;
int readBytes = 0;
...
readBytes = .EndReceive(ar);
...
if (readBytes > 0)
{
...
...
readMessage.PacketOffSet += readBytes;
...
if (readSocket)
{
.BeginReceive(readMessage.PacketBuffer,
readMessage.PacketOffSet,
readMessage.PacketRemaining,
SocketFlags.None, ...);
}
}
...
Check message header
If the socket service uses some header, all the send and receive processes need to create a packet message indicating the header and the message length. This packet label is created using the following structure:
The first label's part is the socket service header. The header is an array of bytes of any length, and you need some advice here: if you choose a very small header, maybe you can have a message with the same array of bytes somewhere, and the host will lose the sequence. If you choose a very long array of bytes, the host can spend the processor's time to verify if the message header is equal to the socket service. The second part is the packet message length. This length is calculated adding the raw message data buffer length, encrypted and/or compressed, plus the header length.
Sending packets
As said before, every time you send messages, the host checks if the data must be encrypted and/or compressed, and, if you choose to use some header, the raw buffer is controlled by the MessageBuffer
class. This class is created using the GetPacketMessage
static
method:
public static MessageBuffer GetPacketMessage(
BaseSocketConnection connection, ref byte[] buffer)
{
byte[] workBuffer = null;
workBuffer = CryptUtils.EncryptData(connection, buffer);
if (connection.Header != null && connection.Header.Length >= 0)
{
int headerSize = connection.Header.Length + 2;
byte[] result = new byte[workBuffer.Length + headerSize];
int messageLength = result.Length;
for (int i = 0; i < connection.Header.Length; i++)
{
result[i] = connection.Header[i];
}
result[connection.Header.Length] =
Convert.ToByte((messageLength & 0xFF00) >> 8);
result[connection.Header.Length + 1] =
Convert.ToByte(messageLength & 0xFF);
Array.Copy(workBuffer, 0, result,
headerSize, workBuffer.Length);
return new MessageBuffer(ref buffer, ref result);
}
else
{
return new MessageBuffer(ref buffer, ref workBuffer);
}
}
Receiving packets
The receive process, if you're using some socket service header, needs to check the header, and continues to read bytes till all the packet message is received. This process is executed in the read callback:
private void BeginReadCallback(IAsyncResult ar)
{
...
byte[] received = null
byte[] rawBuffer = null;
byte[] connectionHeader = connection.Header;
readMessage.PacketOffSet += readBytes;
if ((connectionHeader != null) && (connectionHeader.Length > 0))
{
int headerSize = connectionHeader.Length + 2;
bool readPacket = false;
bool readSocket = false;
do
{
connection.LastAction = DateTime.Now;
if (readMessage.PacketOffSet > headerSize)
{
for (int i = 0; i < connectionHeader.Length; i++)
{
if (connectionHeader[i] != readMessage.PacketBuffer[i])
{
throw new BadHeaderException(
"Message header is different from Host header.");
}
}
int messageLength =
(readMessage.PacketBuffer[connectionHeader.Length] << 8) +
readMessage.PacketBuffer[connectionHeader.Length + 1];
if (messageLength > FMessageBufferSize)
{
throw new MessageLengthException("Message " +
"length is greater than Host maximum message length.");
}
if (messageLength == readMessage.PacketOffSet)
{
rawBuffer =
readMessage.GetRawBuffer(messageLength, headerSize);
readPacket = false;
readSocket = false;
}
else
{
if (messageLength < readMessage.PacketOffSet)
{
rawBuffer =
readMessage.GetRawBuffer(messageLength, headerSize);
rawBuffer = CryptUtils.DecryptData(connection,
ref rawBuffer, FMessageBufferSize);
readPacket = true;
readSocket = false;
received = new byte[rawBuffer.Length];
Array.Copy(rawBuffer, 0, received, 0,
rawBuffer.Length);
FireOnReceived(connection, received, false);
}
else
{
if (messageLength > readMessage.PacketOffSet)
{
if (messageLength > readMessage.PacketLength)
{
readMessage.Resize(messageLength);
}
readPacket = false;
readSocket = true;
}
}
}
}
else
{
if (readMessage.PacketRemaining < headerSize)
{
readMessage.Resize(readMessage.PacketLength + headerSize);
}
readPacket = false;
readSocket = true;
}
} while (readPacket);
if (readSocket)
{
...
.BeginReceive(readMessage.PacketBuffer, readMessage.PacketOffSet,
readMessage.PacketRemaining, SocketFlags.None, ...);
...
}
}
else
{
rawBuffer = readMessage.GetRawBuffer(readBytes, 0);
}
if (rawBuffer != null)
{
rawBuffer = CryptUtils.DecryptData(connection,
ref rawBuffer, FMessageBufferSize);
received = new byte[rawBuffer.Length];
Array.Copy(rawBuffer, 0, received, 0, rawBuffer.Length);
FireOnReceived(connection, received, true);
readMessage.Resize(FSocketBufferSize);
...
The read callback method first checks if the connection has some header and, if not, just gets the raw buffer and continues. If the connection has some header, the method needs to check the message header against the socket service header. Before doing that, it checks if the packet message length is greater than the connection header length, to ensure that it can parse the total message length. If not, it reads some bytes. After checking the header, the method parses the message length, and checks with the packet length. If the length is equal, it gets the raw buffer and terminates the loop. If the message length is less than that of the packet message, we have the message plus some data. So, the method gets the raw buffer and continues to read using the same MessageBuffer
class. If the length of the message is greater than that of the packet message, before reading some data, it just resizes the packet buffer to the message size, ensuring enough room for more read bytes.
Checking idle connections
Using the BeginSend
and BeginReceive
methods of ISocketConnection
doesn't return some IAsyncResult
to know if the method was completed or not allowing disconnection after some timeout value. To prevent this, the BaseSocketConnectionHost
has a System.Threading.Timer
that periodically checks the LastAction
property of BaseSocketConnection
. If LastAction
is greater than the idle timeout, the connection is closed.
Crypto Service
The ICryptoService
describes the authentication methods fired when the connection is made to the other part. The OnSymmetricAuthenticate
method is fired when EncryptType.etRijndael
or EncryptType.etTripleDES
is used, and OnSSLXXXXAuthentication
is fired when EncryptType.etSSL
is used. Like ISocketService
, the ICryptService
can be done in the same host assembly, or another assembly referenced by the host, so you can have one ICryptoService
implementation used in many ISocketService
implementations.
SSL authentication
There's a new stream class called SslStream
in .NET 2.0 which can authenticate SSL streams. The SslStream
's constructor accepts a NetworkStream
class, and this stream is created using the Socket
class. So, using SslStream
, you can send and receive data buffers using socket connections.
Server authentication
The SslStream
authentication is done in both the client and the server, but each one has different parameters. In the server side, you need to pass a certificate using the X509Certificate2
class, either finding in the certificate store using X509Store
, or by creating it from a certification file (.cer). Also, you can request a client authentication and check the certificate's revocation. The following code is an example of an SSL server authentication using ICryptService
:
public void OnSSLServerAuthenticate(out X509Certificate2 certificate,
out bool clientAuthenticate, ref bool checkRevocation)
{
X509Store store = new X509Store(StoreName.My,
StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certs =
store.Certificates.Find(X509FindType.FindBySubjectName,
"ALAZ Library", false);
certificate = certs[0];
clientAuthenticate = false;
checkRevocation = false;
store.Close();
}
Client authentication
On the client side of the SSL authentication, you need to pass the host name of the server certificate, and if this name doesn't match, the authentication fails. You can pass a client certificate collection using X509Certificate2Collection
. If the server doesn't request a client authentication, you don't need to pass the collection but, if the server requests it, you can find the certificates using X509Store
. You can also request a client certificate's revocation. This is an example of SSL client authentication in ICryptoService
:
public void OnSSLClientAuthenticate(out string serverName,
ref X509Certificate2Collection certs, ref bool checkRevocation)
{
serverName = "ALAZ Library";
}
Certificates
To create certificates, you can use the MakeCert.exe tool found in .NET, and there's a lot of information available about it. You can take a look at John Howard's page, this MS post, and this website.
Symmetric authentication
To implement some symmetric encryption and authentication in this library, I decided to put a post in Microsoft newsgroups. Unfortunately, for the post, but luckily for the knowledge sharing (many thanks to Joe Kaplan, Dominick Baier, and Valery Pryamikov), I decided to use William Stacey's implementation example "A generic method to send secure messages using an exchanged session key". In this code, the symmetric key used in the session is encrypted and signed using RSA key pairs, and the client part needs to know the encrypted server's public key, meaning that this key isn't received from the server in the authentication process. Both the client and the server need to know this key through a manual process. To ensure this, the OnSymmetricAuthenticate
needs a RSACryptoServiceProvider
class providing the key pair for encryption. You can fill the RSACryptoServiceProvider
from an XML string, a file, a CspParameters
class, or a certificate. Here is an example of symmetric authentication:
public void OnSymmetricAuthenticate(HostType hostType,
out RSACryptoServiceProvider serverKey)
{
CspParameters param = new CspParameters();
param.KeyContainerName = "ALAZ_ECHO_SERVICE";
serverKey = new RSACryptoServiceProvider(param);
}
The authentication message
The symmetric authentication uses the AuthMessage
structure to exchange session keys between the client and the server. The SessionKey
and SessionIV
properties are, respectively, the symmetric key and the initialization vector of the algorithm. The Sign
property is the hash code generated by the client using the sign RSACryptoServiceProvider
class created internally, and its public key is exchanged using the SourceKey
property. This internal sign key pair is necessary to sign the AuthMessage
, and the server can ensure that the AuthMessage
is accurate. This process is done using the following code:
Client side
...
private byte[] signMessage = new byte[]
{ <sign message array of bytes for authentication> };
...
protected virtual void InitializeConnection(BaseSocketConnection connection)
{
...
if (connection.EncryptType == EncryptType.etRijndael ||
connection.EncryptType == EncryptType.etTripleDES)
{
if (FHost.HostType == HostType.htClient)
{
RSACryptoServiceProvider serverPublicKey;
RSACryptoServiceProvider clientPrivateKey =
new RSACryptoServiceProvider();
FCryptoService.OnSymmetricAuthenticate(FHost.HostType,
out serverPublicKey);
SymmetricAlgorithm sa =
CryptUtils.CreateSymmetricAlgoritm(connection.EncryptType);
sa.GenerateIV();
sa.GenerateKey();
connection.Encryptor = sa.CreateEncryptor();
connection.Decryptor = sa.CreateDecryptor();
AuthMessage am = new AuthMessage();
am.SessionIV = serverPublicKey.Encrypt(sa.IV, false);
am.SessionKey = serverPublicKey.Encrypt(sa.Key, false);
am.SourceKey =
CryptUtils.EncryptDataForAuthenticate(sa,
Encoding.UTF8.GetBytes(clientPrivateKey.ToXmlString(false)),
PaddingMode.ISO10126);
MemoryStream m = new MemoryStream();
m.Write(am.SourceKey, 0, am.SourceKey.Length);
m.Write(am.SessionKey, 0, am.SessionKey.Length);
m.Write(signMessage, 0, signMessage.Length);
am.Sign = clientPrivateKey.SignData(
CryptUtils.EncryptDataForAuthenticate(sa,
m.ToArray(), PaddingMode.PKCS7),
new SHA1CryptoServiceProvider());
XmlSerializer xml = new XmlSerializer(typeof(AuthMessage));
m.SetLength(0);
xml.Serialize(m, am);
MessageBuffer mb = new MessageBuffer(0);
mb.PacketBuffer =
Encoding.Default.GetBytes(Convert.ToBase64String(m.ToArray()));
connection.Socket.BeginSend(
mb.PacketBuffer, mb.PacketOffSet,
mb.PacketRemaining, SocketFlags.None,
new AsyncCallback(InitializeConnectionSendCallback),
new CallbackData(connection, mb));
m.Dispose();
am.SessionIV.Initialize();
am.SessionKey.Initialize();
serverPublicKey.Clear();
clientPrivateKey.Clear();
}
...
}
On the client side of the symmetric authentication, the OnSymmetricAuthenticate
is called, getting the RSACryptoServiceProvider
to encrypt the session key generated by the CryptUtils.CreateSymmetricAlgoritm
method. The AuthMessage
is filled with the encrypted session key, session IV, and the sign public key. To sign the message, the SourceKey
, SessionKey
, and signMessage
are used, and the resulting hash is assigned to the Sign
property.
Server side
protected virtual void InitializeConnection(BaseSocketConnection connection)
{
...
if (FHost.HostType == HostType.htClient)
{
...
}
else
{
MessageBuffer mb = new MessageBuffer(8192);
connection.Socket.BeginReceive(mb.PacketBuffer, mb.PacketOffSet,
mb.PacketRemaining, SocketFlags.None,
new AsyncCallback(InitializeConnectionReceiveCallback), ...);
}
}
private void InitializeConnectionReceiveCallback(IAsyncResult ar)
{
...
bool readSocket = true;
int readBytes = ....EndReceive(ar);
if (readBytes > 0)
{
readMessage.PacketOffSet += readBytes;
byte[] message = null;
try
{
message = Convert.FromBase64String(
Encoding.Default.GetString(readMessage.PacketBuffer,
0, readMessage.PacketOffSet));
}
catch (FormatException)
{
}
if ((message != null) &&
(Encoding.Default.GetString(message).Contains("</AuthMessage>")))
{
RSACryptoServiceProvider serverPrivateKey;
RSACryptoServiceProvider clientPublicKey =
new RSACryptoServiceProvider();
FCryptoService.OnSymmetricAuthenticate(FHost.HostType,
out serverPrivateKey);
MemoryStream m = new MemoryStream();
m.Write(message, 0, message.Length);
m.Position = 0;
XmlSerializer xml = new XmlSerializer(typeof(AuthMessage));
AuthMessage am = (AuthMessage)xml.Deserialize(m);
SymmetricAlgorithm sa =
CryptUtils.CreateSymmetricAlgoritm(connection.EncryptType);
sa.Key = serverPrivateKey.Decrypt(am.SessionKey, false);
sa.IV = serverPrivateKey.Decrypt(am.SessionIV, false);
connection.Encryptor = sa.CreateEncryptor();
connection.Decryptor = sa.CreateDecryptor();
clientPublicKey.FromXmlString(Encoding.UTF8.GetString(
CryptUtils.DecryptDataForAuthenticate(sa,
am.SourceKey, PaddingMode.ISO10126)));
m.SetLength(0);
m.Write(am.SourceKey, 0, am.SourceKey.Length);
m.Write(am.SessionKey, 0, am.SessionKey.Length);
m.Write(signMessage, 0, signMessage.Length);
if (!clientPublicKey.VerifyData(
CryptUtils.EncryptDataForAuthenticate(sa, m.ToArray(),
PaddingMode.PKCS7),
new SHA1CryptoServiceProvider(), am.Sign))
{
throw new
SymmetricAuthenticationException("Symmetric sign error.");
}
readSocket = false;
m.Dispose();
am.SessionIV.Initialize();
am.SessionKey.Initialize();
serverPrivateKey.Clear();
clientPublicKey.Clear();
FHost.FireOnConnected(connection);
}
if (readSocket)
{
....BeginReceive(readMessage.PacketBuffer,
readMessage.PacketOffSet,
readMessage.PacketRemaining,
SocketFlags.None,
new AsyncCallback(
InitializeConnectionReceiveCallback), ...);
}
}
On the server side of the symmetric authentication, a MessageBuffer
is used to receive the socket buffer. The read callback method continues to read till a completed AuthMessage
is received. With this message, the method calls the OnSymmetricAuthenticate
to get the RSACryptoServiceProvider
to decrypt the session key, session IV, and the sign public key. With all the keys decrypted, the method verifies the Sign
property to ensure that the AuthMessage
is accurate, using the SourceKey
, SessionKey
, and signMessage
.
Connection Creator
Although BaseSocketConnectionHost
can manage ISocketConnection
connections, it cannot create them. This job is made by BaseSocketConnectionCreator
which creates and initializes ISocketConnection
s. The CompressionType
and EncryptType
properties define, respectively, the compression and the encryption types that will be used in the connection. The CryptoService
defines the ICrytoService
instance used to initialize the connection, if needed. The Host
property defines the host of the BaseSocketConnectionCreator
; it can be a server or a client host. The LocalEndPoint
defines the socket IP end point used in the connection, and it can have different behavior depending on the type of the creator.
SocketServer and SocketListener
The SocketServer
and SocketListener
are the classes needed to create a socket server. SocketServer
is derived from BaseSocketConnectionHost
, and manages ISocketConnections
. The SocketListener
is derived from BaseSocketConnectionCreator
, and listens for incoming connections, accepts a connection, and creates a new ISocketConnection
to be used. A SocketServer
can have as many SocketListener
s attached as required, each one assigned to a local port to listen.
SocketServer constructor and methods
In the SocketServer
constructor, the socketService
parameter defines the ISocketService
instance used by the server. The header parameters define the array of bytes used in the message header exchange. The socketBufferSize
adjusts the socket buffer size. The messageBufferSize
defines the maximum message size of the service. The idleCheckInterval
indicates the interval for idle connections checking, in milliseconds. The idleTimeoutValue
defines the timeout, in milliseconds, to be compared to each connection LastAction
property.
To add SocketListener
items in SocketServer
, the method AddListener
must be used. The localEndPoint
parameter defines the local socket IP endpoint used to listen to connections. The encryptType
and compressionType
defines, respectively, the encryption and compression methods used in the new accepted connection. The cryptoService
defines the ICryptoService
used to authenticate the encryption method chosen. The backLog
limits the listen queue of the OS socket to the defined number, and acceptThreads
sets the calling number of the socket's BeginAccept
to increase the accepted performance.
HostThreadPool
This library uses asynchronous socket communication which, in turn, uses the .NET ThreadPool
. In the .NET 2.0 ThreadPool
, the thread number can be controlled using the SetMaxThreads
and SetMinThreads
methods, and I think there are a lot of improvements in this class. But, if you don't want to use the .NET class, you can use a managed thread pool called HostThreadPool
, very similar to Stephen Toub's ManagedThreadPool. HostThreadPool
uses a list of managed threads that keeps increasing as more enqueueing tasks are provided. To use this class instead of the .NET ThreadPool
in SocketServer
, just set the minThreads
and maxThreads
constructor parameters to non-zero numbers.
Here are some examples of using SocketServer
and SocketListener
:
SocketServer server = new SocketServer(new SimpleEchoService());
server.AddListener(new IPEndPoint(IPAddress.Any, 8087));
server.Start();
SocketServer server = new SocketServer(new SimpleEchoService(),
new byte[] { 0xFF, 0xFE, 0xFD });
server.AddListener(new IPEndPoint(IPAddress.Any, 8087),
EncryptType.etBase64, CompressionType.ctNone, null);
server.Start();
SocketServer server = new SocketServer(new SimpleEchoService(),
new byte[] { 0xFF, 0xFE, 0xFD },
2048, 8192, 0, 0, 60000, 30000);
server.AddListener(new IPEndPoint(IPAddress.Any, 8087));
server.AddListener(new IPEndPoint(IPAddress.Any, 8088),
EncryptType.etBase64, CompressionType.ctNone, null);
server.AddListener(new IPEndPoint(IPAddress.Any, 8089),
EncryptType.etRijndael, CompressionType.ctGZIP,
new SimpleEchoCryptService(), 50, 10);
server.AddListener(new IPEndPoint(IPAddress.Any, 8090),
EncryptType.etSSL, CompressionType.ctNone,
new SimpleEchoCryptService());
server.Start();
SocketClient and SocketConnector
The SocketClient
and SocketConnector
are the classes needed to create a socket client. SocketClient
is derived from BaseSocketConnectionHost
and, like SocketServer
, manages ISocketConnections
. The SocketConnector
is derived from BaseSocketConnectionCreator
, and it connects with the socket server and creates a new ISocketConnection
to be used. A SocketClient
can have as many SocketConnector
s attached as required, each one connecting to a socket server, and they can be assigned to a local address and a local port to start the connection.
SocketClient constructor and methods
The SocketClient
constructor has the same parameter signature as the SocketServer
class. To add SocketConnector
items in SocketClient
, the method AddConnector
must be used. The remoteEndPoint
parameter defines the remote socket IP endpoint used for the connection. The encryptType
and compressionType
define, respectively, the encryption and compression methods used in the new connection. The cryptoService
defines the ICryptoService
used to authenticate the encrypted method chosen. The reconnectAttempts
and reconnectAttemptInterval
define, respectively, the number of reconnect attempts when using BeginReconnect
method and the time interval to reconnect. The localEndPoint
defines the local socket IP endpoint used to start the connection process to the remote endpoint.
Here are some examples of using SocketClient
and SocketConnector
:
SocketClient client = new SocketClient(new SimpleEchoService());
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087));
client.Start();
SocketClient client = new SocketClient(new SimpleEchoService(),
new byte[] { 0xFF, 0xFE, 0xFD });
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087),
EncryptType.etBase64, CompressionType.ctNone, null);
client.Start();
SocketClient client = new SocketClient(new SimpleEchoService(),
new byte[] { 0xFF, 0xFE, 0xFD },
2048, 8192, 0, 0, 60000, 30000);
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087),
EncryptType.etSSL, CompressionType.ctGZIP,
new SimpleEchoCryptService(),
5, 30000);
client.Start();
SocketClient client = new SocketClient(new SimpleEchoService(),
new byte[] { 0xFF, 0xFE, 0xFD },
4096, 8192, 5, 50, 60000, 30000);
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087),
EncryptType.etSSL, CompressionType.ctGZIP,
new SimpleEchoCryptService(),
5, 30000,
new IPEndPoint(IPAddress.Parse("10.10.3.1"), 2000));
client.Start();
SocketClient client = new SocketClient(new SimpleEchoService());
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087));
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.2"), 8088),
EncryptType.etBase64, CompressionType.ctNone, null);
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.3"), 8089),
EncryptType.etRijndael, CompressionType.ctGZIP,
new SimpleEchoCryptService());
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.4"), 8090),
EncryptType.etSSL, CompressionType.ctNone,
new SimpleEchoCryptService(),
5, 30000,
new IPEndPoint(IPAddress.Parse("10.10.3.1"), 2000));
client.Start();
Echo demo project
There's an Echo
demo project available in the article download file, using Console, Windows Forms, and Windows Service hosts and clients, and all them use the same EchoSocketService
and EchoCryptService
. The demos are divided by their type as follows:
Hosts
- Console
EchoConsoleClient
EchoConsoleServer
- Windows Forms
EchoFormClient
EchoFormServer
Echo<code>
Form (Forms template)
- Windows Service
EchoWindowsServiceServer
Services
EchoSocketService
EchoCryptService
Conclusion
There's a lot going on here, and I think this library can help anyone who wants to write asynchronous sockets with encryption and compression. Any comments will be appreciated.
History
- May 15, 2006: Initial version
- May 19, 2006: Some English text corrections (sorry, I'm still learning!), and rechecking the demo sources
- June 06, 2006: Version 1.2 with the following changes:
- Minor bugs fixed
- All "Sended" changed to "Sent" (thanks vmihalj)
ReadCanEnqueue
now works correctly and with HostThreadPool
(thanks PunCha) - Added
reconnectAttempts
and reconnectAttemptInterval
to allow client connection reconnects as many times as is needed in a timed interval (thanks Tobias Hertkorn)
- April 01, 2007: Version 1.3 with the following changes:
- Fixed
rawbuffer = null
- Fixed
BeginAcceptCallback
: stops accepting if exception occurs - Fixed
BeginSendCallback
: PacketRemaining
bytes should be used - Socket config section added in demos
- New message size (64K)
HosThreadPool
removed - Header changed to Delimiter property with new delimiter options:
dtNone
: No message delimiter dtPacketHeader
: Version 1.2 backward dtMessageTailExcludeOnReceive
: Use custom delimiter at end of message (Exclude delimiter on receiving) dtMessageTailIncludeOnReceive
: Use custom delimiter at end of message (Include delimiter on receiving)
- New connection object properties/methods:
- Nagle, Linger and TTL Algorithm options
- Host and Creator
- Encrypt sign message in service class
- Exception event in service class
- New Creator name property
- July 22, 2007: Version 1.4 with the following changes:
- Connection initialize procedures executing in same thread (not queued in
ThreadPool
) - Connection disconnect now checks windows version and executes the correct disconnect procedure
- Connection Active checking for
Disposed
CheckSocketConnections
disposed check fixed CryptUtils
Flush()
method included - Client Connection
BeginConnect()
exception fixed - Server Connection
BeginSendToAll
array buffer fixed - New
SocketClientSync
class for synchronous using (WinForms demo included)
- September 5, 2007: Version 1.5 with the following changes:
SocketClient
with proxy authentication (SOCKS5, Basic HTTP) BeginRead
bug fixed (messagetail
) BeginDisconnect
changed (threadpool
) BeginSendToAll
reviewed (disposed check) - New
OnSSLClientValidateServerCertificate
event to validate server's certificate - Idle check interval set to 0 and only created when greater than 0
- Using
Buffer.BlockCopy
instead of Array.Copy
- New Chat Demo