Abstracting TCP Communications, and adding what should be the new basics.

0
242

Introduction

Somehow, I have just fallen into doing a lot of network communications programming work. The more time I spend doing it and thinking about it, more more I expect from the communications library I’m using. Not long ago I began imagining how things could be easier and clearer if TCP Sessions worked just a little bit differently – because as it is now, we are free to (and tasked with) managing the logic involved with communications in our applications. For instance, once you create a TCP session, you need to decide how you will handle sending your database data, images, text, etc. If you have a large image or video to send, then maybe your text data takes longer to arrive then you would like.

What if sessions could contain other sessions?

When I first started working with TCP communications, I hated the idea of creating lots of TCP connections to a single server. It seemed slopy. It also seemed like I was cheating, because I believed that I should be able to do all the communicating I needed to over a single session. After all, bytes are bytes, right? If you have a connection to a server, you have a connection to a server.

Over the years I came up with different ways to logically separate and manage the data I was sending – but there’s just nothing like having an entire session to work with when sending whatever you have to send. So instead of comming up with yet another way to synchronize data between the client and the server, I built a session management system into this library. In this library, sessions really can contain other sessions. They’re called Sub-Sessions, and these are the communication channels you will use for everything that you need to logically separate. The beauty of this is that your network hardware and the routers between your local machine and your remote machine will handle the bandwidth balancing between data sent over your subsessions, so while your overall bandwidth per subsession may decrease, they all get as much bandwidth as your switch and router(s) can provide no matter what is being sent over them.

Sub-Session Linking

Once I had the subsession management system in place, another thought occurred to me. If I have a client that handles and manages (potentially) lots of TCP sessions, what if I logically connected two subsessions belonging to different clients at the server? If it was done correctly, data sent from client 1 over this linked subsession would naturally flow to the other client… and the other client would be able to send data to client 1. All we need is a server that is smart enough to do the associations when we ask it to.  We would also need to be able to get a list of connected clients, and clear error messages in the event that we asked the server to create a linked subsession, and for some reason it couldn’t.

Of course, this is peer to peer communication. In this library, we’re calling it “Sub-Session linking”. Linked subsessions can be created between any two connected clients.

Bytes? Who needs bytes?

Most TCP libraries out there will allow you to send text, but if you want to send anything else you need to convert the data into a string or a byte array. I spent a long time doing that, and it gets old. It forces you to do quite a bit of parsing, converting strings to byte arrays and converting classes to xml, and xml back into classes in your code. The more different kinds of data you need to send, the more of this there is. Things tend to get ugly fast.

This library has only a single .Send() function in the Client and Session classes for sending data. If you use it the way I intended, you will never even think about sending bytes again – because this library’s .Send() function accepts any .net serializable object. You drop your string into the Send() function, and you get a string on the other end. Have a DataTable to send? Drop it in the send function. Have some of your own classes to send? Mark them as serializable, and drop them into the Send() function – and off they go.

For this reason, I named the library AbstractTcpLib; Because it abstracts you from the details of sending your data.

But what if I WANT to work with bytes?

Of course, serializing objects and deserializing them at the remote end uses some CPU resourses. I’m using the .NET binary serializer, and although it is relatively fast and efficient, those among us who are interested in raw performance will see this as an issue. The good news is that byte arrays aren’t serialized. Why? Because, well – that would be silly. The socket class sends byte arrays. Serializing a byte array would just use CPU resourses needlessly, and add a few bytes to the byte array to include information about the byte array object – all for nothing.

So if you really want to send Byte[] because you want the best possible network performance, or you’re concerned about the binary serializer for one reason or another, or you just would rather work with bytes – then go right ahead. Just drop your byte array into the .Send() function.

Security – Encryption and Authentication

Everyone is concerned about security these days. I consult for an access control and video survalence company, and of course security is at the top of their list of their priorities. Everything must be encrypted. Everything must be authenticated. It’s so important in our society today, that it seems to me that no communications library can be considered complete without security fucnctionality of some kind. In this library, I included two different methods of implementing security measures:

AES256 Encryption: The server class works in two modes – encrypted mode and mixed mode. In encrypted mode, all clients must have the correct pre-shared key to connect. Connecting clients are required to register themselves with the server as either a session or a subsession immediately uppon connection. If the server can not decrypt this registration request with it’s own PSK, the connection is rejected and the client will simply be disconnected with no error message whatsoever. If the server can decrypt the registration request, it immediately changes the session PSK to a new randomly generated one using RNGCryptoServiceProvider.

