Holy Smokes!

Saturday, November 8, 2008

"Traditional Sockets" in .NET -- Part 2

The other day I started talking about sockets in .NET. The whole reason this has been on my mind is because I have been discussing them with a coworker of mine. In Part 1 we created a simple server that would allow a telnet client to connect, the server would transmit a simple text message, and the server would terminate. Creating a simple client is not an awful lot different than creating a server. So I will just assume you have saved your Server project. If not, the code can be copy/pasted in part 1. Ok, you have your Server again? Great. We need to create a new project for the client. We can either do this in a new solution, in the usual way, or we can do this in the same solution. Keep in mind that if you choose to do your client in a new solution, any time I say run solution, I mean run the server, then the client. If you choose to add the client project to the current solution, Locate the solution node in the Solution Explorer Treeview Control, as seen here. The idea is that you need to right click this Solution Node to bring up a context menu, locate a sub menu called "Add", and in the sub menu select "Project". Creating this project is not an awful lot different than creating any other project. It will bring up a project template selection window, I would just select another Console application. The only trick once this project is selected, is to ensure that both the Client and the Server are starting at the same time. This can be accomplished by right clicking on that same Solution Node, to bring up a context menu, and by selecting properties on that context menu. This will bring up the solution Properties dialog. I have already set the properties properly. You want to chance the radio button selection from 'Single' or 'Current' to 'Multiple'. Then you want to choose 'Start' in both projects. Lastly, you want to make sure the Server is starting first. You can do this by selecting a project in the grid, and using the arrow buttons on the side. Once that is done, you can hit OK. Now we can move on making the client connect to the server. So make sure the source window for Program .cs in the Client project is open for editing. And make sure again that you are importing from the Networking Namespace and the Socket Namespace.
using System;
using System.Net;
using System.Net.Sockets;



Now move down to the Main function. I told you that we would define a socket in the client application in a very similar way. When we construct the socket, we are defining how the socket is going to behave, and this is highly important. So why don't we start with pretty well the same line of code, except that we will give it a more convincing name.

Socket socketClient =
new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
 

Looks similar right? That's good, because it should. Now if we think about the terminology, A Server Accepts connections from clients, whereas a client Connects to a server. You will notice that a member function for socketClient exists called Connect. Connect


is a little more complicated than Accept, in that it actually takes a parameter. You will notice that the parameter it expects is an EndPoint. Do you remember that an EndPoint on the server was basically an IP address and a Port? I hope your connecting the dots. What do you suppose would happen if we supplied an endpoint, that described a location where the server was listening for connections. I believe we specified the last endpoint as..
IPEndPoint epLocal = new IPEndPoint(IPAddress.Any, 13001);


Oh but wait. We had decided to use the IPAddress.Any constant. We actually need to give it an address. If I were to leave you ponder about this for a while, or to try and make an address fit in there, you might eventually come to realize that even though IP addresses have numbers in them, as we generally express them, they are actually strings. So are web addresses. The string notation, 192.168.1.1 is expressed that way for a lot of reasons. Most of which is very much out of scope of this tutorial. In .NET, an address is a long number. You cant just plug in 192.168.1.1. And of course we cannot plug in www.google.com. Instead we use a facility called DNS (Domain Name System.) And while an IP notated string does not need to be looked up, it can be translated, an address like www.google.com would certainly have to be looked up. Luckily in .NET we can use the very same facility to look up either.
Dns.GetHostEntry("127.0.0.1");
Dns.GetHostEntry(www.google.com);


Very straight forward right? Good. Now also keep in mind that a lookup can return more than one IP address, or none. It's a great idea to make sure you are testing the output from such a lookup. We will not. This example is so simple, that not much will go wrong, and I will get into more detail if someone really wants to know. So lets create an EndPoint that can locate the server.
IPEndPoint epRemote =
new IPEndPoint(Dns.GetHostEntry("127.0.0.1").AddressList[0], 13001);


I of course renamed the IPEndPoint to make a little more sense. But now we can complete our connection. Why don't you make your Main function look something like this.
static void Main(string[] args) {
IPEndPoint epRemote =
new IPEndPoint(Dns.GetHostEntry("127.0.0.1").AddressList[0], 13001);
Socket socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketClient.Connect(epRemote);

return;
}


You could technically run this, and it would probably execute so quickly that you would have not much idea anything happened. If it worked correctly, both the server and the client consoles would open then close. Now I will interrupt here for a second. Because there is a possibility that this will not work. Especially if you are using Vista. Vista configurations that I have seen have IPV6 support enabled. And when we do a look up, the GetHostEntry will return multiple addresses. If the first address that is returned is IPV6, then you will get an error indicating that the address is incompatible with the protocol. If you get this message, you can use the immediate window to locate the proper index, or you can just keep incrementing the index until it works. If anyone has any issues with that, I will gladly provide more insight. Moving along. So why did we not see the text in the client? You might be thinking the window closed to fast to read it. That's not the only reason you didn't see anything. But for now, lets solve that problem. Place this snippit in the bottom of both the client and the server.
Console.WriteLine("Press  to terminate");
Console.ReadLine();