LDAP Authentication: The server can be configured to require windows authentication. Before connecting with your Client, enter credentials using the .Login(username, password) function. Credentials are immediately stored as encrypted strings, and passed to the server in the regisration request. The server will attempt to authenticate the credentials against the domain the server machine is in, so sending a domain along with the username (ie: domain\username) is not necessary. If the server is not in a domain, it will attempt to authenticate against the windows workstation it is running on. If the client’s authentication fails, it receives an “authentication failure” message, and is disconnected.

Mixed Mode vs Encrypted Mode: In mixed mode, clients can create encrypted sessions or subsessions if they choose. It’s important to know that if a server is configured to require authentication, but not to require encryption and you create non-encrypted sessions or subsessions, then your windows credentials are being sent as serialized strings – and this is not secure. If you configure the server to require authentication, use encryption also.

Files, Files and more files: Concurrent file transfers per client

So you need to send a file. Ok, great – this library has you covered. Need to send two? Sure, of course. How would you like to send them – one at a time, or both at the same time? Or maybe three or four at the same time? It’s completely up to you, and what you think your hardware can handle.

AbstractTcpLib sends files over subsessions. In fact, if you look at the Client class, you won’t see a SendFile() or GetFile() function there at all – those functions live in the Session class. You create a file transfer by first creating a subsession or two. Then you get a refrence to your subsessions (which are Session objects) by using Client.GetSubSession(), and then call Session.SendFile() or Session.GetFile().

You can create as many as you like, and you can transfer files between your Client and your Server, or between your Client and another connected client.

To transfer files between clients, create a linked subsession first. Then get a refrence to it using Client.GetSubSession() the same way you would any other subsession.

Subscribing to File Transfer Events: There are three delegates to subscribe to when initiating or receiving a file transfer: TransferProgress(Uint16 percentComplete)TransferError(String errorMessage), and TransferComplete(). I believe this is self explanatory, and thy work the way you think they will. If you have any questions, please feel free to ask.

Files are transfered using the FileTransfer class. During a transfer, a file transfer object will be associated with each end of a subsession. Subscribing to the Server.receivingAFile(FileTransfer) or Client.receivingAFile(FileTransfer) delegate will allow you to become aware of when you are receiving a file, and you can also subscribe to the transferProgress, transferError and transferComplete delegates on the fileTransfer object so you can track the progress of the incomming file. If you don’t want to allow the transfer to continue you can always .Cancel() the transfer on the receiving end before it completes.

Some Details

The Client, Server and Session classes talk to each other using XML. Because this library serializes sent objects, I never needed to parse any xml myself. Instead, I used an xml parser I built called XmlObject. Using XmlObject and this library’s ability to serialize objects and pass them back and fourth, I was able to easily and clearly create XmlObject(s), add parameters and other data, pass them to a remote machine where they arrive as XmlObjects, use the tools available in the XmlObject class to easily get at the passed data. 

This library allows some of that communication to be filtered up to you, so that you are notified when sessions disconnect, when they connect and register themselves, when subsessions are created, when there is a connection failure due to incompatible encryption keys, authentication failure, etc. So your client and server will be receiving XmlObjects in your callbacks. Don’t be afraid… they are your friends.

Using the Code

Both the server and the client use a delegate to pass you incomming data (your sent objects) as it comes in. These delegates have a single CommData object as a parameter. A CommData object is just a wrapper for your passed object. It contains the serialized byte array that came in, a deSerialized Object that is the object you put into sendbytes, only it isn’t your String – it’s an Object. You could simply test it to make sure it’s your String using typeof(String), and then cast it to a String or var, or use the CommData.GetObject(), like this:

 this.client = new Client((Core.CommData data) =>
            {
                
                var o = data.deSerialized;

                if (o.GetType() == typeof(XmlObject) && ((XmlObject)o).Name.Equals("ATcpLib"))
                {
                    XmlObject xml = (XmlObject)data.deSerialized;

                    String msg      = xml.GetAttribute("", "internal");
                    String originId = xml.GetAttribute("", "id");

                    
                    if(msg.Contains("disconnected") && originId.Equals(client.GetConnectionId()))
                    {
                        UI(() =>
                        {
                            lblStatus.Text = "Disconnected.";
                            btConnect.Text = "Connect";
                            lbSubSessions.Items.Clear();
                        });
                    }

                    if (msg.Contains("CreateSubSession") && originId.Equals(client.GetConnectionId()) && xml.GetAttribute("", "status").Equals("true"))
                    {
                        UI(() =>
                        {
                            lbSubSessions.Items.Add(xml.GetAttribute("", "subSessionName"));
                        });
                    }

                    
                    if (msg.Contains("disconnected") && !originId.Equals(client.GetConnectionId()))

As you see, the client’s constructor takes the incomming data delegate. You connect to the server as follows:

String errMsg = "";
client.Login(tbUserName.Text, tbPassword.Text);
if (!client.Connect(System.Net.IPAddress.Parse(tbIpAddress.Text.Trim()), ushort.Parse(tbPort.Text.Trim()), tbSessionId.Text.Trim(), out errMsg, cbUseEncryption.Checked, tbPsk.Text))
{ MessageBox.Show(errMsg, "Connection failed.", MessageBoxButtons.OK, MessageBoxIcon.Error); return;
}

When you connect, a session object is created to handle your client’s connection to the server, and added to the server’s SessionCollection. You can send objects to the server using your session if you choose, or you can create subsessions. 

Subsessions are sessions also. When you create a subsession with your client, a Session object is creates and added to your client’s subsession collection. On the server, your subsession is registered and added to your session’s subsession collection.

To send data to the server (or to a peer) over your client’s session, use the Client.Send() (or Session.Send()) function, as follows:

String errMsg = ""; if(!client.Send(tbMessage.Text, out errMsg)) { MessageBox.Show(errMsg, "Send failed.", MessageBoxButtons.OK, MessageBoxIcon.Error); }

To send data over a subsession, first get the subsession using it’s name (String sessionId) as follows:

Session session = null;
String errMsg = "";
if(!client.GetSubSession(lbSubSessions.SelectedItems[0].ToString(), out session, out errMsg))
{ MessageBox.Show(errMsg, "Could not get subsession.", MessageBoxButtons.OK, MessageBoxIcon.Error);
} else
{ if(!session.Send(tbMessage.Text, out errMsg)) { MessageBox.Show(errMsg, "Send failed.", MessageBoxButtons.OK, MessageBoxIcon.Error); }
}

To subscribe to the server  (or client’s) Incomming FileTransfer delegate, do it as follows (this example is taken from the example application. As such, it is updating a listview with the transfer’s information):

 server.receivingAFile = (FileTransfer transfer) =>
{ ListViewItem lvi = new ListViewItem(transfer.FileName()); FileTransfer.TransferComplete complete = null; FileTransfer.TransferProgress updateProgress = null; FileTransfer.TransferError transferError = null; lvi.SubItems.Add("0%"); lvi.SubItems.Add(transfer.DestinationFolder()); lvi.SubItems.Add("Transfering file"); complete = () => { UI(() => lvi.SubItems[3].Text = "Complete"); transfer.transferComplete -= complete; transfer.transferError -= transferError; transfer.transferProgress -= updateProgress; }; updateProgress = (ushort percentComplete) => { UI(() => { lvIncommingFiles.BeginUpdate(); lvi.SubItems[1].Text = percentComplete.ToString() + "%"; lvIncommingFiles.EndUpdate(); }); }; transferError = (String errorMessage) => { UI(() => { lvi.SubItems[2].Text = "Error: " + errorMessage; lvi.ForeColor = Color.Red; }); transfer.transferComplete -= complete; transfer.transferError -= transferError; transfer.transferProgress -= updateProgress; }; transfer.transferComplete += complete; transfer.transferError += transferError; transfer.transferProgress += updateProgress; UI(() => lvIncommingFiles.Items.Add(lvi));
};

Wait – isn’t there more?

Of course. Please see the example application for anything else you need to know how to do. If you can’t find it there, or you run into an iseue, please feel free to ask below.

History

None yet.

Will you be adding anything in the future?

Definitelly.

Cooperative throttling, for starters. More as I think of it, or you ask for it, and I have the time.

Found a Bug?

This library is cool… if I do say so myself. I have enjoyed building it, and to a small degree testing it. But there’s a lot of code here, and there’s just no way I could test it as thoroughly as I would like… and it’s brand new.

I’m going to use it in future projects, and post fixes as bugs appear. If you find one, please feel free to let me know. I’ll fix it and post a new build as I’m able.

Thanks,

– Pete

LEAVE A REPLY