Your code in the server should look like this:

static void Main(string[] args) {
IPEndPoint epLocal = new IPEndPoint(IPAddress.Any, 13001);

Socket socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketServer.Bind(epLocal);
socketServer.Listen(4);
Socket socketClient = socketServer.Accept();

byte[] bytSend = System.Text.ASCIIEncoding.ASCII.GetBytes("Hello, World!");
socketClient.Send(bytSend, 0, bytSend.Length, SocketFlags.None);

Console.WriteLine("Press to terminate");
Console.ReadLine();

socketClient.Close();
socketServer.Close();
}


And your code in the client should look like this:

static void Main(string[] args) {
IPEndPoint epRemote = new IPEndPoint(Dns.GetHostEntry("127.0.0.1").AddressList[2], 13001);
Socket socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketClient.Connect(epRemote);

Console.WriteLine("Press to terminate");
Console.ReadLine();

return;
}


Try running the solution again. Both consoles stay open, but you only see the Message indicating that you can hit the enter key to terminate right? So why is it that the Telnet window gets the message, and our client does not. We do know that the client is connecting, because if you never ran the client, the server window would stay black, and you would get no Press Enter Message. Then if you attempted to connect with telnet again, you would get the expected result. The answer is simpler than you might think, or maybe I'm not giving you enough credit. Sockets work very similarly to working with files, or working with standard input / output. The truth is the client has in fact received the data, we just haven't read it out of the buffers yet. Why not add a read and print statement to the client.
static void Main(string[] args) {
IPEndPoint epRemote = new IPEndPoint(Dns.GetHostEntry("127.0.0.1").AddressList[2], 13001);
Socket socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketClient.Connect(epRemote);

byte[] bytRecv = new byte[13];
socketClient.Receive(bytRecv, 0, bytRecv.Length, SocketFlags.None);
Console.WriteLine(System.Text.ASCIIEncoding.ASCII.GetString(bytRecv, 0, bytRecv.Length));

Console.WriteLine("Press to terminate");
Console.ReadLine();

return;
}


Ok, there are a few things happening here. Again, I am making a byte array, and I am running a similar operation off of the socketClient as that operation that we ran against the server. This one is called receive. Pretty simple right? Then I am simply using the ASCII encoding to convert the binary data back into the string. Test it out! Look at that... it's magic. There is still a matter that we must discuss however, and it's very important. You see we created the byte array by allocating only 13 bytes of memory. This is not nearly sufficient for almost any application that we will write. So why did I do this? Why didn't I allocate larger? I could have allocated larger. That is not the issue. The issue lies with the read/recieve operation. You see much like the write operation, the read asks me to specify a byte array to read data into, an index to start reading from, and a size, or in this case length to read. I told it to consume the whole array. I knew the message "Hello, World!" was 13 bytes. Lets Experiment. Try setting the array size to 10, then run the application. Runs as expected, the client pulls only 10 of the 13 bytes and both ready to terminate. Now try to set it to 100. Then Run it. Does it run as expected? No, not exactly. I think what you will find is that you get the message, but you're getting a lot of extra 'nothings'. In this example, perhaps it's not much of a problem. As your programs get more complex, especially once you get into asynchronous sockets, this becomes a real issue. In some circumstances, your application could deadlock waiting for more data to be written to the buffer before ending the receive operation. Ok, I think we can leave this at that for now. A lot of ground has been covered in the last two posts. Maybe a good time for you to experiment and become comfortable with some of the concepts. Also, if you can try changing things, and seeing if you can break it, that would be another great experience. However, for those who want more... I also said I would get into how to enable to server to accept more clients. Probably the easiest way to do this with what we have is to use a loop that keeps executing the Accept statement. And I think for at least the purposes of explaining how simple Blocking Sockets are, and some of their shortcomings, that is exactly what we will do. Let's do something rash. Lets create an infinite loop that accepts connections and sends our welcome message. Yes, I realize this will cause unreachable code, and I'm fine with that for now. This is just to illustrate a point, so bear with me...
static void Main(string[] args) {
IPEndPoint epLocal = new IPEndPoint(IPAddress.Any, 13001);

Socket socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketServer.Bind(epLocal);
socketServer.Listen(4);

Socket socketClient;
for (; ; ) {
socketClient = socketServer.Accept();
byte[] bytSend = System.Text.ASCIIEncoding.ASCII.GetBytes("Hello, World!");
socketClient.Send(bytSend, 0, bytSend.Length, SocketFlags.None);
}

Console.WriteLine("Press to terminate");
Console.ReadLine();

socketServer.Close();
}


OK, we can see that the client that connects get's its message, and is given the option to hit enter and then terminate. But the server, still nothing. Lets try to connect to it with telnet now, without restarting the server. It works.. so how do we stop the server? I think that's a good question. I mean yes, we could just terminate the process, but how about something a little more solid/proper? I'm going to do something now that I didn't plan on doing right away, because I think it would be highly beneficial, again this will illustrate the strengths and weaknesses of Blocking Sockets vs. Asynchronous Sockets. Are you familiar with Asynchronous Programming? If you said no, don't fret. If you have programmed in almost any visual environment, you're more than likely more familiar with Asynchronous Programming than you even realize. Asynchronous Programming allows us to perform certain tasks. while our main thread continues executing. Or maybe more appropriately, it allows us to wait for certain things to happen while our main thread continues executing. The main thread would get interrupted when this task completes. If you picture in your mind, programming the result of clicking a button, you're in the right mindset, on how the code will look. If we replaced the Accept call with a BeginAccept() call, that would kick off an asynchronous Accept. This means that rather than wait for the connection on the current main thread, the server will wait for the connection, and let us know when a connection has been made. This means that the server will walk right over BeginAccept Call, and then display the message that states we can press the Enter Key and the server will terminate. (of course we no longer need the loop). Once a connection is made, our callback gets fired. This is a method we designate at the time we call BeginAccept(). That method being fired is our notification that the Accept operation has been completed. If we call EndAccept passing in our AsyncResult, we get our client socket just the same as though we had called Accept() straight up. And there is no reason why we cannot call BeginAccept() again. So clear out that Accept loop altogether. And create a new Method. This method will be used as a callback when the Accept operation completes.
static void OnAcceptComplete(IAsyncResult result) {
Socket socketServer = (Socket)result.AsyncState;
Socket socketClient = socketServer.EndAccept(result);
byte[] bytSend = System.Text.ASCIIEncoding.ASCII.GetBytes("Hello, World!");
socketClient.Send(bytSend, 0, bytSend.Length, SocketFlags.None);

socketServer.BeginAccept(OnAcceptComplete, socketServer);

return;
}


This is a special method signature. returning nothing, and taking only an IAsyncResult parameter, this is the signature that pretty well all Asynchronous Methods will expect as a callback. The first thing we are doing is extracting the server socket, which is expected to be the Asynchronous state. (I will explain that in a few minutes) Next, we are going to call EndAccept on the server socket. This returns the client socket just like calling Accept Directly, as well as cleaning up the Async Operation. Then we send the "Hello, World!" message the same way we did previously. And we attempt to wait for another connection asynchronously. Now in order to tie this altogether, we need to kick off the AsyncAccept pattern in the first place. This can be accomplished by replacing the original Accept with a Begin Accept. Make your server application look like this.
static void Main(string[] args) {
IPEndPoint epLocal = new IPEndPoint(IPAddress.Any, 13001);

Socket socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketServer.Bind(epLocal);
socketServer.Listen(4);

socketServer.BeginAccept(OnAcceptComplete, socketServer);

Console.WriteLine("Press to terminate");
Console.ReadLine();

socketServer.Close();
}

static void OnAcceptComplete(IAsyncResult result) {
Socket socketServer = (Socket)result.AsyncState;
Socket socketClient = socketServer.EndAccept(result);
byte[] bytSend = System.Text.ASCIIEncoding.ASCII.GetBytes("Hello, World!");
socketClient.Send(bytSend, 0, bytSend.Length, SocketFlags.None);

socketServer.BeginAccept(OnAcceptComplete, socketServer);
return;
}


Try and run the solution again now. You should see everything executes as expected. The client gets its welcome message, and a chance to terminate. So does the server. But if you don't close the server, you can start connecting telnet clients to it, notice they work as expected as well? This is really only one advantage to Asynchronous sockets. I will leave you on that note, because I know a lot of these concepts must be hard to swallow all at once. So please, if you have any questions, or I'm not clear enough on something, let me know, and I'll try and straighten you out. I suspect that will be the case, because my two year old is helping me out right now.. =) Next lesson, I will try not to introduce much more, but rather re-enforce some of these concepts. After all, if you can grasp all of this so far in just two lessons, you're doing way better than I did. I figure once we get some of these concepts down, I'll remove all the blocking stuff, and show you pure Asynchronous sockets, and some of the patterns that I use. I'd also like to show you some of the .NET features that actually make sockets convenient.

2 comments:

  1. This template is way better than the other one. Yeah I know this comment has nothing to do with this post :P

    But this is really good. Not a lot of people would put a lot of effort to explain like this.

    ReplyDelete
  2. Yeah, I appreciate the feedback anyways.
    I think I'll end up modifying the template to suit my personal tastes, but now I don't need to worry about the preformatted sections as much (Code samples)

    Now I have to go back and update this and the last post so they look more normal...

    ReplyDelete

Followers